目次

Dockerの内部原理チュートリアル:namespace、cgroup、overlayfsを実際に確認

Dockerとはでは、たとえを使ってコンテナを説明しました。このセクションではDockerホスト上で、Linuxにおけるコンテナの正体を直接確認します。答えは一言です。

コンテナは通常のLinuxプロセスです。ただし、namespaceによって「何が見えるか」が制限され、cgroupによって「どれだけ使えるか」が制限され、overlayfsで構成されたディレクトリがルートディレクトリとして使われます。さらにcapabilitiesとseccompによって「何ができるか」が制限されます。

namespace、cgroup、overlayfs、安全機構に囲まれたLinuxプロセスとしてのコンテナ
コンテナ = 1つのプロセス + namespace + cgroup + overlayfs + セキュリティ制限

ここでいう「ホスト」は、このチュートリアルの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 モード でのみ使用されます
cgroupcgroup ディレクトリの表示範囲この環境(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 = 268435456268435456 bytes = 256 MiB、つまり --memory 256m です
memory.usage_in_bytes = 10842112現在約 10 MB 使用しています(カーネルの統計値で、ファイルキャッシュを含むため、docker stats の表示より大きくなります)
cpu.cfs_quota_us = 50000、cpu.cfs_period_us = 100000100 ミリ秒ごとに最大 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このコンテナ専用の書き込み可能なレイヤー
workdiroverlayfs が内部で使う一時ディレクトリ
マウントポイント /var/lib/docker/rootfs/overlayfs/コンテナIDマージ後の結果で、コンテナから見える / です
overlayfs:読み取り専用のイメージレイヤーに書き込み可能な upperdir を加え、コンテナのファイルシステムに統合
overlayfs:複数の読み取り専用レイヤー + 1 つの書き込み可能なレイヤー = コンテナのルートディレクトリ

コンテナが書き込んだファイルの保存先

コンテナ内にファイルを作成し、ホスト上の 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日