✍️ 필사 모드: コンテナとDocker内部完全攻略 — Namespace、cgroups、OverlayFS、seccomp、Capabilities、そしてKubernetesまで (2025)
日本語0. 「コンテナはVMではない」
多くのエンジニアはコンテナを「軽量VM」と捉えているが、実際にはまったく違う:
| 項目 | VM | Container |
|---|---|---|
| 隔離単位 | ハードウェア | OS Namespace |
| カーネル | VMごとに独立 | ホストと共有 |
| 起動時間 | 秒〜分 | 数十ms |
| メモリオーバヘッド | 数百MB | 数MB |
| ディスクオーバヘッド | GB | MB |
| 隔離強度 | 強い | 弱い |
コンテナ = プロセス + カーネル機能 (Namespace, cgroup, rootfs) の組み合わせ。別のOSがある訳ではない。
本記事は docker run nginx という一行がLinuxカーネル内で実際に何をするか、その裏にある10年以上のエンジニアリング史を掘り下げる。
1. コンテナの短くない歴史
1.1 1979 — chroot
Unix v7の chroot() システムコール。「このプロセスのファイルシステムルートを変える」→ 限定されたディレクトリしか見えない。ただしプロセス一覧、ネットワーク、ユーザはそのまま見え、chdirやmountで脱出できた。
1.2 2000 — FreeBSD Jails
chrootを拡張し、ネットワーク・プロセス・ユーザまで隔離。本当の「軽量仮想化」の始まり。
1.3 2004 — Solaris Zones
完全な隔離技術。コンテナ概念の商業的初成功例。
1.4 2006 — cgroups、2007 — LXC
Googleエンジニアが内部リソース隔離のため「process containers」を開発 → Linuxのcgroupsへ。Namespaceも段階的に追加。LXCがこれらを組み合わせた最初の実用的コンテナランタイムを提供。
1.5 2013 — Docker
Solomon HykesのdotCloudがLXCを使いやすいAPIで包装。docker run 一行の魔法。キラー機能は イメージレイヤ (OverlayFS) と レジストリ (Docker Hub)。
1.6 2015 — OCI、2016 — containerd、2017 — Kubernetes
OCI (Open Container Initiative) がイメージ/ランタイムの標準を確立。Dockerはランタイムをcontainerdへ分離。Kubernetesがオーケストレーション標準となり、Docker Engineは抽象化されていった。
1.7 2024 — 現在
Docker Desktop、Podman (daemonless)、Kubernetes + containerd + runc、ECS、Cloud Run、Fly.io。コンテナはすべてのクラウドデプロイの基本単位となった。
2. Linux Namespace — 隔離の7次元
Namespaceは「このプロセスが見えるOSリソース」を制限する。2024年時点で7種類。
2.1 PID Namespace
プロセスID隔離。コンテナ内で ps すると:
PID TTY TIME CMD
1 ? 00:00:00 nginx
10 ? 00:00:00 worker
11 ? 00:00:00 worker
コンテナ内のnginxがPID 1、ホストではPID 12345。別の番号空間だ。PID 1は特別で、カーネルはゾンビリープ (init役割) を期待し、終了すると全子プロセスも終了する。多くのアプリはinitとして設計されていないため tini、dumb-init のような小さなinitが使われる。
2.2 Mount Namespace
マウントポイントの隔離。各コンテナが独自の / ツリーを持つ。chrootの進化系。ホストの特定マウントを共有可能。
2.3 Network Namespace
各コンテナが独自のネットワークインターフェース、ルーティングテーブル、iptablesルールを持つ。
# ホストからコンテナのNetwork Namespaceへ入る
sudo nsenter -t <container_pid> -n ip addr
Dockerのbridgeモード: 仮想ブリッジ docker0 + vethペア。
2.4 UTS Namespace
Unix Timesharing System。ホスト名・ドメインを隔離。
2.5 IPC Namespace
System V IPC、POSIXメッセージキューを隔離。
2.6 User Namespace
UID/GIDマッピング。コンテナ内のroot (UID 0) がホストでは非特権ユーザ UID 100000 にマップされる。セキュリティ上決定的だが設定が複雑で、Dockerはデフォルト無効。Podman rootlessが活用。
2.7 Cgroup Namespace
cgroup階層を隔離。コンテナは自分のcgroupパスを / として見る。2016年導入。
2.8 Time Namespace
Linux 5.6 (2020) で追加。各コンテナが独自の CLOCK_BOOTTIME を持てる。チェックポイント/復元 (CRIU) で利用。
2.9 Namespace操作
unshare --pid --mount --net --fork bash # 新しいNamespaceで bash
nsenter -t PID -n -p # 既存Namespaceへ侵入
docker exec は実質、対象コンテナのNamespaceに入って新しいプロセスを生成する。
3. cgroups — リソース制限
3.1 cgroups v1 (2007-)
リソースごと (CPU、memory、networkなど) に別階層:
/sys/fs/cgroup/
├── cpu/
│ ├── docker/
│ │ └── <container_id>/
│ │ ├── cpu.cfs_quota_us
│ │ └── cpu.cfs_period_us
├── memory/
├── blkio/
└── ...
独立した階層で複雑。
3.2 cgroups v2 (2016-、systemd 232+)
単一の統合階層:
/sys/fs/cgroup/
├── user.slice/
├── system.slice/
│ └── docker-<id>.scope/
│ ├── cpu.max # "100000 100000" = 1 CPU
│ ├── memory.max # "536870912" = 512MB
│ ├── io.max
│ └── pids.max
一つのツリー、階層的リソース配分、よりシンプルなモデル。
3.3 CPU制限の実際
cpu.max = "50000 100000" は「100ms周期ごとに最大50ms CPU使用」= 0.5 CPU。JVMやNodeが短時間CPUを爆食いするとthrottleがかかり応答遅延。Dockerの --cpus=1 は cpu.max = "100000 100000"。
3.4 メモリ制限
memory.max: 上限。超えるとOOM。memory.high: ソフト制限。越えるとreclaim圧力。memory.min、memory.low: 保護メモリ。
コンテナが memory.max に達すると コンテナ内OOM Killer が発動。ホストは無事。
3.5 JVM、Node、Goのcgroup認識
JVM 10+ は -XX:+UseContainerSupport がデフォルトでcgroup上限を読む。Go 1.19+ は GOMEMLIMIT。Nodeは --max-old-space-size。これを設定しないとホストメモリ64GBと誤認してヒープを大きく取り、OOMする。
4. OverlayFS — イメージレイヤの秘密
4.1 なぜレイヤか
Dockerイメージは 読み取り専用レイヤのスタック:
Layer 4 (10KB): アプリコード
Layer 3 (50MB): npm install 結果
Layer 2 (80MB): apt packages
Layer 1 (70MB): ubuntu base
各レイヤは独立・不変。複数イメージがbaseレイヤを共有。100GBイメージでも変更されたtop layerだけpush/pull。
4.2 OverlayFSの構造
┌─────────────────────┐
│ upperdir (RW) │ ← コンテナ書き込み
├─────────────────────┤
│ merged view │ ← アプリが見るFS
├─────────────────────┤
│ lowerdir (RO) ×N │ ← イメージレイヤ
└─────────────────────┘
読み込み: upperdirに無ければlowerdir。書き込み: 常にupperdir。修正は Copy-up (元をupperdirへコピーしてから修正)。
4.3 Copy-upのコスト
100MBファイルに1バイト変更 = 100MBコピー。大きなDBファイルをコンテナ内に置かない理由。解決: Volume mount。
4.4 Whiteout — ファイル削除
rm してもlowerdirからは消えない。代わりにupperdirに特殊な「whiteout」ファイルを作成しmerged viewから隠す。実ファイルは下に残る。Dockerfileで後段の RUN rm をしてもレイヤサイズは減らない。同じ RUN 内で生成+削除する必要がある。
4.5 Storage driverの進化
- aufs (2013-): 初期、upstreamなし。
- devicemapper (2014-): thin provisioning、遅い。
- btrfs、zfs: ファイルシステム依存。
- overlay (2014): upstream、速い。
- overlay2 (2016-): 現在のデフォルト。複数lowerdir対応。
Podmanはrootless対応のfuse-overlayfsも選択可能。
5. コンテナランタイム — runcと仲間たち
5.1 Dockerの内部階層
docker CLI
↓
dockerd (daemon)
↓
containerd (コンテナライフサイクル)
↓
containerd-shim (コンテナごとに1つ)
↓
runc (OCI runtime、実際のコンテナ生成)
↓
Linux kernel (Namespace、cgroup、...)
5.2 runcの役割
OCI Runtime Specificationを実装。入力: rootfs + config.json。出力: 動いているコンテナ。Go/Cで書かれ、clone() + Namespaceフラグ、setns()、cgroup設定、rootfs mount、execve() を直接呼ぶ。
5.3 代替ランタイム
- crun: C実装、runcより速い (Goランタイムなし)。
- youki: Rust実装。
- Kata Containers: 各コンテナを軽量VM内で実行 → VM級の隔離。
- gVisor: Google。ユーザ空間カーネルでシステムコールを捕獲。
- Firecracker: AWS LambdaのmicroVM。
マルチテナント環境 (Lambda、Fargate) はVMベースランタイムを採用。
5.4 shimの役割
containerd-shim はコンテナごとに存在。containerd daemonが死んでもコンテナは生き残る。stdout/stderr収集、exit code記録。kubelet → CRI → containerd → shim → runc。
6. セキュリティ — コンテナが「本物の隔離」ではない理由
6.1 共有カーネルのリスク
コンテナは同じカーネルを共有 → カーネル脆弱性 = 全コンテナ脱出。実例:
- Dirty COW (CVE-2016-5195): 権限昇格、コンテナ脱出。
- runc CVE-2019-5736: runcバイナリ上書きでホストroot奪取。
- Leaky Vessels (2024): runc、containerdの複数脆弱性。
対策: カーネル更新、AppArmor/SELinux、seccompプロファイル。
6.2 Linux Capabilities
従来のUnix: root (UID 0) = 全権限。コンテナには危険すぎる。Capabilitiesはrootを細分化:
CAP_NET_ADMIN: ネットワーク設定。CAP_SYS_ADMIN: 大部分の危険な操作。CAP_NET_BIND_SERVICE: 1024以下のポートバインド。
Dockerデフォルト: 制限されたcap set (最小権限)。--cap-add、--cap-drop で調整。--privileged は全cap = ホストrootとほぼ同等。
6.3 seccomp — システムコールフィルタ
「このコンテナは特定syscallのみ許可」:
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{ "names": ["read", "write", "open"], "action": "SCMP_ACT_ALLOW" }
]
}
Dockerのデフォルトseccompプロファイルは約300のsyscallを許可し、keyctl、ptrace、mount などを遮断。
6.4 AppArmor / SELinux
ファイルシステムレベルのMAC (Mandatory Access Control)。UbuntuはAppArmor、RHELはSELinux。
6.5 Rootless Container
コンテナdaemonも非特権ユーザで実行。User NamespaceでUIDマッピング。Podmanのデフォルト。Dockerもrootlessモード対応。セキュリティ大幅強化、一部機能制限 (1024以下のポートなど)。
6.6 セキュリティチェックリスト
- 最新カーネル。
- デフォルトseccompプロファイル維持 (
--security-opt seccomp=unconfined禁止)。 - Capability最小化 (
--cap-drop ALL+ 必要なもののみadd)。 - Non-rootユーザでアプリ実行 (
USERディレクティブ)。 - Read-only rootfs (
--read-only+ tmpfs volumes)。 - Image scanning: Trivy、Grype、Docker Scout。
- Runtime monitoring: Falco。
7. イメージ作成の技術
7.1 レイヤ最適化
# 悪い: 各RUNが新レイヤ
FROM node:20
COPY package.json .
RUN npm install
COPY . .
RUN npm run build
RUN rm -rf /tmp/cache
# 良い: 同じRUN内でクリーンアップ
FROM node:20
COPY package.json .
RUN npm install && rm -rf /tmp/* /var/cache/*
COPY . .
RUN npm run build
7.2 Multi-stage Build
FROM node:20 AS builder
WORKDIR /app
COPY . .
RUN npm install && npm run build
FROM node:20-alpine
COPY /app/dist /app
COPY /app/node_modules /app/node_modules
CMD ["node", "/app/index.js"]
ビルドツール (gcc、webpackなど) は最終イメージに入らない → サイズ5倍以上削減。
7.3 Distroless Image
Googleが作る最小イメージ (shellすら無し):
FROM gcr.io/distroless/nodejs20
COPY /app /app
CMD ["/app/index.js"]
サイズ数十MB、攻撃表面極小。短所はデバッグが難しい (execで入れない)。
7.4 Alpine vs Debian
- Alpine (
node:20-alpine): 5MB base、musl libc。 - Debian slim (
node:20-slim): 80MB、glibc。
Alpineの注意: musl互換性問題 (DNS resolver、pthread挙動の微差)。PythonのC extensionsビルドには追加ツール必要。
7.5 BuildKitとキャッシュ
DOCKER_BUILDKIT=1 docker build .
並列stageビルド、キャッシュのexport/import (--cache-to=type=registry,ref=...)、ビルドタイムsecrets (--secret)、containerdネイティブ。
8. ネットワーキング — docker0からCNIまで
8.1 Docker デフォルト bridge
docker0 (bridge, 172.17.0.1)
├── veth0 → container1 eth0 (172.17.0.2)
├── veth1 → container2 eth0 (172.17.0.3)
└── veth2 → container3 eth0 (172.17.0.4)
vethペア (片方ホスト、片方コンテナ)、iptables MASQUERADE でNAT、-p 80:8080 はiptables DNAT。
8.2 ネットワークモード
- bridge (デフォルト)。
- host: ホストネットワーク共有 (隔離無し、最速)。
- none: ネットワーク無し。
- container: 他コンテナと共有 (podに似る)。
8.3 Kubernetes CNI
Kubernetesは「全podが平らなIPを持つ」哲学。Docker bridgeでは不十分。
- Flannel: VXLANトンネリング、シンプル。
- Calico: BGPベース、eBPF対応、ネットワークポリシー。
- Cilium: eBPFネイティブ、観測性/セキュリティ豊富。
- AWS VPC CNI: ENIをpodに直接割当。
CNI = Container Network Interface。プラグイン交換可能な標準。
9. Kubernetesとの統合
9.1 なぜKubernetesはDockerを捨てたか (2020)
Kubernetes 1.20でdockershim非推奨を告知、1.24 (2022) で削除。理由: Docker Engineはkubeletが使わない機能 (イメージビルド、swarmなど) を多数含む。CRIに合わせるためのshim層が余計。containerdが既にDockerの中核。結果: kubelet → CRI → containerd直結。OCIイメージはそのまま動作。
9.2 Pod = Namespaceを共有するコンテナ群
PodはNetwork、IPC、UTS NamespaceをコンテナたちでShareする。Mount、PIDは(デフォルト)独立。
pause container (Namespace所有者)
├── app container
└── sidecar container
pause は何もしないミニプロセス。Namespaceだけ維持。
9.3 Init container と sidecar
- Init container: Pod起動前に順次実行。
- Sidecar (native 2023+): アプリと並行実行、独立リスタート可能。
Envoy (Istio)、Fluent Bitなどの補助プロセスがsidecarで。
9.4 オーケストレーションの意味
スケジューリング、ヘルスチェックと自動再起動、ローリング更新/canary、サービス検出/ロードバランシング、ストレージ/Config/Secret、水平オートスケーリング — すべて宣言的YAMLで。
10. 実践Tips
10.1 デバッグツールキット
docker ps -a
docker logs -f <container>
docker exec -it <container> sh
docker inspect <container>
docker stats
docker events
Kubernetes:
kubectl logs -f <pod>
kubectl exec -it <pod> -- sh
kubectl describe pod <pod>
kubectl get events --sort-by='.lastTimestamp'
10.2 Distrolessのデバッグ
shellが無いため exec で入れない。代替:
kubectl debug <pod> --image=busybox --target=app
# または
docker run --rm -it --pid=container:<id> --net=container:<id> busybox
pid/net Namespaceを共有してデバッグコンテナでアクセス。
10.3 イメージサイズ分析
docker history <image>: レイヤ別サイズ。divetool: レイヤ内容探索。docker image ls --format="..."。
10.4 性能観測
docker stats --no-stream
cat /sys/fs/cgroup/memory.current
cat /sys/fs/cgroup/cpu.stat
docker run --cap-add SYS_ADMIN --pid host ...
11. おわりに — docker run 一行の深さ
docker run nginx 一行が実際にやること:
- Docker Hubからnginxイメージレイヤをダウンロード。
- OverlayFSでレイヤスタック組立。
- containerdがcontainerd-shimを生成。
- shimがruncを起動。
- runcが7つのNamespace + cgroup + rootfsを設定。
- seccomp/AppArmorプロファイル適用。
execve("nginx")でプロセス起動。- コンテナネットワーク (veth + bridge) 構成。
- iptablesポートフォワーディング追加。
50ms以内に完了する。2000年代にVMを起動するのに数分かかっていたのと比べれば奇跡だ。背後には2006 cgroups、2008 Namespace、2013 Docker、2015 OCI、2016 overlay2、2020 CRIなど10年超のエンジニアリングがある。
同時にこの利便性の代価は カーネル共有 = セキュリティ弱点。Lambda、Fargateのような商用プラットフォームがmicroVMを使う理由。マルチテナントなら「コンテナ = 隔離」を全面的に信用するな。
次回は Kubernetes内部 — etcdのRaft合意、スケジューラのフィルタ/スコアリング、コントローラパターン、カスタムリソース、CRI/CNI/CSIのプラグインシステム — を掘り下げる。
参考資料
- Jérôme Petazzoni — "Anatomy of a Container" (LinuxCon 2015)。
- Michael Kerrisk — "Understanding Linux Namespaces" (LWN シリーズ)。
- OCI Image/Runtime Specifications (GitHub)。
- Julia Evans — Container Networking シリーズ。
- Liz Rice — "Container Security" (O'Reilly, 2020)。
- Kubernetes公式ドキュメント — Pods、CNI、CRI。
- Red Hat crunブログ。
- Firecracker論文 (NSDI 2020)。
- Google gVisor論文。
- "The Kubernetes Book" — Nigel Poulton。
현재 단락 (1/232)
多くのエンジニアはコンテナを「軽量VM」と捉えているが、実際にはまったく違う: