Dockerコンテナは「軽量な仮想マシン」ではありません。実態はLinuxカーネルの3つの仕組みで隔離されたプロセスです。この仕組みを理解すると、コンテナの起動が速い理由・メモリ制限が効く理由・ファイルシステムが独立している理由がすべて説明できるようになります。
本記事では 名前空間(namespaces)・cgroups・ユニオンファイルシステム(overlayfs)の3つをシミュレータと図解で掘り下げます。1-1でVM vs コンテナの違いを学んだ方が次に読む記事として設計しています。
目次
- コンテナの正体は「隔離されたプロセス」
- 名前空間(namespaces)— 「見える世界」を分離する
- PID名前空間:コンテナ内でPID 1が生まれる仕組み
- NET・MNT・UTS:ネットワーク・ファイルシステム・ホスト名の分離
- cgroups — リソース使用量を制限・計測する
- ユニオンファイルシステム — イメージのレイヤー構造
- 3つの仕組みが合わさってコンテナになる
- コマンドで確認する(Linux環境)
- まとめ
1. コンテナの正体は「隔離されたプロセス」
「Dockerコンテナ」という言葉からは仮想マシンのようなイメージを持ちがちですが、実態はずっと単純です。
コンテナは、Linuxカーネルの機能によって「見える世界」と「使えるリソース」を制限された、ただの Linux プロセスです。ゲストOSも独立したカーネルも存在しません。
docker run を実行したとき、内部では次のシステムコールが呼ばれています。
# Docker(runc)が内部で呼び出す主要なシステムコール(概念)
clone(CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC, ...)
# → 新しい名前空間グループを持つプロセスを生成する
clone() は Linux の「プロセスを生成する」システムコールです。フラグによって「どの名前空間を新しく作るか」を指定します。VMのようにハードウェアをエミュレートするのではなく、カーネルが提供する「見せ方の仕組み」を使って隔離するだけです。これがコンテナの起動がミリ秒単位で終わる理由です。
この隔離を実現する仕組みが次の3つです。
| 仕組み | 役割 | 隔離・制限する対象 |
|---|---|---|
| 名前空間(namespaces) | 「見える世界」を分離 | PID・ネットワーク・ファイルシステム・ホスト名 など |
| cgroups | 「使えるリソース」を制限・計測 | CPU・メモリ・I/O・プロセス数 など |
| ユニオンファイルシステム(overlayfs) | 「ファイルシステム」を分離・重ね合わせ | イメージのレイヤー・書き込み可能レイヤー |
2. 名前空間(namespaces)— 「見える世界」を分離する
名前空間とは、「カーネルリソースのビュー(見え方)」をプロセスごとにカスタマイズする仕組みです。同じカーネルを共有しながら、各プロセスグループが自分だけの世界を持っているように見せます。
Linux では次の7種類の名前空間が利用できます。Dockerはデフォルトで5〜6種類を使ってコンテナを作ります。
| 名前空間 | フラグ | 隔離されるもの | Dockerでの使用 |
|---|---|---|---|
| pid | CLONE_NEWPID | プロセスID(PID) | 常に使用(コンテナ内PID 1) |
| net | CLONE_NEWNET | ネットワークインターフェース・ルーティング・ポート | 常に使用(コンテナ独自のIPアドレス) |
| mnt | CLONE_NEWNS | マウントポイント(ファイルシステムの見え方) | 常に使用(コンテナのルートファイルシステム) |
| uts | CLONE_NEWUTS | ホスト名・ドメイン名 | 常に使用(コンテナ独自のホスト名) |
| ipc | CLONE_NEWIPC | SysV IPC・POSIXメッセージキュー | 常に使用(プロセス間通信の分離) |
| user | CLONE_NEWUSER | UID・GIDのマッピング | rootlessモード(Docker 20.10+)で使用 |
| cgroup | CLONE_NEWCGROUP | cgroupのルートディレクトリの見え方 | Linux 4.6+ / Docker 20.10+で使用 |
mount 名前空間はLinuxカーネルに最初に実装された名前空間(2002年・Linux 2.4.19)であり、当時は「名前空間」といえばこれだけでした。そのため省略形の
NEWNS(New Namespace)が今もそのまま使われています。
3. PID名前空間:コンテナ内でPID 1が生まれる仕組み
PID名前空間は最もわかりやすい例です。コンテナ内でプロセス一覧を見ると、プロセスが数個しかなく、かつ最初のプロセスのPIDが 1 から始まります。
これは「コンテナが独立したOSを持っている」からではありません。カーネルが「このプロセスグループにはPIDを1から振り直して見せる」というビューを提供しているだけです。
| PID | COMMAND |
|---|---|
| 1 | systemd |
| 2 | kthreadd |
| 3 | rcu_gp |
| … | (多数のカーネルスレッド) |
| 856 | systemd-resolved |
| 923 | sshd |
| 3842 | nginx: master ← 本当のPID |
| 3857 | nginx: worker |
| PID | COMMAND |
|---|---|
| 1 | nginx: master process nginx |
| (← コンテナの “init”) | |
| 29 | nginx: worker process |
コンテナ内からは PID 1 に見える
コンテナ内の PID 1 の重要性
Linuxではプロセス1(init / systemd)が特別な役割を担います。親を失った子プロセス(孤立プロセス)を引き取り、wait() を呼んでゾンビプロセスを回収します。
コンテナ内でも同じ役割が必要です。アプリケーションがゾンビ回収を実装していない場合、コンテナ内のPID 1に直接アプリを起動するとゾンビプロセスが蓄積する問題が起きます。これを解決するのが軽量initプロセス tinidocker run –init フラグで有効化できます)です。
# tini を使ってゾンビプロセスを適切に回収する
docker run --init nginx
# コンテナ内のプロセス一覧(PID 1 が tini になる)
docker exec <container> ps aux
# → PID COMMAND
# → 1 /sbin/tini -- nginx -g daemon off;
# → 7 nginx: master process
# → 21 nginx: worker process
下のシミュレータで、ホストとコンテナの「見える世界」の違いを体験してみてください。
4. NET・MNT・UTS:ネットワーク・ファイルシステム・ホスト名の分離
NET 名前空間 — 独立したネットワークスタック
NET名前空間により、コンテナは独立したネットワークインターフェース・IPアドレス・ルーティングテーブル・ポート番号空間を持ちます。ホストとコンテナが同じポート番号(例:80番)を同時に使っても衝突しないのはこの仕組みのためです。
docker0: 172.17.0.1(仮想ブリッジ)
docker run -p 8080:80 のポートマッピングは、ホストの8080番ポートへのパケットをコンテナのNET名前空間内の80番ポートへ転送するルールを iptables(または nftables)に追加することで実現しています(第5章で詳しく解説)。
MNT 名前空間 — コンテナ独自のルートファイルシステム
MNT名前空間により、コンテナは独自のマウントポイントのツリーを持ちます。コンテナ内で / を参照すると、イメージで定義されたファイルシステムが見えます。ホストの /etc/passwd などは(バインドマウントで明示しない限り)見えません。
UTS 名前空間 — 独自のホスト名
UTS(Unix Time-sharing System)名前空間はホスト名とNISドメイン名を分離します。コンテナ内で hostname を実行するとコンテナIDの先頭12文字が返り、ホストのホスト名とは独立しています。
# ホストのホスト名
hostname
# → server01
# コンテナ内のホスト名(デフォルトはコンテナIDの先頭12文字)
docker exec demo hostname
# → a3f2b1c4d5e6
# --hostname オプションでカスタムホスト名を設定できる
docker run --hostname myapp nginx
5. cgroups — リソース使用量を制限・計測する
名前空間は「見える世界」を分離しますが、リソース(CPU・メモリ・ディスクI/O)の消費量まで制限するわけではありません。1つのコンテナが暴走してホスト全体のメモリを食い尽くすことを防ぐのが cgroups(Control Groups)です。
cgroups はLinuxカーネルのサブシステムで、プロセスグループに対してリソースの「上限設定」「計測」「優先度制御」を提供します。
| コントローラー | 制限・計測の対象 | Dockerオプション例 |
|---|---|---|
| memory | メモリ・スワップの使用量 | --memory=256m |
| cpu | CPU時間のシェア・クォータ | --cpus=0.5 |
| io(v1: blkio) | ディスクI/Oの帯域・IOPS | --device-read-bps |
| pids | 生成できるプロセス数(fork bomb対策) | --pids-limit=100 |
| cpuset | 使用できるCPUコアの番号 | --cpuset-cpus=0,1 |
cgroups v1 と v2
cgroups には2つのバージョンがあり、現在はv2への移行が進んでいます。
| 比較項目 | cgroups v1 | cgroups v2(推奨) |
|---|---|---|
| マウントパス | /sys/fs/cgroup/{コントローラー}/ |
/sys/fs/cgroup/(統一階層) |
| デフォルト採用OS | CentOS 7、Ubuntu 20.04以前 | Ubuntu 22.04+、Fedora 31+、Debian 11+ |
| コントローラー管理 | コントローラーごとに別の階層 | 単一の統一階層 |
| Dockerの対応 | Docker 1.x〜(長年の実績) | Docker 20.10+ |
# メモリ256MB・CPU 0.5コアに制限したコンテナを起動
docker run -d --name limited \
--memory=256m \
--cpus=0.5 \
--pids-limit=100 \
nginx
# コンテナのリソース使用量をリアルタイム確認
docker stats limited
# cgroups v2 環境(Ubuntu 22.04+ など)でのメモリ上限確認
CONTAINER_ID=$(docker inspect --format '{{.Id}}' limited)
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.max
# → 268435456 (= 256 × 1024 × 1024 バイト)
コンテナが
--memory の上限を超えてメモリを確保しようとすると、Linuxカーネルの OOM Killer がコンテナ内のプロセスを強制終了します。docker inspect の OOMKilled フィールドが true になっていれば原因はメモリ不足です。本番環境では docker stats や監視ツールで使用量を把握し、適切な上限を設定してください。
次のシミュレータでメモリ制限の動作を体験してみてください。
6. ユニオンファイルシステム — イメージのレイヤー構造
名前空間とcgroupsで「隔離」と「制限」はできました。残る課題は「コンテナ独自のファイルシステムをどう用意するか」です。VMのようにディスクを丸ごとコピーするのでは時間もスペースも無駄です。
Dockerはここで ユニオンファイルシステム(Union File System)を使います。Docker のデフォルト実装は overlay2(OverlayFS)です。
重要なポイントは3つです。
- イメージレイヤーは読み取り専用かつ複数コンテナで共有される(ディスク効率が高い)
- コンテナ内でのファイル変更はすべて書き込み可能レイヤーに記録される(コピーオンライト方式)
- コンテナを削除すると書き込み可能レイヤーも消える(永続化にはVolumeを使う — 第4章)
1-1の記事に埋め込んだ
シミュレータで、ubuntu・python・myappの3イメージが同じレイヤーAを共有している様子を確認できます。レイヤーキャッシュの詳細は第3章「3-2. レイヤーキャッシュの仕組みとビルド最適化」で扱います。
7. 3つの仕組みが合わさってコンテナになる
docker run nginx を実行したとき、内部では次のような処理が順番に行われています。
VMとの決定的な違いは、BIOSもブートローダーもカーネル起動も存在しない点です。カーネルはホストのものをそのまま使い、ステップ①〜③はミリ秒〜数百ミリ秒で完了します。これがコンテナの起動が速い理由です。
runc はコンテナを実際に作る低レベルツール(OCI仕様準拠)、containerd はruncを呼び出してコンテナのライフサイクル(起動・停止・イメージ管理)を管理する上位ランタイムです。
docker CLIはcontainerdのAPIを通じてコンテナを操作します。全体のアーキテクチャは 1-2 で詳しく解説します。
8. コマンドで確認する(Linux環境)
ここで説明した内容は実際のコマンドで確認できます。Docker が動いているLinux環境(WSL2含む)で試してみてください。
① コンテナのPIDとnamespaceファイルを確認する
# テスト用コンテナを起動
docker run -d --name demo nginx
# ホスト上でのコンテナの実際のPIDを取得
HOST_PID=$(docker inspect --format '{{.State.Pid}}' demo)
echo "Host PID: $HOST_PID"
# そのPIDの名前空間ファイル一覧(各名前空間へのシンボリックリンク)
ls -la /proc/$HOST_PID/ns/
# → cgroup → cgroup:[4026532456]
# → ipc → ipc:[4026532458]
# → mnt → mnt:[4026532459]
# → net → net:[4026532461]
# → pid → pid:[4026532460]
# → uts → uts:[4026532457]
# → user → user:[4026531837] ← デフォルトはホストと共有(同じinode)
# ホストのPID 1と比較すると pid/mnt/net/uts/ipc が異なるinode番号になっている
ls -la /proc/1/ns/
② コンテナ内からPIDの見え方を確認する
# コンテナ内のプロセス一覧(PID 1から始まる)
docker exec demo ps aux
# → PID USER COMMAND
# → 1 root nginx: master process nginx -g daemon off;
# → 29 nginx nginx: worker process
# ホスト上では全プロセスが見える(PIDはHOST_PIDと一致)
ps aux | grep nginx
# → 3842 root nginx: master process nginx ...
# → 3857 nginx nginx: worker process
③ cgroupsでリソース制限を確認する
# メモリ256MB制限付きコンテナを起動
docker run -d --name limited --memory=256m nginx
# cgroups v2 環境(Ubuntu 22.04+ など)での確認
CONTAINER_ID=$(docker inspect --format '{{.Id}}' limited)
cat /sys/fs/cgroup/system.slice/docker-${CONTAINER_ID}.scope/memory.max
# → 268435456 (= 256 × 1024 × 1024 バイト = 256 MiB)
# リアルタイムのリソース使用量モニタリング
docker stats limited
# → CONTAINER CPU % MEM USAGE / LIMIT MEM %
# → limited 0.0% 8.5MiB / 256MiB 3.3%
# OOM Killされたか確認する
docker inspect --format '{{.State.OOMKilled}}' limited
# → false(問題なし)/ true(メモリ超過で強制終了)
Docker Desktop for Windows(WSL2バックエンド)・macOSでは、コンテナがLinux VM内で動作するため
/proc/{PID}/ns/ や /sys/fs/cgroup/ はVM内のパスになります。docker exec demo cat /proc/self/cgroup を実行すると、コンテナが属しているcgroupのパスを確認できます。
9. まとめ
コンテナは「軽量VM」ではなく、Linuxカーネルの3つの機能によって隔離・制限されたプロセスです。
| 仕組み | 何を分離・制限するか | 「速い・軽い」への貢献 |
|---|---|---|
| 名前空間 | PID・ネットワーク・ファイルシステム・ホスト名(7種類) | カーネルを共有するためゲストOS起動が不要 |
| cgroups | CPU・メモリ・I/O・プロセス数 | 必要な分だけリソースを割り当てられる |
| overlayfs | ファイルシステム(レイヤー重ね合わせ) | イメージを共有しディスクを節約・起動を高速化 |
1-2 では、Docker全体のアーキテクチャ(Docker Client・Docker Daemon・containerd・runc)を俯瞰します。今回学んだ runc がどこに位置づけられ、
docker コマンドを打ってからコンテナが起動するまでの全体像を把握できるようになります。
参考リンク
- Linux man-pages: namespaces(7) — Linux名前空間の公式リファレンス(種類・フラグの詳細)
- Linux man-pages: cgroups(7) — cgroupsの公式リファレンス(v1/v2の違いも記載)
- Docker セキュリティドキュメント — namespaces・cgroups・capabilitiesのDockerでの使われ方
- opencontainers/runc(GitHub) — clone()・cgroup設定など低レベルコンテナ作成の実装が読めるOSS
- Docker: Runtime options with Memory, CPUs, and GPUs — –memory / –cpus 等のリソース制限オプションの公式ドキュメント



コメント