最小コンテナランタイム自作(シェルスクリプトだけで動かすDocker)
「Dockerとは、巨大で難解なブラックボックスである」 多くの開発者が、無意識のうちにそう思い込んでいます。
Go言語で書かれた何万行ものソースコード、バックグラウンドで動き続けるデーモン(dockerd)、コンテナを管理する containerd やOCI準拠ランタイムの runc。
それらの専門用語が幾重にも重なることで、コンテナはまるで「特別な仮想化テクノロジー」であるかのように見えています。
しかし、本連載で暴いてきた物理の真実は全く逆でした。
- 第1話:視野を狭窄させる「Namespaces」
- 第2話:リソースの胃袋を縛る「cgroups」
- 第3話:セル画のように重ねる「OverlayFS」
- 第4話:仮想の直結ケーブルを引く「veth & Bridge」
- 第5話:rootの牙を抜く「Capabilities & Seccomp」
これらはすべて、すでに私たちが使っているLinuxカーネルの中に、最初から標準機能として組み込まれている道具に過ぎません。
新企画Hの最終回となる本稿では、Dockerを一切使わず、Linuxの標準コマンドだけで「本物のコンテナ」を起動する最小ランタイム(わずか約50行のBashスクリプト)をゼロから組み上げます。 すべての魔術を剥ぎ取り、剥き出しのカーネル物理をこの手で証明しましょう。
1. コンテナ生成の設計図(物理パイプライン)
私たちがシェルスクリプトで作る自作ランタイム(仮称:mini-docker.sh)の物理的な処理シーケンスは、本家Docker(runc)の挙動と寸分違わず一致します。
なぜ chroot ではなく pivot_root なのか?
コンテナのルートファイルシステム(/)を差し替える際、古くからある chroot ではなく、現代のコンテナは必ず pivot_root システムコールを使います。
chrootの脆弱性:chrootはプロセスが見ている「現在のルートのポインタ」を書き換えるだけです。内部でroot権限を持つプロセスがファイル記述子(fd)を辿る細工を行うと、外側のホストディレクトリへ脱出(chroot脱獄)できてしまいます。pivot_rootの絶対隔離: マウント空間(Mount Namespace)の中で、「ファイルシステムのマウントツリーそのもの」を物理的に反転させます。古いホストのルートディレクトリを一度サブディレクトリに退避させた後、umount -lで完全に切り離して消滅させるため、物理的に脱出する道が100%遮断されます。
触って試す:最小ランタイムの起動ステップ実行
🐧 最小ランタイムの起動ステップ実行
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万メートルからクリアに見下ろすことができます。
私たちが今回書いた約50行のシェルスクリプトが行ったこと。
それこそが、業界標準の低レベルランタイムである**runc(Dockerが内部で呼び出している心臓部)がC言語やGo言語で行っていることのすべて**です。
- OCI標準仕様(Open Container Initiative)とは、単に**「コンテナの設定(Namespacesやcgroupsのパラメータ)を
config.jsonという統一フォーマットのJSONで記述しよう」**と定めた取り決めに過ぎません。 containerdとは、Docker Hubからイメージをダウンロードして解凍し、そのconfig.jsonを組み立ててruncに手渡す「手配師」に過ぎません。- そして
runcは、手渡されたJSONの指示に従って、私たちがシェルスクリプトで書いたようにunshare、cgroups、mount、pivot_rootシステムコールを発行しているだけなのです。
結論:すべての魔術は「Linuxカーネルの幾何学」へと還る
全6話にわたって私たちが目撃してきたのは、仮想化というバズワードの裏に隠された**「Linuxカーネルの純粋な幾何学」**でした。
- 第1話:プロセスは
Namespacesによって視覚を遮断され、小さな孤島に閉じ込められた。 - 第2話:プロセスは
cgroupsによって胃袋を縛られ、上限を超えれば即死する首輪を嵌められた。 - 第3話:ファイルシステムは
OverlayFSによって不変のセル画となり、変更点だけが薄い皮に蓄積された。 - 第4話:ネットワークは
vethとdocker0とiptablesによって、メモリ上に仮想の電脳都市を敷設した。 - 第5話:特権は
CapabilitiesとSeccompによって分子に解体され、rootを名乗る巨人は牙を抜かれた。 - 第6話:そしてそれらは、わずか50行のスクリプトで束ねられる「Linuxの標準機能」に過ぎなかった。
コンテナとは、ハードウェアのシミュレーションではありません。 **「長年にわたるOSの進化が培ってきた、美しくも無慈悲なカーネルの制約機構を極限まで調和させた芸術」**です。
この物理の芯を掴んだあなたにとって、DockerもKubernetesも、もはや得体の知れないブラックボックスではありません。 エラーログの1行、リソース枯渇の1秒の裏で、カーネルがどのような力学で動いているのかが、手に取るように見えているはずです。
📝 3行で頭に入る『試験・実務・投資の要点まとめ』
- 【コンテナの正体】 特殊な仮想化ソフトではなく、
unshare(視覚)、cgroups(資源)、OverlayFS(ディスク)、pivot_root(根拠地)を束ねた通常のLinuxプロセスに過ぎない。 - 【pivot_rootの安全性】
chrootがポインタを書き換えるだけで脱獄リスクを持つのに対し、pivot_rootはマウントツリーごと反転させて古いホストを完全アンマウント・隠滅する。 - 【OCIとruncの本質】 Dockerのエコシステムは「イメージの配布とJSON設定作成」を担う周辺層であり、最深部の実行実体(runc)が行っている物理は本稿のBashスクリプトと完全に同一である。