- 技術 💻
コンテナとは?仮想マシンとの違い|namespace・cgroupで手作りして理解
結論
コンテナとは、アプリと動かすのに要るファイルを1つにまとめ、ホストのカーネルを共有したまま「見える範囲(namespace)」と「使える量(cgroup)」を分けて動かすプロセスです。仮想マシンのように OS を丸ごと持たないため、軽く、速く起動します。
この記事でわかること
- 仮想マシン・サーバーレスとの違いと、使い分けの判断表
- unshare・chroot・cgroup で手作りしたコンテナの ps・hostname・ip a・メモリ上限の実物
- OCI イメージの manifest・レイヤー・whiteout を展開した中身と、runc での実行
仮想マシンは「機械(ハードウェア)を丸ごと真似る」技術で、コンテナは「1つの OS の中でプロセスを仕切る」技術です。図にすると、ゲスト OS の層があるかどうかが違います。
flowchart TD
subgraph V[仮想マシン]
VA[アプリ] --> VG[ゲストOS<br>とカーネル]
VG --> VH[ハイパー<br>バイザー]
end
subgraph C[コンテナ]
CA[アプリと<br>ファイル] --> CK[ホストの<br>カーネル]
end
VH --> HW[物理マシン]
CK --> HW
よく調べられることの答えを先にまとめます。
| 知りたいこと | 答え | 詳しい節 |
|---|---|---|
| コンテナとは | カーネルを共有し、見える範囲と使える量を分けたプロセス | コンテナとは |
| コンテナと仮想マシンの違い | ゲスト OS を持つか、ホストのカーネルを共有するか | コンテナと仮想マシンの違い |
| コンテナ技術の中身 | Linux の namespace と cgroup と、ファイルの切り替え(chroot・pivot_root) | コンテナの仕組み |
| コンテナイメージとは | レイヤー(tar)と設定と manifest を、ハッシュ値でつないだもの | コンテナイメージとは |
| Docker とコンテナの関係 | Docker はコンテナを作って動かす道具の1つ。中で runc が動く | Docker・Podman・Kubernetesの関係 |
| Kubernetes とは | 多数のコンテナを複数のサーバーに配置し、見張る仕組み | Docker・Podman・Kubernetesの関係 |
| どれを選ぶか | 別の OS や強い分離なら仮想マシン、アプリの配布ならコンテナ | 使い分け(判断表) |
| コンテナは安全か | カーネルを共有するため、分離は仮想マシンより弱い | メリット・デメリット |
確認環境:Ubuntu 24.04.5 LTS(カーネル 6.18、root 権限)の上で、Docker のデーモン(dockerd)は起動せずに試しました。dockerd は起動するとホストの iptables と sysctl を書き換えるためです(Docker の公式ドキュメント)。代わりに util-linux 2.39.3 の unshare・nsenter と chroot、BusyBox 1.36.1(静的リンク)で作った 2.1MB のルートファイルシステムで「手作りのコンテナ」を作り、PID・mount・UTS・network・IPC の namespace を分けて ps・hostname・ip a を比べました。cgroup は、この環境が v1 と v2 の併用(hybrid)だったため、v2 では作業用のサブグループで freeze を、メモリとプロセス数の上限は v1 のサブグループで確かめ、v2 のファイル名と意味はカーネルの公式ドキュメントで確かめています。イメージは、レジストリへの接続がこの環境から許可されていなかったため、umoci 0.4.7 で OCI イメージを手元で作り、skopeo 1.13.3 で読み、runc 1.5.2 で実行しました。overlayfs は自分の mount namespace の中だけでマウントしています。作った namespace・cgroup・マウントは確認後にすべて片付けました。namespace と cgroup の仕様は man7.org とカーネルのドキュメント、イメージの形式は OCI の仕様、Docker・Podman・Kubernetes の説明は各公式ドキュメントで確かめました。図は mermaid で描画を確かめています(確認日 2026年10月29日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「コンテナとは?仮想マシンとの違い|namespace・cgroupで手作りして理解」
コンテナとは(一言でいうと「仕切られたプロセス」)
この節では、コンテナという言葉が IT で何を指すのかが、一言と身近なたとえで分かります。
IT でいうコンテナは、アプリを動かすのに要るファイル(プログラム・ライブラリ・設定)を1つにまとめ、ほかのプロセスから仕切って動かす仕組みです。 Docker の公式ドキュメントも、コンテナを「アプリの部品ごとに分離されたプロセス」と説明しています。名前は、荷物を同じ形の箱に詰めて船・鉄道・トラックへそのまま積み替える輸送用コンテナから来ています。
大事なのは、コンテナの中身がふつうの Linux のプロセスだという点です。後の節で手作りしたコンテナでも、中で動く sleep はホストから PID 1246 の普通のプロセスとして見えました。違うのは、そのプロセスから見える「世界」が狭められていることだけです。
まとめる
アプリと、動かすのに要るファイルを「イメージ」という1つの形にする。
どこでも同じ中身
見える範囲を分ける
プロセス・ホスト名・ネットワーク・ファイルを、ほかから見えないようにする(namespace)。
隣が見えない
使える量を決める
メモリ・CPU・プロセス数の上限を決める(cgroup)。
使いすぎない
カーネルは共有
OS の心臓部(カーネル)はホストのものを使う。だから軽く、速く起動する。
仮想マシンとの差
コンテナと仮想マシンの違い(比較表)
この節では、コンテナと仮想マシン(VM)の違いが、項目ごとの表と手元の実測で分かります。
仮想マシンはハードウェアを仮想化し、その上でゲスト OS(自分のカーネル)を丸ごと動かします。コンテナは OS の中でプロセスを仕切るだけで、カーネルはホストと共有します。 Docker の公式ドキュメントは、VM を「自分のカーネル・ドライバー・プログラムを持つ OS 全体」、コンテナを「動かすのに要るファイルを持った、分離されたプロセス」と対比しています。
| 項目 | 仮想マシン | コンテナ |
|---|---|---|
| 仮想化するもの | ハードウェア | OS の中の見え方(プロセス単位) |
| カーネル | ゲストごとに持つ | ホストと共有(手元でも uname -r は同じ 6.18.44 だった) |
| 大きさ | OS を含むため GB 単位が多い | MB 単位から。手元の最小イメージは圧縮で約1.2MB |
| 起動 | OS の起動を待つ(数十秒〜分) | プロセスの起動と同じ。手元で中央値 3.7ms |
| 動かせる OS | Windows の上で Linux など、別の OS も動く | ホストのカーネルで動くもの(Linux なら Linux 用) |
| 分離の強さ | 強い(カーネルが別) | 仮想マシンより弱い(カーネルを共有) |
| 管理する道具 | ハイパーバイザー(Hyper-V・KVM・VMware など) | Docker・Podman・containerd・Kubernetes など |
起動時間は、同じ環境でホストの /bin/true が中央値 1.0ms、chroot だけが 2.2ms、namespace を5つ分けた unshare+chroot が 3.7ms でした(Python の time.perf_counter で30回ずつ)。コンテナの起動は「OS の起動」ではなく「プロセスの起動」なので、ミリ秒単位で終わります。 仮想マシンはこの環境では動かせないため数値で比べていません。Windows で仮想マシンを作る手順は、関連記事「Hyper-Vとは|有効化・仮想マシン作成・仮想スイッチ・エラー対処」で扱っています。
コンテナと仮想マシンは対立するものではなく、仮想マシンの中でコンテナを動かすのがクラウドではふつうの形です。たとえば Kubernetes のノード(サーバー)の多くは仮想マシンで、その中でコンテナが動きます。
コンテナの仕組み:unshareとchrootで手作りする
この節では、コンテナを作っている Linux の3つの機能と、それだけでコンテナが作れることが、実際に動かした出力で分かります。
コンテナの正体は、Linux カーネルの「namespace(名前空間)」「cgroup(コントロールグループ)」と、ルートディレクトリの切り替え(chroot・pivot_root)の組み合わせです。 Docker の公式ドキュメントも、コンテナという隔離された作業空間を namespace で作っている、と説明しています。
1namespace(見える範囲)
プロセス・ホスト名・ネットワーク・マウントなどを、グループごとに別々に見せる。8種類ある(man7.org)。
2cgroup(使える量)
メモリ・CPU・プロセス数などの上限と使用量を、プロセスのグループごとに管理する。
3ルートの切り替え
「/」をイメージを展開したディレクトリに付け替え、ホストのファイルを見せない。
手順:BusyBox でルートファイルシステムを作る
中で動かすコマンド一式として、静的リンクの BusyBox(1つのファイルに ls・ps・ip などが入ったもの)を作業用のディレクトリに置きました。
R=/srv/work/rootfs
mkdir -p $R/{bin,proc,sys,tmp,etc,root,dev}
cp /bin/busybox $R/bin/
cd $R/bin && for c in $(./busybox --list); do [ "$c" = busybox ] || ln -sf busybox $c; done
echo 'Hello from rootfs' > $R/etc/motd
du -sh $R # 2.1M
実行:namespace を分けて chroot する
unshare は、新しい namespace を作ってその中でコマンドを動かすコマンドです。PID・mount・UTS(ホスト名)・network・IPC を分け、/proc をマウントし直してから、chroot で「/」を付け替えました。
unshare --pid --fork --mount --uts --net --ipc --mount-proc=$R/proc \
chroot $R /bin/sh -c 'hostname t22-box; hostname; echo "pid of me: $$"; ps; ip a; ls /; uname -r'
t22-box
pid of me: 1
PID USER COMMAND
1 root /bin/sh -c hostname t22-box; ...
4 root {ps} /bin/sh -c hostname t22-box; ...
1: lo: <LOOPBACK> mtu 65536 qdisc noop qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
bin
dev
etc
proc
root
sys
tmp
6.18.44-fc-v185
同じ時刻のホストでは、ホスト名は vm、プロセスは69個、ネットワークは lo・eth0 など4つありました。
| 確かめたもの | ホスト | 手作りコンテナの中 |
|---|---|---|
| hostname | vm | t22-box(ホストは vm のまま) |
| ps の数 | 69個 | 2個(自分が PID 1) |
| ip a | lo・eth0 など4つ | lo だけ(しかも DOWN) |
| ls / | ホストの全ファイル | bin・etc など7つだけ |
| uname -r | 6.18.44-fc-v185 | 6.18.44-fc-v185(同じカーネル) |
中からは「自分だけの小さな Linux」に見えますが、カーネルは同じです。 これがコンテナと仮想マシンのいちばん大きな違いです。
ホストから見ると、ふつうのプロセス
コンテナの中で sleep 300 を動かしたまま、ホスト側から見ました。中では PID 1 の sleep が、ホストでは PID 1246 でした。namespace の番号(inode)は、中で見た値とホストの /proc/1246/ns が一致し、ホストの値(pid:[4026531836] など)とは違っていました。
$ ps -o pid,ppid,comm --ppid 1244 # 1244 は unshare
PID PPID COMMAND
1246 1244 sleep
(コンテナの中) (ホストから /proc/1246/ns)
pid -> pid:[4026532267] pid pid:[4026532267]
mnt -> mnt:[4026532264] mnt mnt:[4026532264]
uts -> uts:[4026532265] uts uts:[4026532265]
net -> net:[4026532268] net net:[4026532268]
$ nsenter -t 1246 -p -m -u -n -r -w /bin/sh -c 'hostname; ps'
t22-box
PID USER COMMAND
1 root sleep 300
8 root /bin/sh -c hostname; ps
nsenter は、既にある namespace に入るコマンドです。docker exec で動いているコンテナに入るのも、考え方は同じです。
namespaceを1つずつ足すと何が分かれるか
この節では、namespace の種類ごとの役割と、chroot だけではコンテナにならない理由が、1つずつ足した実測で分かります。
chroot は「/」を付け替えるだけで、プロセス・ホスト名・ネットワークは分かれません。 chroot(2) の man ページも、chroot はセキュリティの目的や、プロセスを完全に閉じ込める(サンドボックス)ためのものではない、と書いています。
| 分けたもの | hostname | ps の数 | ネットワーク |
|---|---|---|---|
| chroot と mount だけ | vm(ホストのまま) | 101(ホストの全プロセス) | 4つ(ホストと同じ) |
| +UTS | box(中だけ変わる) | 99 | 4つ |
| +PID | box | 5(その場のコマンドだけ) | 4つ |
| +network | box | 3 | 1つ(lo だけ) |
chroot だけの段階では、/proc をマウントするとホストの全プロセスが見えました(数が101なのは、計測の時点で動いていたプロセスの数です)。namespace を足すたびに、見える世界が狭くなることが数字で分かります。
namespace は、man7.org の namespaces(7) によると次の8種類です。
| namespace | 分けるもの | unshare の指定 |
|---|---|---|
| PID | プロセス ID(中では自分が PID 1) | --pid --fork |
| Mount | マウントの一覧 | --mount |
| UTS | ホスト名・NIS ドメイン名 | --uts |
| Network | ネットワーク機器・IP・ポート・ルーティング | --net |
| IPC | System V IPC・POSIX メッセージキュー | --ipc |
| User | ユーザー ID・グループ ID(中だけ root にする) | --user |
| Cgroup | cgroup の見え方(ルート) | --cgroup |
| Time | 起動からの時刻(boot・monotonic の時計) | --time |
User namespace を使うと、ホストでは一般ユーザーのまま、中だけ root として振る舞えます。Podman の「rootless(root 権限なし)」のコンテナは、この仕組みを使っています。
cgroupでメモリとプロセス数を制限する(OOMの実物)
この節では、cgroup でコンテナが使える量を決める方法と、上限を超えたときに何が起きるかが、エラーの原文で分かります。
cgroup は、プロセスのグループごとにメモリ・CPU・プロセス数などの上限を決め、使った量を数える仕組みです。 cgroups(7) によると、最初の実装は Linux 2.6.24 で入り、新しい cgroup v2 は Linux 4.5 で正式になりました。Docker の --memory や Kubernetes の resources.limits は、最終的にこの cgroup の値になります。
この環境は v1 と v2 の併用だった
作業用のサブグループを v2 の階層に作ると、使えるコントローラーは hugetlb だけで、memory.max も pids.max もありませんでした。メモリを v2 で有効にしようとすると、書き込みが失敗しました。
$ mkdir /sys/fs/cgroup/unified/t22
$ cat /sys/fs/cgroup/unified/t22/cgroup.controllers
(空)
$ ls /sys/fs/cgroup/unified/t22/memory.max
ls: cannot access 'memory.max': No such file or directory
$ echo "+memory" > /sys/fs/cgroup/unified/cgroup.subtree_control
/bin/bash: line 1: echo: write error: No such file or directory
cgroups(7) は「1つのコントローラーを v1 と v2 の両方で同時には使えない」と書いています。この環境では memory と pids が v1 に結びついていたため、v2 からは使えませんでした。v2 だけで動く環境では、同じことを memory.max・pids.max に書き込んで行います。 今どちらで動いているかは mount | grep cgroup で分かります。cgroup2 on /sys/fs/cgroup type cgroup2 の1行だけなら v2 だけです。
v2 の作業用サブグループでは、コントローラーが無くても使える cgroup.freeze を試しました。コンテナのプロセスを入れて 1 を書くと止まり、0 で再開しました(docker pause と同じ考え方です)。
$ echo 1246 > cgroup.procs
$ echo 1 > cgroup.freeze; grep frozen cgroup.events
frozen 1
$ echo 0 > cgroup.freeze; grep frozen cgroup.events
frozen 0
プロセス数の上限(pids)
pids の上限を5にしたグループにコンテナを入れ、中で sleep を6個起動しようとしました。
started 1
started 2
/bin/sh: can't fork: Resource temporarily unavailable
$ cat pids.current pids.max pids.events
3
5
max 1
$ dmesg | tail -1
cgroup: fork rejected by pids controller in /t22
上限に達すると、新しいプロセスを作る fork が失敗します。カーネルのドキュメントも、上限を超えるプロセスの作成は -EAGAIN で失敗する、と説明しています。同じプロセスが増え続けるような不具合があっても、ホスト全体を道連れにしません。
メモリの上限と OOM(Killed・137)
メモリの上限を32MBにしたグループで、16MB と 64MB を確保する dd を動かしました。
$ echo 32M > memory.limit_in_bytes # v2 なら memory.max
(コンテナの中)
$ dd if=/dev/zero of=/dev/null bs=16M count=1 && echo ok16
1+0 records in
1+0 records out
ok16
$ dd if=/dev/zero of=/dev/null bs=64M count=1; echo exit=$?
Killed
exit=137
$ dmesg | tail -1
Memory cgroup out of memory: Killed process 1896 (dd) total-vm:67968kB, anon-rss:32256kB, ...
上限を超えたプロセスは、カーネルの OOM killer に SIGKILL で止められ、終了コードは 137(128+9)になりました。 ホストのほかのプロセスには影響がありません。Docker で Exited (137) や OOMKilled と出るのは、この動きです。直し方は関連記事「Docker Exited (137)の原因と直し方|OOMKilled」で扱っています。
| 制限したいもの | cgroup v2 のファイル | Docker の指定 |
|---|---|---|
| メモリ | memory.max |
--memory |
| CPU | cpu.max(「上限 周期」の形) |
--cpus |
| プロセス数 | pids.max |
--pids-limit |
| 一時停止 | cgroup.freeze |
docker pause |
| グループごと停止 | cgroup.kill |
(コンテナの停止に使われる) |
コンテナイメージとは(OCIのmanifestとレイヤー)
この節では、コンテナイメージの正体が、手元で作った OCI イメージを展開した中身で分かります。
コンテナイメージは「ファイルの差分を固めた tar(レイヤー)」と「設定(config)」と、それらを並べた「manifest」を、SHA-256 のハッシュ値でつないだものです。 形式は OCI(Open Container Initiative)の image-spec で決まっていて、Docker・Podman・containerd・Kubernetes は同じ形式のイメージを扱えます。
この環境からはレジストリ(Docker Hub など)への接続が許可されていなかったため、skopeo inspect docker://docker.io/library/alpine:3.22 は次のエラーになりました。
level=fatal msg="Error parsing image name \"docker://docker.io/library/alpine:3.22\": pinging container registry registry-1.docker.io: Get \"https://registry-1.docker.io/v2/\": Forbidden"
そこで、前の節の BusyBox のルートファイルシステムを1枚目のレイヤー、アプリ(/app/hello.sh)を2枚目のレイヤーにした OCI イメージを、umoci で作りました。
umoci init --layout oci
umoci new --image oci:mini
# 1枚目:BusyBox の rootfs を入れて repack、2枚目:/app/hello.sh を足して repack
umoci config --image oci:mini --config.cmd /app/hello.sh \
--config.env APP_ENV=demo --config.workingdir /app --tag 1.0
できたディレクトリは、oci-layout・index.json・blobs/sha256/ の3つだけです(OCI の image-layout の仕様どおり)。
oci/blobs/sha256/a1770e99a271bd556bb0f8bd070ffc9307fc3fe9958564b7e38f25df4cab91f6
oci/blobs/sha256/b0d72c86c44f6033316ff220ff874d91e06249833c9a6951aa3bba99c82594e3
oci/blobs/sha256/ca537581821f9535d5d22b164259e9fc677bdf54ef1fae8c12e579f3071a99d2
oci/blobs/sha256/ea1996a615277d64015a99e36d964777b127682ffb251684df16eec2827d5a3e
oci/index.json
oci/oci-layout
入口の index.json から順にたどります。
// index.json:タグ 1.0 → manifest のハッシュ
{"schemaVersion": 2, "manifests": [{
"mediaType": "application/vnd.oci.image.manifest.v1+json",
"digest": "sha256:a1770e99a271...", "size": 503,
"annotations": {"org.opencontainers.image.ref.name": "1.0"}}]}
// blobs/sha256/a1770e99...(manifest):config と layers の一覧
{"schemaVersion": 2,
"config": {"mediaType": "application/vnd.oci.image.config.v1+json",
"digest": "sha256:b0d72c86c44f...", "size": 616},
"layers": [
{"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:ca537581821f...", "size": 1168934},
{"mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
"digest": "sha256:ea1996a61527...", "size": 196}]}
// blobs/sha256/b0d72c86...(config):起動コマンドと環境変数
{"architecture": "amd64", "os": "linux",
"config": {"Env": ["APP_ENV=demo"], "Cmd": ["/app/hello.sh"], "WorkingDir": "/app"},
"rootfs": {"type": "layers", "diff_ids": ["sha256:75e6aaf6c4da...", "sha256:812cb176fd9e..."]}}
レイヤーはただの tar.gz で、tar で中身が見られます。
$ tar tzvf blobs/sha256/ca537581...(1枚目)
drwxr-xr-x 0/0 0 2026-10-29 19:15 bin/
lrwxrwxrwx 0/0 0 2026-10-29 19:15 bin/[ -> busybox
lrwxrwxrwx 0/0 0 2026-10-29 19:15 bin/acpid -> busybox
...(284 項目)
$ tar tzvf blobs/sha256/ea1996a6...(2枚目)
drwxr-xr-x 0/0 0 2026-10-29 19:17 app/
-rwxr-xr-x 0/0 51 2026-10-29 19:17 app/hello.sh
$ sha256sum blobs/sha256/a1770e99...
a1770e99a271bd556bb0f8bd070ffc9307fc3fe9958564b7e38f25df4cab91f6
$ gunzip -c blobs/sha256/ca537581... | sha256sum
75e6aaf6c4da6551a2bfb3388b1f1943875f865112f471e7741d5bba8b9030f3 ← config の diff_ids[0] と一致
| 部品 | 中身 | 手元の大きさ |
|---|---|---|
| index.json | タグと manifest のハッシュ(入口) | 数百バイト |
| manifest | config と layers のハッシュ・大きさ | 503バイト |
| config | 起動コマンド・環境変数・CPU の種類・レイヤーの元のハッシュ | 616バイト |
| レイヤー1 | BusyBox の rootfs(tar.gz) | 1,168,934バイト(展開前は 2,272,256) |
| レイヤー2 | /app/hello.sh だけの差分 | 196バイト |
ファイル名がハッシュ値そのものなので、中身が1バイトでも変われば名前も変わります。 同じレイヤーは何度ダウンロードしても1つで済み、改ざんも見つけられます。manifest の digest は圧縮後の tar のハッシュ、config の diff_ids は展開後の tar のハッシュで、手元でも両方が一致しました。OCI の仕様では、layers の0番目がいちばん下(ベース)のレイヤーで、順に上へ重ねます。
skopeo inspect oci:oci:1.0 でも、同じ manifest のハッシュとレイヤーの一覧が読めました。skopeo copy oci:oci:1.0 dir:skodir で別の形式に写すと、「Copying blob sha256:ca5375…」と同じハッシュのまま写されました。Docker の docker pull も、レジストリから同じ形の manifest とレイヤーを取ってきています。
レイヤーとコピーオンライト(overlayfsとwhiteout)
この節では、読み取り専用のイメージの上でコンテナがファイルを書き換えられる理由と、ファイルを消したときにレイヤーに何が残るかが分かります。
コンテナは、イメージのレイヤーを読み取り専用で下に敷き、その上に書き込み用の層を1枚重ねて動きます。 Linux では overlayfs がこの重ね合わせをします。自分の mount namespace の中だけで、下の層(lower)と上の層(upper)を重ねて試しました。
(重ねた merged で)
$ echo "written in container" > new.txt
$ echo "edited" >> base.txt
$ rm old.txt
--- lower(イメージ側)は変わらない
-rw-r--r-- 1 root root 17 base.txt
-rw-r--r-- 1 root root 14 old.txt
from image layer
--- upper(コンテナの書き込み層)に差分だけが残る
-rw-r--r-- 1 root root 24 base.txt ← 書き換えたときに丸ごとコピーされた
-rw-r--r-- 1 root root 21 new.txt
c--------- 2 root root 0, 0 old.txt ← 消した印(whiteout)
書き換えたファイルは、そのときに上の層へコピーされ(コピーオンライト)、下のイメージは変わりません。 消したファイルは、上の層に「0, 0 の文字デバイス」という印が置かれ、下の層のファイルを隠します。コンテナを消すと上の層も消えるため、コンテナの中で書いたデータは、ボリュームに置かない限り残りません。
イメージのレイヤーでは、この「消した印」を .wh. で始まる空のファイルで表します(OCI の layer の仕様)。umoci で /etc/motd を消して repack すると、3枚目のレイヤーは次の1項目だけでした。
drwxr-xr-x 0/0 0 2026-10-29 19:20 etc/
---------- 0/0 0 1970-01-01 09:00 etc/.wh.motd
大きなファイルをイメージから消したいとき
whiteout は「隠す印」で、下の層の中身は消えない
Docker のデータの置き場所(ボリューム・バインドマウント)は、関連記事「Dockerコマンド早見表|よく使う操作とつながらない時の対処」で扱っています。
runcで同じイメージをコンテナとして動かす
この節では、Docker や Kubernetes の下で実際にコンテナを起動している「低レベルのランタイム」が何をしているかが、実行結果で分かります。
runc は、OCI の runtime-spec にしたがってコンテナを起動するコマンドです。 「バンドル」と呼ぶ、展開したルートファイルシステム(rootfs/)と設定ファイル(config.json)の組を受け取り、namespace と cgroup を用意してプロセスを起動します。umoci でイメージを展開するとバンドルができます。
$ umoci unpack --image oci:1.0 bundle
$ ls bundle
config.json rootfs sha256_a1770e99....mtree umoci.json
(config.json の一部)
process: {"args": ["/app/hello.sh"], "env": [..., "APP_ENV=demo", ...], "cwd": "/app"}
namespaces: ['pid', 'network', 'ipc', 'uts', 'mount']
イメージの config に書いた Cmd と Env が、そのまま config.json の process になっています。ホスト名を runc-box、メモリの上限を64MB、プロセス数の上限を10にして起動しました。
$ cd bundle && runc --root ./state run t22demo
hello from runc-box, pid=1
PID USER COMMAND
1 root {hello.sh} /bin/sh /app/hello.sh
7 root {ps} /bin/sh /app/hello.sh
(sleep 120 に変えて -d で起動し、ホストから)
$ runc --root ./state list
ID PID STATUS BUNDLE ...
t22demo 3527 running /srv/s4/r17/t22/bundle ...
$ grep -E 'memory|pids' /proc/3527/cgroup
8:pids:/t22runc
4:memory:/t22runc
$ cat /sys/fs/cgroup/memory/t22runc/memory.limit_in_bytes /sys/fs/cgroup/pids/t22runc/pids.max
67108864
10
$ runc --root ./state exec t22demo hostname
runc-box
手作りのコンテナで1つずつやったこと(namespace・cgroup・ルートの切り替え)を、runc は config.json を読んでまとめて行っています。 runc kill・runc delete で消すと、cgroup のディレクトリ(t22runc)も消えました。
Docker・Podman・Kubernetesの関係(コンテナ技術の全体図)
この節では、Docker・Podman・containerd・runc・Kubernetes がそれぞれどの役割を持ち、どうつながっているかが図で分かります。
flowchart TD
D[docker CLI] --> DD[dockerd]
DD --> C[containerd]
K[kubelet] -->|CRI| C
K -->|CRI| O[CRI-O]
P[podman] --> R[runc など<br>OCI ランタイム]
C --> R
O --> R
R --> N[namespace と cgroup<br>Linux カーネル]
| 名前 | 役割 | 公式の説明から |
|---|---|---|
| Docker | イメージを作る・取る・動かす一式(CLI・dockerd・レジストリ) | dockerd が API を受け付け、イメージやコンテナを管理する |
| Podman | Docker とほぼ同じ操作を、常駐デーモンなしで行う | デーモンレス。root でも一般ユーザーでも動かせる |
| containerd | イメージの取得・保存からコンテナの実行・監視まで | Linux と Windows で動くデーモン |
| CRI-O | Kubernetes 専用のコンテナランタイム | kubelet と CRI でつながる |
| runc | OCI の仕様どおりにコンテナを起動する(低レベル) | Linux でコンテナを起動・実行する CLI |
| Kubernetes | 多数のコンテナを複数のノードに配置し、数や状態を保つ | 各ノードの kubelet が CRI でランタイムを使う |
- Docker は「コンテナそのもの」ではなく、コンテナを扱う道具の1つです。 この環境の docker コマンド(Client 29.9.2)は、dockerd が動いていないため
failed to connect to the docker API at unix:///var/run/docker.sockと返し、コンテナは runc で直接動かせました。 - Kubernetes は v1.24 で dockershim(Docker Engine を直接使うための部品)を外しました。ただし Docker で作ったイメージは OCI の形式なので、containerd や CRI-O でそのまま動きます。
- Podman の公式ドキュメントは、多くの人は
alias docker=podmanとしてそのまま使える、と書いています。
Docker 以外の選択肢は、関連記事「Podmanとは|Dockerとの違い・使い方とrootless・Quadlet実例」で、Kubernetes の書き方は関連記事「Kubernetes入門|主要リソースとYAMLの書き方、kubectl早見表」で扱っています。
コンテナ技術の歴史と標準
コンテナは突然できた技術ではなく、Unix・Linux の機能を積み重ねたものです。公式の資料で日付を確かめられた出来事を並べます。
- 1chroot(ルートの付け替え)SVr4・4.4BSD の時代からある機能(chroot(2) の HISTORY)
- 2Linux 2.6.24 で cgroups が入る使える量を管理する仕組み(cgroups(7))
- 32015年6月22日 OCI が発足Docker・CoreOS などが、イメージと実行の仕様を共通にする
- 4Linux 4.5 で cgroup v2 が正式に階層を1本にまとめた新しい版
- 52020年5月 OCI の distribution-spec が v1.0レジストリとのやり取りも共通に
- 6Kubernetes v1.24 で dockershim を削除kubelet は CRI でランタイムを使う形に統一
コンテナのメリット・デメリット
この節では、コンテナを使うと何がよくなり、何に気をつけるべきかが、手元で見えたことと合わせて分かります。
メリット
どこでも同じ中身で動く(イメージのハッシュが同じなら中身も同じ)
起動が速く軽い(手元でミリ秒単位・MB 単位)
ホストを汚さない。消せば書き込み層ごと消える
バージョンを並べて動かし、戻すのも簡単(タグを替えるだけ)
上限(cgroup)で、1つの不具合がホスト全体に広がりにくい
デメリット・注意
カーネルを共有するため、分離は仮想マシンより弱い
ホストと違う OS(カーネル)のアプリは動かない
中で書いたデータは消える。データはボリュームに置く
台数が増えると、Kubernetes などの管理の手間と知識が要る
イメージの中のライブラリの更新(脆弱性への対応)は自分で行う
「Windows でも Docker で Linux のコンテナが動く」のは、Docker Desktop が裏で Linux の仮想マシン(WSL 2 など)を動かし、その中でコンテナを動かしているためです。詳しくは関連記事「Docker Desktopとは|インストール・使い方・有料の条件とエラー対処」で扱っています。
仮想マシン・コンテナ・サーバーレスの使い分け(判断表)
この節では、仮想マシン・コンテナ・サーバーレスのどれを選ぶかを、条件ごとに決められます。
サーバーレス(AWS Lambda・Cloud Run など)は、サーバーの管理をクラウド側に任せ、リクエストが来たときだけコードやコンテナを動かす形です。 AWS Lambda の公式ドキュメントは、関数は分離された実行環境で動き、初回の準備(コールドスタート)は100ms 未満から1秒超までかかる、と説明しています。Lambda は OCI 形式のコンテナイメージ(展開後 10GB まで)も受け付けます。
| 項目 | 仮想マシン | コンテナ | サーバーレス |
|---|---|---|---|
| 管理する範囲 | OS から上すべて | イメージ(アプリとライブラリ) | コード(またはイメージ)だけ |
| 起動 | 数十秒〜分 | ミリ秒〜秒 | 温まっていれば速い。初回は待ちが出る |
| 料金の考え方 | 動かしている時間 | 載せるサーバーの時間 | リクエストと実行時間 |
| 長く動き続ける処理 | 向く | 向く | 実行時間の上限がある |
| OS の自由 | 高い(Windows も可) | ホストのカーネルに合わせる | 用意された環境の範囲 |
| こういうとき | 選ぶもの | 理由 |
|---|---|---|
| Linux の上で Windows のアプリを動かしたい | 仮想マシン | カーネルが違う OS はコンテナでは動かない |
| 他社や他の利用者と同じサーバーを分け合う | 仮想マシン(またはその中のコンテナ) | カーネルを共有しない強い分離が要る |
| 開発環境を全員でそろえたい | コンテナ | 同じイメージを配れば中身が同じになる |
| Web API をいくつも動かし、増減させたい | コンテナ(+Kubernetes) | 起動が速く、台数の増減が簡単 |
| CI でテストを毎回まっさらな環境で動かしたい | コンテナ | 使い捨てができ、起動が速い |
| アクセスがまばらで、待ち時間に料金を払いたくない | サーバーレス | リクエストがない間は動かない |
| 特殊なカーネル設定やドライバーが要る | 仮想マシン | コンテナからはホストのカーネルを変えられない |
コンテナでよくあるエラー・つまずき(原文つき)
この節では、コンテナを手作り・実行したときに手元で出たエラーの原文と、原因・直し方が分かります。
| 出た文言 | 原因 | 直し方 |
|---|---|---|
failed to connect to the docker API at unix:///var/run/docker.sock |
dockerd が動いていない | Docker Engine か Docker Desktop を起動する |
Killed・終了コード 137 |
cgroup のメモリ上限を超えて OOM killer に止められた | 上限を上げるか、アプリのメモリ使用を減らす |
can't fork: Resource temporarily unavailable |
pids の上限に達した | 上限を見直す。プロセスが増え続ける原因を探す |
can't open '/dev/null': No such file or directory |
手作りの rootfs に /dev が無い | 必要なデバイスを用意する(runc は config.json どおり自動で作る) |
echo: write error: No such file or directory |
v1 に結びついたコントローラーを v2 で有効にしようとした | v2 だけの構成にするか、v1 側で設定する |
JSON specification file config.json not found |
runc をバンドルの外で実行した | バンドルのディレクトリで実行するか --bundle を付ける |
pinging container registry ... Forbidden |
レジストリへの通信がプロキシなどで止められている | 社内のプロキシ設定・許可されたミラーを確かめる |
PID 1 は SIGTERM で止まらない
手作りコンテナの PID 1(sleep 300)に、ホストから kill -TERM 1246 を送っても止まりませんでした。kill -KILL で止まり、親の unshare も終わりました。
$ kill -TERM 1246; sleep 1; ps -o pid,stat,comm -p 1246
PID STAT COMMAND
1246 S sleep
still alive after SIGTERM
$ kill -KILL 1246 # これで止まった
pid_namespaces(7) によると、namespace の init(PID 1)には、自分でシグナルハンドラーを登録したシグナルしか届きません(祖先の namespace からの SIGKILL・SIGSTOP は例外)。docker stop で止まるまで10秒ほど待たされるのは、アプリが PID 1 として SIGTERM を処理していないことが多い原因です。 Docker の公式ドキュメントは、小さな init を PID 1 に入れる --init を案内しています。
- 1アプリが SIGTERM を受けて終了処理をするかしないなら --init(tini など)を使う
- 2メモリの上限と、アプリの使用量が合っているか137 で落ちるなら、上限か使用量のどちらかを見直す
- 3消えて困るデータをコンテナの中に書いていないか書き込み層はコンテナと一緒に消える
- 4イメージのタグを固定しているかlatest は中身が変わる。ハッシュ(@sha256:)で固定もできる
よくある質問
Q. コンテナとは簡単にいうと何ですか? A. アプリと動かすのに要るファイルをまとめ、ほかのプロセスから仕切って動かす仕組みです。中身はふつうの Linux のプロセスで、見える範囲(namespace)と使える量(cgroup)が分けられています。
Q. コンテナと仮想マシンの違いは何ですか? A. 仮想マシンはゲスト OS(自分のカーネル)を丸ごと動かし、コンテナはホストのカーネルを共有します。そのためコンテナは軽く速く起動しますが、分離は仮想マシンより弱く、違う OS は動かせません。
Q. コンテナと Docker は同じものですか? A. 違います。コンテナは技術(仕組み)の名前で、Docker はコンテナを作って動かす道具の1つです。Podman・containerd・runc でも同じ OCI 形式のコンテナを動かせます。
Q. コンテナイメージとコンテナの違いは? A. イメージは読み取り専用のひな形(レイヤーと設定)、コンテナはそのイメージから起動した実行中のプロセスです。1つのイメージから、いくつでもコンテナを起動できます。
Q. コンテナの中で root なら、ホストでも root ですか? A. User namespace を使わない場合、中の root はホストの root と同じユーザー ID です。Podman の rootless や Docker の rootless モードは、User namespace で中の root をホストの一般ユーザーに対応させます。
Q. Windows や Mac でもコンテナは動きますか? A. Linux のコンテナは Linux のカーネルが必要なので、Windows・Mac では Docker Desktop などが裏で Linux の仮想マシンを動かし、その中で動かします。Windows 用のコンテナ(Windows のカーネルを共有するもの)もあります。
Q. コンテナは仮想マシンより安全ではないのですか? A. カーネルを共有するぶん、分離の強さは仮想マシンに劣ります。信頼できない人のコードを動かすときは、仮想マシンの中でコンテナを動かす、軽量な仮想マシンでコンテナを包む、といった形が使われます。
Q. コンテナを止めるとデータは消えますか? A. 止めただけでは消えませんが、コンテナを削除すると書き込み層ごと消えます。残したいデータはボリュームやバインドマウントに置きます。
Q. Kubernetes は何をしてくれますか? A. 複数のサーバーにコンテナを配置し、落ちたら起動し直し、負荷に合わせて数を増減し、外からの通信を振り分けます。1台で数個のコンテナを動かすだけなら Docker Compose で間に合うことも多いです。
Q. コンテナ技術を学ぶ順番は? A. Docker でイメージの取得・起動・削除を覚え、Dockerfile でイメージを作り、Compose で複数のコンテナをつなぎ、最後に Kubernetes に進む順が一般的です。仕組みを知りたいときは、この記事の unshare の実験を作業用の環境で試すと理解が深まります。
まとめ
- コンテナとは、ホストのカーネルを共有したまま、namespace で見える範囲を、cgroup で使える量を分けたプロセス。手作りコンテナの
sleepもホストからは PID 1246 のふつうのプロセスだった。 - 仮想マシンとの違いはゲスト OS(カーネル)の有無。手作りコンテナは中央値 3.7ms で起動し、
uname -rはホストと同じだった。 - namespace を足すたびに hostname・ps・ip a がホストと分かれ、cgroup の上限を超えると fork 失敗や
Killed(137)になった。 - イメージは、tar のレイヤー・config・manifest をハッシュでつないだもの(OCI)。消したファイルは
.wh.の印で隠すだけで、下のレイヤーには残る。 - Docker・Podman・Kubernetes は、どれも最後は runc などの OCI ランタイムで namespace と cgroup を使う。強い分離や別 OS は仮想マシン、アプリの配布と増減はコンテナ、まばらな処理はサーバーレス。
参考・出典
確認日はいずれも 2026年10月29日です。
- man7.org「namespaces(7)」:https://man7.org/linux/man-pages/man7/namespaces.7.html
- man7.org「pid_namespaces(7)」:https://man7.org/linux/man-pages/man7/pid_namespaces.7.html
- man7.org「cgroups(7)」:https://man7.org/linux/man-pages/man7/cgroups.7.html
- man7.org「chroot(2)」:https://man7.org/linux/man-pages/man2/chroot.2.html
- The Linux Kernel documentation「Control Group v2」:https://docs.kernel.org/admin-guide/cgroup-v2.html
- Open Container Initiative「About the OCI」:https://opencontainers.org/about/overview/
- OCI image-spec「Image Layout」:https://github.com/opencontainers/image-spec/blob/main/image-layout.md
- OCI image-spec「Image Manifest」:https://github.com/opencontainers/image-spec/blob/main/manifest.md
- OCI image-spec「Image Layer Filesystem Changeset」:https://github.com/opencontainers/image-spec/blob/main/layer.md
- runc(README):https://github.com/opencontainers/runc
- Docker Docs「What is a container?」:https://docs.docker.com/get-started/docker-concepts/the-basics/what-is-a-container/
- Docker Docs「What is Docker?」:https://docs.docker.com/get-started/docker-overview/
- Docker Docs「Packet filtering and firewalls」:https://docs.docker.com/engine/network/packet-filtering-firewalls/
- Docker Docs「Run multiple processes in a container」(--init):https://docs.docker.com/engine/containers/multi-service_container/
- containerd:https://containerd.io/
- Podman Documentation:https://docs.podman.io/en/latest/
- Kubernetes「Container Runtime Interface (CRI)」:https://kubernetes.io/docs/concepts/containers/cri/
- Kubernetes Blog「Updated: Dockershim Removal FAQ」:https://kubernetes.io/blog/2022/02/17/dockershim-faq/
- AWS「Understanding the Lambda execution environment lifecycle」:https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtime-environment.html
- AWS「Create a Lambda function using a container image」:https://docs.aws.amazon.com/lambda/latest/dg/images-create.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「コンテナとは?仮想マシンとの違い|namespace・cgroupで手作りして理解」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。