最小コンテナランタイム自作(シェルスクリプトだけで動かすDocker)

2026-11-06 / シリーズ「コンテナとLinuxカーネルの物理」第6話 / Docker, Linux, コンテナ自作, シェルスクリプト, Namespaces, cgroups, pivot_root

「Dockerとは、巨大で難解なブラックボックスである」 多くの開発者が、無意識のうちにそう思い込んでいます。

Go言語で書かれた何万行ものソースコード、バックグラウンドで動き続けるデーモン(dockerd)、コンテナを管理する containerd やOCI準拠ランタイムの runc。 それらの専門用語が幾重にも重なることで、コンテナはまるで「特別な仮想化テクノロジー」であるかのように見えています。

しかし、本連載で暴いてきた物理の真実は全く逆でした。

これらはすべて、すでに私たちが使っているLinuxカーネルの中に、最初から標準機能として組み込まれている道具に過ぎません。

新企画Hの最終回となる本稿では、Dockerを一切使わず、Linuxの標準コマンドだけで「本物のコンテナ」を起動する最小ランタイム(わずか約50行のBashスクリプト)をゼロから組み上げます。 すべての魔術を剥ぎ取り、剥き出しのカーネル物理をこの手で証明しましょう。


1. コンテナ生成の設計図(物理パイプライン)

私たちがシェルスクリプトで作る自作ランタイム(仮称:mini-docker.sh)の物理的な処理シーケンスは、本家Docker(runc)の挙動と寸分違わず一致します。

1. ルートファイルシステムの準備 自作ランタイム:lowerdir(Alpine rootfs)と upperdir を OverlayFS で合成 2. 資源の首輪(cgroups)を作成 /sys/fs/cgroup にディレクトリを作り、メモリ上限(100MB)を設定 3. 視野の隔離(Namespaces) unshare で PID・UTS・MOUNT・IPC 空間を分離して子プロセスを起動 4. ルートディレクトリの完全入れ替え 子プロセスのPIDを cgroup に登録 → pivot_root で合成 dir を新しい「/」にし、旧ルートをアンマウント 5. 隔離環境でプロセスを実行 誕生したコンテナプロセス:exec /bin/sh(PID 1 として起動!)

なぜ chroot ではなく pivot_root なのか?

コンテナのルートファイルシステム(/)を差し替える際、古くからある chroot ではなく、現代のコンテナは必ず pivot_root システムコールを使います。

触って試す:最小ランタイムの起動ステップ実行

🐧 最小ランタイムの起動ステップ実行

mini-docker.sh がコンテナを1つ起動する流れを、「次へ」ボタンで1ステップずつ再現します。各ステップでカーネルに何が起きたかを確認してください。


2. 実装:わずか50行の完全自作コンテナスクリプト

それでは、自作コンテナランタイムの全貌です。 Linux環境(Ubuntu 22.04/24.04、WSL2など)で、root権限(sudo)で実行できる完全なBashスクリプトです。

#!/usr/bin/env bash
# ==============================================================================
# mini-docker.sh: Linux標準機能だけで動かす最小コンテナランタイム
# ==============================================================================
set -euo pipefail

# 1. コンテナIDと作業パスの定義
CONTAINER_ID="box_$(date +%s)"
BASE_DIR="/tmp/mini-docker/${CONTAINER_ID}"
ROOTFS_TAR="/tmp/alpine-minirootfs.tar.gz"

echo "=== [1/5] コンテナ環境の初期化中: ${CONTAINER_ID} ==="
mkdir -p "${BASE_DIR}"/{lower,upper,work,merged}

# 2. 軽量なベースイメージ(Alpine Linux)の調達
if [ ! -f "${ROOTFS_TAR}" ]; then
  echo ">>> Alpine Linux の rootfs をダウンロードしています..."
  curl -sSL "[https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-minirootfs-3.20.0-x86_64.tar.gz](https://dl-cdn.alpinelinux.org/alpine/v3.20/releases/x86_64/alpine-minirootfs-3.20.0-x86_64.tar.gz)" -o "${ROOTFS_TAR}"
  tar -xzf "${ROOTFS_TAR}" -C "${BASE_DIR}/lower"
else
  tar -xzf "${ROOTFS_TAR}" -C "${BASE_DIR}/lower"
