目次
Dockerの内部原理チュートリアル:namespace、cgroup、overlayfsを実際に確認
Dockerとはでは、たとえを使ってコンテナを説明しました。このセクションではDockerホスト上で、Linuxにおけるコンテナの正体を直接確認します。答えは一言です。
コンテナは通常のLinuxプロセスです。ただし、namespaceによって「何が見えるか」が制限され、cgroupによって「どれだけ使えるか」が制限され、overlayfsで構成されたディレクトリがルートディレクトリとして使われます。さらにcapabilitiesとseccompによって「何ができるか」が制限されます。
ここでいう「ホスト」は、このチュートリアルのDockerテスト環境(Linux 5.4カーネル、cgroup v1)を指します。お使いのコンピューターで実行すると、IDやパスなどの値は異なります。cgroup v2を使うシステムではファイル名も異なります。その場合は本文中に記載します。Docker Desktopをお使いの場合は、
docker run -it --rm --privileged --pid=host alpine:3.23 nsenter -t 1 -m -u -i -n shを実行してDocker DesktopのLinux仮想マシンに入り、確認できます。
コンテナはホスト上の1つのプロセスです
nginxコンテナを起動し、メモリを256 MB、CPUを0.5コアに制限します。
$ docker run -d --name demo --memory 256m --cpus 0.5 nginx:1.30-alpine
WARNING: Your kernel does not support swap limit capabilities or the cgroup is not mounted. Memory limited without swap.
57dcd17505d4e7c71e57df55edef3e137c18249e6ad402c7e7b3f215341c47b4
$ docker exec demo ps
PID USER TIME COMMAND
1 root 0:00 nginx: master process nginx -g daemon off;
29 nginx 0:00 nginx: worker process
30 root 0:00 ps
$ docker inspect -f "{{.State.Pid}}" demo
12458
- コンテナ内では、nginxのメインプロセスのPIDは1で、自分のプロセスだけが見えます。
docker inspectの.State.Pidは、ホスト上でのPIDです:12458。
ホスト上のプロセスを一覧表示します。
$ ps -o pid,ppid,args | grep -v -E 'ps -o|grep|cut' | cut -c1-110
PID PPID COMMAND
1 0 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --registry-mir
19 1 dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --registry-mirror=https://mir
12434 1 /usr/local/bin/containerd-shim-runc-v2 -namespace moby -id 57dcd17505d4e7c71e57df55edef3e137c18249
12458 12434 nginx: master process nginx -g daemon off;
12495 12458 nginx: worker process
ホスト上では、完全なプロセスツリーが見えます:dockerd → containerd-shim-runc-v2 → nginx: master process(12458)→ nginx: worker process。
- containerd-shim-runc-v2:コンテナごとに1つあり、コンテナのプロセスを管理します。dockerdを再起動してもコンテナが停止しないのは、これがあるためです。
- runc:コンテナを実際に作成するプログラムです。namespace、cgroup、ルートディレクトリを設定した後、nginxを実行して終了するため、プロセスツリーには表示されません。
同じプロセスでも、コンテナ内ではPID 1、ホスト上ではPID 12458です。これがPID namespaceの効果です。
namespace:コンテナから見えるものを決める
lsns は、プロセスが属する namespace を一覧表示します。
$ lsns -p 12458 -o NS,TYPE,PID,COMMAND
NS TYPE PID COMMAND
4026531835 cgroup 1 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --registry-mirror=https://mirror.gcr.io --dns=1.1.1.1 --dns=8.8.8.8
4026531837 user 1 docker-init -- dockerd --host=unix:///var/run/docker.sock --host=tcp://0.0.0.0:2375 --registry-mirror=https://mirror.gcr.io --dns=1.1.1.1 --dns=8.8.8.8
4026533161 mnt 12458 nginx: master process nginx -g daemon off;
4026533162 uts 12458 nginx: master process nginx -g daemon off;
4026533163 ipc 12458 nginx: master process nginx -g daemon off;
4026533164 pid 12458 nginx: master process nginx -g daemon off;
4026533166 net 12458 nginx: master process nginx -g daemon off;
nginx プロセスには独自の namespace が 5 つあります(ID はホストと異なります)。残りの 2 つ(cgroup、user)は、ホスト上の PID 1 と共有しています。
| namespace | 隔離されるもの | コンテナ内での効果 |
|---|---|---|
pid | プロセス ID | コンテナ内では自分のプロセスだけが見え、メインプロセスは PID 1 です |
net | ネットワークインターフェース、IP、ポート、ルート | コンテナは独自の eth0 と IP を持ち、それぞれポート 80 をリッスンできます |
mnt | マウントポイント | コンテナは独自のルートディレクトリ / を持ちます |
uts | ホスト名 | コンテナの hostname はコンテナ ID です |
ipc | 共有メモリ、セマフォ | コンテナ同士が IPC で互いに干渉することはありません |
user | ユーザー ID のマッピング | デフォルトでは隔離されません。コンテナ内の root はホストの root と同じです。Rootless モード でのみ使用されます |
cgroup | cgroup ディレクトリの表示範囲 | この環境(cgroup v1)では隔離されません。cgroup v2 のシステムではデフォルトで隔離されます |
uts と net namespace の効果を確認します。
$ docker exec demo hostname
57dcd17505d4
$ docker exec demo ip -4 addr show eth0
2: eth0@if130: <BROADCAST,MULTICAST,UP,LOWER_UP,M-DOWN> mtu 1500 qdisc noqueue state UP
inet 172.18.0.2/16 brd 172.18.255.255 scope global eth0
valid_lft forever preferred_lft forever
コンテナのホスト名はコンテナ ID の先頭 12 文字です。eth0 は独自の IP(172.18.0.2)を持ち、これは Docker ネットワークによって割り当てられます。詳しくはネットワークとコンテナ間通信をご覧ください。
cgroup:コンテナが使えるリソース量を決める
--memory 256m --cpus 0.5 は Docker 自体が計算するのではなく、Linux カーネルの cgroup 設定に書き込まれます。コンテナ内から設定を読み取ります。
$ docker info --format "Cgroup Version: {{.CgroupVersion}}"
Cgroup Version: 1
$ docker exec demo cat /sys/fs/cgroup/memory/memory.limit_in_bytes
268435456
$ docker exec demo cat /sys/fs/cgroup/memory/memory.usage_in_bytes
10842112
$ docker exec demo cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us /sys/fs/cgroup/cpu/cpu.cfs_period_us
50000
100000
$ docker stats --no-stream --format "{{.Name}} {{.MemUsage}} {{.CPUPerc}}" demo
demo 5.613MiB / 256MiB 0.00%
| 読み取った値 | 意味 |
|---|---|
memory.limit_in_bytes = 268435456 | 268435456 bytes = 256 MiB、つまり --memory 256m です |
memory.usage_in_bytes = 10842112 | 現在約 10 MB 使用しています(カーネルの統計値で、ファイルキャッシュを含むため、docker stats の表示より大きくなります) |
cpu.cfs_quota_us = 50000、cpu.cfs_period_us = 100000 | 100 ミリ秒ごとに最大 50 ミリ秒の CPU 時間を使用できます。つまり 0.5 コアです |
docker stats に表示される 256MiB の上限も cgroup から読み取られます。プロセスが上限を超えるメモリを使うと、カーネルによって終了されます(OOM)。終了コードは 137 です。
cgroup v2 のシステム(近年の Ubuntu、Debian、Fedora、および Docker Desktop)では、ファイル名が異なります。メモリ上限は
/sys/fs/cgroup/memory.max、CPU クォータは/sys/fs/cgroup/cpu.maxにあります(内容は50000 100000です)。
overlayfs:コンテナのファイルシステム
イメージのレイヤー
イメージは複数の読み取り専用のレイヤーで構成されています。
$ docker image inspect -f "{{range .RootFS.Layers}}{{println .}}{{end}}" nginx:1.30-alpine
sha256:74d97c428c51a828f9051a7a40a53ff1fc99e54fc30323ce36760701b0b7f711
sha256:2d3fbe0ca19dd389108f7e9a1d89ec5c1f1d7dc3c11fb2123bc425fed39c5f22
sha256:ed752e851ebe79a0e72be1fd48e90474d36ae2bb2ddccff2475f8e050c20a804
sha256:ec2b9a359ab960e2254545e79e9bca77edaeefb652d97b26855217fb8a9c80b3
sha256:267e83adf4c4e342b08cbe40814662493e030c88311c720fbce2f0d7c809f3f4
sha256:8fb5da5ad6fa936ba8c24999da33095b2b6c5fd2bcf4aa22acbf299b0e7294df
sha256:0d250d8db7c20b3c0bd3059b3431dd47f130cb81e7a4c7820950139daafb91d6
sha256:e46f4042ccf5f36d1605617ef6912bdaac437fa0bca12530e77529b191eb02fa
$ docker history --format "{{.Size}}\t{{.CreatedBy}}" nginx:1.30-alpine | cut -c1-90
51.9MB RUN /bin/sh -c set -x && apkArch="$(cat …
0B ENV ACME_VERSION=0.4.1
0B ENV NJS_RELEASE=1
0B ENV NJS_VERSION=1.0.1
0B CMD ["nginx" "-g" "daemon off;"]
0B STOPSIGNAL SIGQUIT
0B EXPOSE map[80/tcp:{}]
0B ENTRYPOINT ["/docker-entrypoint.sh"]
16.4kB COPY 30-tune-worker-processes.sh /docker-ent…
12.3kB COPY 20-envsubst-on-templates.sh /docker-ent…
12.3kB COPY 15-local-resolvers.envsh /docker-entryp…
12.3kB COPY 10-listen-on-ipv6-by-default.sh /docker…
8.19kB COPY docker-entrypoint.sh / # buildkit
5.62MB RUN /bin/sh -c set -x && addgroup -g 101…
0B ENV DYNPKG_RELEASE=1
0B ENV PKG_RELEASE=1
0B ENV NGINX_VERSION=1.30.5
0B LABEL maintainer=NGINX Docker Maintainers <d…
0B CMD ["/bin/sh"]
9.08MB ADD alpine-minirootfs-3.24.2-x86_64.tar.gz /…
RootFS.Layersは 8 つのレイヤーのハッシュ値を一覧表示します。docker historyは、各レイヤーが Dockerfile のどの行によって作成されたかを一覧表示します。サイズが0Bのもの(ENV、CMD、EXPOSE)は設定を変更するだけで、ファイルを生成しないため、対応するレイヤーはありません。一番下のADD alpine-minirootfs...が Alpine ベースイメージです。
重なってルートディレクトリになる
コンテナの起動時、Docker は overlayfs を使ってこれらのレイヤーを重ね、さらに書き込み可能なレイヤーを追加します。ホスト上でこのコンテナのマウント情報を確認します。
$ grep overlay /proc/mounts | grep $(docker inspect -f '{{.Id}}' demo) | tr ',' '\n' | sed 's#/var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.overlayfs/snapshots#…/snapshots#g'
overlay /var/lib/docker/rootfs/overlayfs/57dcd17505d4e7c71e57df55edef3e137c18249e6ad402c7e7b3f215341c47b4 overlay rw
relatime
lowerdir=…/snapshots/260/fs:…/snapshots/21/fs:…/snapshots/20/fs:…/snapshots/19/fs:…/snapshots/18/fs:…/snapshots/17/fs:…/snapshots/16/fs:…/snapshots/15/fs:…/snapshots/1/fs
upperdir=…/snapshots/261/fs
workdir=…/snapshots/261/work
xino=off 0 0
(パスが長いため、… で /var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.overlayfs を置き換えています。)
| 項目 | 意味 |
|---|---|
lowerdir | 読み取り専用のイメージレイヤーが上から順に重なっています。このイメージを使うすべてのコンテナで共有されます |
upperdir | このコンテナ専用の書き込み可能なレイヤー |
workdir | overlayfs が内部で使う一時ディレクトリ |
マウントポイント /var/lib/docker/rootfs/overlayfs/コンテナID | マージ後の結果で、コンテナから見える / です |
コンテナが書き込んだファイルの保存先
コンテナ内にファイルを作成し、ホスト上の upperdir で探します。
$ docker exec demo sh -c "echo hello > /usr/share/nginx/html/new.html"
$ find /var/lib/docker/containerd/daemon/io.containerd.snapshotter.v1.overlayfs/snapshots/261/fs -type f | sed 's#.*/fs/#upperdir/#'
upperdir/usr/share/nginx/html/new.html
upperdir/run/nginx.pid
upperdir/etc/nginx/conf.d/default.conf
$ docker diff demo
C /etc
C /etc/nginx
C /etc/nginx/conf.d
C /etc/nginx/conf.d/default.conf
C /usr
C /usr/share
C /usr/share/nginx
C /usr/share/nginx/html
A /usr/share/nginx/html/new.html
C /run
A /run/nginx.pid
C /var
C /var/cache
C /var/cache/nginx
A /var/cache/nginx/client_temp
A /var/cache/nginx/fastcgi_temp
A /var/cache/nginx/proxy_temp
A /var/cache/nginx/scgi_temp
A /var/cache/nginx/uwsgi_temp
- 新しく作成した
new.htmlは upperdir にあります。 nginx.pidは nginx の起動時に書き込まれます。default.confはイメージの起動スクリプトによって変更されています(IPv6 の設定が追加されています)。イメージ内にすでにあるファイルを変更する場合、overlayfs はまずファイルを upperdir にコピーしてから変更します(copy-up)。docker diffは upperdir の内容を一覧表示します。Aは追加、Cは変更(上位ディレクトリを含む)、Dは削除を表します。
つまり、次のようになります。
- コンテナを削除すると、その upperdir も削除され、コンテナ内に書き込んだデータは失われます。そのため、データは Volume に保存します。Volume は overlayfs を経由せず、ディレクトリを直接マウントします。
- 1 つのイメージから 100 個のコンテナを起動しても、イメージのレイヤーは 1 つだけ保存されます。
- Dockerfile で「COPY してから RUN rm」を実行しても、イメージは小さくなりません。ファイルは下位の読み取り専用レイヤーに残るためです。セキュリティのベストプラクティスでも、この点が取り上げられています。
capabilities と seccomp:コンテナができることを決める
$ docker info --format "{{.Driver}} {{json .SecurityOptions}}"
overlayfs ["name=apparmor,profile=default","name=seccomp,profile=builtin"]
$ docker exec demo grep CapEff /proc/1/status
CapEff: 00000000a80425fb
$ docker run --rm --cap-drop ALL alpine:3.23 grep CapEff /proc/1/status
CapEff: 0000000000000000
docker infoは、この環境で AppArmor と seccomp のデフォルト設定が有効になっていることを示します。seccomp は、コンテナが一部の危険なシステムコール(カーネルモジュールの読み込みなど)を呼び出すのをブロックします。CapEffは、プロセスが現在持っている capabilities(ビット単位)です。コンテナ内の root がデフォルトで持つ権限は一部だけ(a80425fb)で、完全な root 権限ではありません。--cap-drop ALLを指定すると、権限はすべてなくなります。実際の効果についてはセキュリティのベストプラクティスを参照してください。--privilegedを指定すると、コンテナにすべての capabilities が付与され、seccomp と AppArmor が無効になります。この保護レイヤーが取り除かれます。
仮想マシンとの比較
| コンテナ | 仮想マシン | |
|---|---|---|
| カーネル | ホストの Linux カーネルを共有 | 仮想マシンごとに独自のカーネルを持つ |
| 分離方法 | namespace、cgroup、seccomp などのカーネル機能 | ハードウェア仮想化 |
| 起動 | プロセスを起動し、通常 1 秒未満 | 起動に数秒から数十秒かかる |
| オーバーヘッド | ほとんどない | 仮想マシンごとにメモリと CPU を割り当てる必要がある |
| 分離の強度 | 弱め。カーネルの脆弱性がすべてのコンテナに影響する可能性がある | 強め |
この違いから、次のようなよくある現象も説明できます。
- Linux ホストでは
psでコンテナのプロセスを確認できます。Windows と macOS では確認できません。コンテナは Docker Desktop の仮想マシン内で動作するためです。 - コンテナでは異なるカーネルの OS を実行できません。たとえば、Linux ホスト上で Windows コンテナを直接実行することはできません。
- マルチプラットフォームイメージで発生する
exec format error:コンテナは独自の CPU を持たないため、プログラムはホストの CPU で実行できる形式でなければなりません。
まとめ
- コンテナはホスト上の Linux プロセスです。runc が作成し、containerd-shim が管理します。
- namespace はプロセス、ネットワーク、マウント、ホスト名などの「見えるもの」を分離します。デフォルトではユーザーは分離されません。
- cgroup は
--memory、--cpus、--pids-limitを実装します。設定値は cgroup ファイルから直接読み取れます。 - overlayfs は読み取り専用のイメージレイヤーと書き込み可能な upperdir を統合し、コンテナのルートディレクトリを作ります。コンテナを削除すると upperdir も削除されます。
- capabilities、seccomp、AppArmor はコンテナ内の root ができることを制限します。
--privilegedを指定すると、これらの制限がすべて取り除かれます。
練習問題
練習 1:docker run -d --name t1 --pids-limit 50 alpine:3.23 sleep 600 を実行し、コンテナ内から PID の上限を読み取ってください。
解答
cgroup v1:
docker exec t1 cat /sys/fs/cgroup/pids/pids.max
cgroup v2:
docker exec t1 cat /sys/fs/cgroup/pids.max
どちらも 50 と表示されるはずです。docker info --format "{{.CgroupVersion}}" を使って、システムがどちらのバージョンか確認してください。
練習 2:コンテナ内でイメージにもともと含まれているファイル(たとえば docker exec t1 rm /etc/motd)を削除し、docker diff t1 を実行すると何が表示されますか? イメージ内のファイルは本当に削除されたのでしょうか?
解答
docker diff は D /etc/motd と表示します。イメージのレイヤーは読み取り専用なので、ファイルは実際には削除されていません。overlayfs は upperdir に「削除マーカー」(whiteout)を置き、マージ後のビューから見えないようにします。同じイメージから新しいコンテナを起動すると、/etc/motd は引き続き存在します。
練習 3:nginx:1.30-alpine コンテナを 2 つ同時に起動し、それぞれの lowerdir と upperdir を比較してください。
ヒント
Linux ホストで grep overlay /proc/mounts を実行します。2 つのコンテナの lowerdir はまったく同じ(イメージのレイヤーを共有)で、upperdir はそれぞれ異なります。
よくある質問
コンテナと仮想マシンの根本的な違いは何ですか?
仮想マシンはコンピューター全体をエミュレートし、その中で独自のOSカーネルが動作します。コンテナには独自のカーネルがなく、すべてのコンテナがホストのLinuxカーネルを直接使い、namespaceとcgroupによって隔離されます。そのため、コンテナの起動時に必要なのはプロセスの起動だけで、ほとんど時間がかかりません。
WindowsやmacOSでもLinuxコンテナを実行できるのはなぜですか?
Docker Desktopがバックグラウンドで小さなLinux仮想マシン(WindowsではWSL 2)を起動し、コンテナはその仮想マシンのLinuxカーネル上で実行されるためです。
cgroup v1とv2にはどのような違いがありますか?
どちらもLinuxカーネルがリソースを制限する仕組みです。v2は新しいバージョンで、ディレクトリ構造が統一されています。ファイル名は異なります。たとえばメモリ上限はv1ではmemory.limit_in_bytes、v2ではmemory.maxです。CPUクォータはv2ではcpu.maxです。近年のディストリビューションとDocker Desktopでは、既定でv2が使われています。docker infoのCgroup Versionに表示されます。
1つのイメージからコンテナを100個起動すると、容量も100倍必要ですか?
いいえ。イメージのレイヤーは読み取り専用のlowerdirで、すべてのコンテナで共有されます。各コンテナに追加されるのは、それぞれが書き込んだファイルや変更したファイルを保存する独自のupperdirだけです。
更新日 2026年10月14日