fi

# 3. OverlayFS によるレイヤードファイルシステムの合成(第3話の物理)
echo "=== [2/5] OverlayFS をマウント中 ==="
mount -t overlay overlay \
  -o lowerdir="${BASE_DIR}/lower",upperdir="${BASE_DIR}/upper",workdir="${BASE_DIR}/work" \
  "${BASE_DIR}/merged"

# 4. cgroups v2 によるメモリ上限(100MB)の設定(第2話の物理)
echo "=== [3/5] cgroups による資源制限(メモリ 100MB)を設定中 ==="
CGROUP_PATH="/sys/fs/cgroup/mini-docker-${CONTAINER_ID}"
mkdir -p "${CGROUP_PATH}"
echo "104857600" > "${CGROUP_PATH}/memory.max" # 100MB
echo "0" > "${CGROUP_PATH}/memory.swap.max"    # スワップ禁止(即OOMさせる)

# 5. コンテナ内で実行するセットアップスクリプト
CONTAINER_SETUP="/tmp/container-entry.sh"
cat << 'EOF' > "${CONTAINER_SETUP}"
#!/bin/sh
set -e
# ホスト名を変更(UTS名前空間の独立)
hostname "mini-docker-isolated"

# pivot_root でルートディレクトリを完全に切り替える
cd /merged
mkdir -p old_root
pivot_root . old_root

# 仮想ファイルシステムの再マウント
mount -t proc proc /proc
mount -t sysfs sysfs /sys

# 古いホストのルートを切り離して完全に隠滅
umount -l /old_root
rm -rf /old_root

# 隔離されたシェルを起動!
echo "=========================================================="
echo "  ようこそ!Linux標準機能だけで組み上げた自作コンテナへ"
echo "=========================================================="
exec /bin/sh
EOF
chmod +x "${CONTAINER_SETUP}"

# 6. Namespacesの切り離しと起動(第1話の物理)
echo "=== [4/5] Namespaces を切り離し、コンテナを起動します ==="
# 現在のPIDをcgroupsに登録してからunshareを実行
echo $$ > "${CGROUP_PATH}/cgroup.procs"

# 終了時のクリーンアップ処理を登録
cleanup() {
  echo ""
  echo "=== [5/5] コンテナ終了・後始末中 ==="
  umount -l "${BASE_DIR}/merged/proc" 2>/dev/null || true
  umount -l "${BASE_DIR}/merged/sys" 2>/dev/null || true
  umount -l "${BASE_DIR}/merged" 2>/dev/null || true
  rm -rf "/sys/fs/cgroup/mini-docker-${CONTAINER_ID}" 2>/dev/null || true
  rm -rf "${BASE_DIR}" "${CONTAINER_SETUP}"
  echo "クリーンアップ完了。ホストOSは完全に無傷です。"
}
trap cleanup EXIT

# unshare で独立空間を生成し、中へ突入!
unshare --mount --uts --ipc --pid --fork "${CONTAINER_SETUP}"

3. 実証実験:自作コンテナの内部で物理挙動をテストする

このスクリプトに実行権限を与え、sudo ./mini-docker.sh を叩いてみましょう。 一瞬でAlpine Linuxのプロンプト(#)が立ち上がります。

ここが本物のコンテナであることの「動かぬ証拠」を、1つずつ検証していきます。

テスト①:PID 1 の確認(Namespacesの検証)

コンテナ内でプロセス一覧を表示します。

/ # ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    5 root      0:00 ps aux

ホストOSのプロセスは何一つ見えません。 自作スクリプトが産み落とした /bin/sh が、**正真正銘の「PID 1(初代プロセス)」**として君臨しています。

テスト②:ホスト名の独立性(UTS Namespacesの検証)

/ # hostname
mini-docker-isolated

コンテナ内でホスト名を書き換えても、ホストOS側のターミナルを開いて hostname を叩くと、ホスト側の名前(例: ubuntu-desktop)は1ミリも変わっていません。

テスト③:Copy-on-Writeの実証(OverlayFSの検証)

コンテナ内で、新しいファイルを作成してみます。

/ # echo "This is inside container" > /test-file.txt
/ # cat /test-file.txt
This is inside container

この状態で、ホストマシンの別のターミナルから実体ディレクトリを覗いてみます。

# ホスト側の別ターミナルで確認
$ ls /tmp/mini-docker/box_*/upper
test-file.txt   # ★ upperdirにだけ差分が書き込まれている!

$ ls /tmp/mini-docker/box_*/lower
# ★ lowerdir(ベースイメージ)には test-file.txt は存在しない!

第3話で解説した通りのCopy-on-Writeが完璧に機能しています。

テスト④:OOM Killerの処刑実験(cgroupsの検証)

最後に、この自作コンテナに課した「メモリ上限100MB」の首輪が機能しているか、150MBのメモリを乱暴に確保するコマンドを叩いてみます。

/ # python3 -c "a = 'x' * (150 * 1024 * 1024)" 2>/dev/null || dd if=/dev/zero of=/dev/null bs=150M count=1
Killed

限界を超えた瞬間に、カーネルのOOM Killerが発動して Killed の文字とともにプロセスが即座に撃ち落とされました。 ホストマシンのRAMは何十ギガバイトも空いているにもかかわらず、コンテナ内のプロセスは100MBの壁を踏み越えた瞬間に処刑されたのです。


4. OCI(Open Container Initiative)と Docker の正体

自作スクリプトを動かした今、私たちはコンテナ業界の「業界標準規格(エコシステム)」の全体像を、上空1万メートルからクリアに見下ろすことができます。

👤 ユーザー / CLI 🐳 Docker Engine / Podman docker run イメージ展開・ダウンロード 📦 containerd / CRI-O 📄 OCI Runtime 仕様 config.json + rootfs の標準バンドル OCI標準バンドル作成 実行命令 ★ runc / crun(低レベル・コンテナランタイム) 🐧 Linuxカーネル(本連載で解剖した物理) Namespaces unshare / clone cgroups sysfs 書込 OverlayFS mount -t overlay VFS pivot_root Seccomp & Caps libseccomp / prctl

私たちが今回書いた約50行のシェルスクリプトが行ったこと。 それこそが、業界標準の低レベルランタイムである**runc(Dockerが内部で呼び出している心臓部)がC言語やGo言語で行っていることのすべて**です。

  1. OCI標準仕様(Open Container Initiative)とは、単に**「コンテナの設定(Namespacesやcgroupsのパラメータ)を config.json という統一フォーマットのJSONで記述しよう」**と定めた取り決めに過ぎません。
  2. containerd とは、Docker Hubからイメージをダウンロードして解凍し、その config.json を組み立てて runc に手渡す「手配師」に過ぎません。
  3. そして runc は、手渡されたJSONの指示に従って、私たちがシェルスクリプトで書いたように unshare、cgroups、mount、pivot_root システムコールを発行しているだけなのです。

結論:すべての魔術は「Linuxカーネルの幾何学」へと還る

全6話にわたって私たちが目撃してきたのは、仮想化というバズワードの裏に隠された**「Linuxカーネルの純粋な幾何学」**でした。

コンテナとは、ハードウェアのシミュレーションではありません。 **「長年にわたるOSの進化が培ってきた、美しくも無慈悲なカーネルの制約機構を極限まで調和させた芸術」**です。

この物理の芯を掴んだあなたにとって、DockerもKubernetesも、もはや得体の知れないブラックボックスではありません。 エラーログの1行、リソース枯渇の1秒の裏で、カーネルがどのような力学で動いているのかが、手に取るように見えているはずです。


📝 3行で頭に入る『試験・実務・投資の要点まとめ』

  1. 【コンテナの正体】 特殊な仮想化ソフトではなく、unshare(視覚)、cgroups(資源)、OverlayFS(ディスク)、pivot_root(根拠地)を束ねた通常のLinuxプロセスに過ぎない。
  2. 【pivot_rootの安全性】 chroot がポインタを書き換えるだけで脱獄リスクを持つのに対し、pivot_root はマウントツリーごと反転させて古いホストを完全アンマウント・隠滅する。
  3. 【OCIとruncの本質】 Dockerのエコシステムは「イメージの配布とJSON設定作成」を担う周辺層であり、最深部の実行実体(runc)が行っている物理は本稿のBashスクリプトと完全に同一である。

確認クイズ

📝 確認クイズ(3問)