Dockerとは?仮想マシンとの違い・仕組み・情シスでの使いどころを整理
Dockerとは何かをnamespace・cgroup・overlayの実測出力で説明。仮想マシンとの違い、Dockerではないもの、情シスのコンテナ/VM/SaaS判断基準まで。
Dockerとは、Linuxカーネルのnamespace・cgroup・overlayファイルシステムを使って、アプリを「イメージ」として配布し「コンテナ」という隔離されたプロセスとして動かすためのツール一式です。仮想マシンではなく、コンテナの中のプログラムはホストのカーネルをそのまま使います。この記事の実行結果はすべて、Ubuntu 26.04.1(カーネル7.0.0)上のDocker Engine 29.1.3、containerd 2.2.2、runc 1.4.0で採取したものです(執筆時点)。
読者として想定しているのは、ベンダーから「Dockerで動きます」と言われた製品を評価する情シス、1〜2台のサーバーに複数のアプリを載せたいインフラ担当、そして「軽い仮想マシン」という説明で止まっている人です。読み終える頃には、docker run・inspect・diff・historyの出力を根拠にコンテナの正体を説明でき、コンテナ・VM・SaaSのどれを選ぶかを自分のチームの条件で決められるようになります。
この記事でわかること
- コンテナは別のカーネルではなく、namespaceとcgroupで隔離されたホスト上の1プロセスであること(uname・ps・/proc/<pid>/nsで確認)
--memoryや--cpusがcgroupのどこに書かれ、上限を超えると何が起きるか(終了コード137)- イメージのレイヤー構造と、コンテナの書き込み層・ボリュームの違い(docker history・diffの実測値)
- Dockerが「ではない」もの:仮想マシン、標準設定でのセキュリティ境界、バックアップ
- 情シス向けの判断基準:コンテナ/VM/SaaSの選び分け、Docker Engine・Docker Desktop・Podmanの比較、ライセンスと運用責任
Dockerとは:ホストのカーネルを共有する「隔離されたプロセス」
いちばん短い証明は、ホストとコンテナの中でuname -rを比べることです。ディストリビューションはUbuntuとAlpineで別物なのに、カーネルのバージョンは1文字も違いません。
$ uname -r
7.0.0-31-generic
$ docker run --rm --memory 256m alpine:3 uname -r
7.0.0-31-generic
$ head -1 /etc/os-release
PRETTY_NAME="Ubuntu 26.04.1 LTS"
$ docker run --rm --memory 256m alpine:3 head -1 /etc/os-release
NAME="Alpine Linux"中にあるのはAlpineのファイル一式(/bin/sh、/etc/os-release、musl libcなど)であって、Alpineのカーネルではありません。もう1つの証拠がプロセス一覧です。コンテナでpsを実行すると、PID 1がそのps自身になります。initもsystemdもsshdもcronも動いていない、つまりOSは「起動」していないのです。
$ docker run --rm --memory 256m alpine:3 ps
PID USER TIME COMMAND
1 root 0:00 ps
$ docker run --rm --memory 256m ubuntu:24.04 cat /proc/1/comm
cat
$ docker run --rm --memory 256m ubuntu:24.04 systemctl status
docker: Error response from daemon: failed to create task for container: ... exec: "systemctl": executable file not found in $PATHubuntu:24.04イメージにはsystemctlの実行ファイル自体が入っていません。VMの感覚でsystemctl enableを書いたスクリプトをコンテナに持ち込むと、この「executable file not found in $PATH」で止まります。サービスの起動と再起動はDocker側(--restartポリシーやCompose)が受け持つ設計です。
では誰がこのプロセスを作っているのか。エンジンの実体は1つのプログラムではなく、APIとイメージ管理を担うdockerd、コンテナのライフサイクルを管理するcontainerd、実際にnamespaceとcgroupを組み立ててプロセスを起動するrunc(OCIランタイム)の3層です。sleep 600を動かすコンテナを起動し、そのプロセスをホスト側から追うと、親がcontainerdのshimであることが分かります。
$ docker run -d --memory 256m --name what-is-docker-sleep alpine:3 sleep 600
$ docker inspect --format '{{.State.Pid}}' what-is-docker-sleep
812101
$ ps -o pid,ppid,user,cmd -p 812101
PID PPID USER CMD
812101 812080 root sleep 600
$ ps -o pid,ppid,user,cmd -p 812080
PID PPID USER CMD
812080 1 root /usr/bin/containerd-shim-runc-v2 -namespace moby -id 06db7b43329c... -address /run/containerd/containerd.sock
$ docker exec what-is-docker-sleep ps
PID USER TIME COMMAND
1 root 0:00 sleep 600
7 root 0:00 psホストからはPID 812101に見える同じプロセスが、コンテナの中ではPID 1です。プロセスは1つで、番号の見え方だけが違います。イメージの形式と実行時の仕様はOCI(Open Container Initiative)として標準化されているので、同じイメージをPodmanやKubernetes(containerd)でもそのまま動かせます。
Dockerと仮想マシンの違い:何を仮想化しているか
仮想マシンは、ハイパーバイザー(Proxmox VEのKVM、Hyper-V、vSphere)がCPU・メモリ・ディスク・NICを模倣し、その上でゲストOSが自分のカーネルを起動します。コンテナが分けるのは、カーネルが各プロセスに見せる「プロセス一覧・ネットワーク・マウント・ホスト名」だけです。違いを表にすると次のようになります。
| 観点 | 仮想マシン | Dockerコンテナ |
|---|---|---|
| 何を仮想化するか | ハードウェア(CPU・メモリ・ディスク・NIC) | カーネルが見せる名前空間(プロセス・ネットワーク・マウント・ホスト名) |
| カーネル | ゲストごとに独立 | ホストと共有(上のuname -rが同じ) |
| 起動するもの | ブートローダー→カーネル→init→各サービス | 指定したプロセス1つ(ミリ秒〜秒) |
| 動かせるOS | ホストと異なるOS(Windows Server、別バージョンのカーネル) | ホストのカーネルで動くユーザーランドのみ。Linuxホスト上ではLinuxディストリビューションだけ |
| 隔離の強さ | ハイパーバイザーの境界。ゲストのカーネル脆弱性はゲスト内で止まりやすい | カーネル共有。カーネルの脆弱性や設定ミスがホストに直結 |
| リソースの上乗せ | ゲストOS分のメモリとディスク(数百MB〜数GB) | イメージのレイヤーと書き込み層のみ(alpine:3で約13MB) |
| 保存・復元の単位 | ディスクイメージとスナップショット | イメージ+ボリューム(コンテナ本体は使い捨て) |
| 管理ツールの例 | Proxmox VE、vSphere、Hyper-V | Docker CLI、Compose、Portainer、Kubernetes |
「カーネルを共有している」ことが実務で効いてくる場面の1つが、リソースの見え方です。--memory 256mを付けて起動したコンテナの中で/proc/meminfoを読むと、ホストの物理メモリがそのまま表示されます。
$ docker run --rm --memory 256m alpine:3 sh -c 'head -1 /proc/meminfo; nproc'
MemTotal: 8126508 kB
4
$ head -1 /proc/meminfo; nproc
MemTotal: 8126508 kB
4/procはカーネルが持つ情報の窓口で、cgroupで上限を付けてもここには反映されません。JVMやデータベースのように、起動時にメモリ量を見てキャッシュサイズを自動で決めるソフトは、ホストの8GBを前提に確保しようとして上限の256MBを超え、後述のOOM killで落ちます。コンテナで動かすときはヒープやバッファプールのサイズを設定で明示するのが決まりごとです(新しめのJVMはcgroupの上限を自分で読みますが、使うバージョンで確かめてください)。
VMを選ぶべき条件は明確です。Windows ServerやActive Directoryのようにホストと違うOSが要る、ベンダーのサポート条件が「VM上のRHEL 9.x」のようにOSとVMを指定している、カーネルモジュールや特殊なデバイスドライバが必要、互いに信頼できない利用者や部門に環境を貸す、のどれか1つでも当てはまればVMです。コンテナはこれらを解決しません。
仕組み①:namespaceが「見えるもの」を分ける
namespaceは、カーネルの資源をプロセスのグループごとに別の名前空間で見せる仕組みで、pid・mnt・net・uts(ホスト名)・ipc・user・cgroup・timeの種類があります。各プロセスがどのnamespaceに属しているかは/proc/<pid>/ns/のシンボリックリンクで分かり、リンク先の番号が同じなら同じnamespaceです。ホストのシェルと、先ほどのsleep 600(PID 812101)を比べます。
$ ls -l /proc/self/ns/ | awk '{print $9,$10,$11}' | grep -E '^(pid|mnt|net|uts|user) '
mnt -> mnt:[4026531832]
net -> net:[4026531833]
pid -> pid:[4026531836]
user -> user:[4026531837]
uts -> uts:[4026531838]
$ ls -l /proc/812101/ns/ | awk '{print $9,$10,$11}' | grep -E '^(pid|mnt|net|uts|user) '
mnt -> mnt:[4026532631]
net -> net:[4026532636]
pid -> pid:[4026532634]
user -> user:[4026531837]
uts -> uts:[4026532632]mnt・net・pid・utsの4つは番号が変わっています。ルートファイルシステム、ネットワークスタック、PIDの採番、ホスト名がそれぞれ別になった、という意味です。一方でuserだけは4026531837のままホストと同じです。つまりコンテナ内のroot(uid 0)はホストのuid 0と同一人物だということで、これが後述の「セキュリティ境界ではない」の根拠になります。
utsとnetの効果は、ホスト名とIPアドレスに現れます。標準のbridgeネットワークでは、コンテナごとにveth(仮想NICの対)が作られ、docker0ブリッジ配下の172.17.0.0/16からアドレスが振られます。--network noneにするとループバックしか残りません。
$ docker exec what-is-docker-sleep hostname
06db7b43329c
$ docker run --rm --memory 256m alpine:3 ip -4 -o addr
1: lo inet 127.0.0.1/8 scope host lo ...
2: eth0 inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0 ...
$ docker run --rm --memory 256m --network none alpine:3 ip -4 -o addr
1: lo inet 127.0.0.1/8 scope host lo ...外から到達させるには-pでポートを公開します。ホスト側ではdocker-proxyというプロセスが待ち受け、コンテナの8000番に転送します。次の例はpython:3.12-slimの内蔵HTTPサーバーを127.0.0.1の18080番に結びつけたものです。
$ docker run -d --memory 256m --name what-is-docker-web -p 127.0.0.1:18080:8000 -w /srv python:3.12-slim \
sh -c 'echo "<h1>hello from container</h1>" > index.html && python3 -m http.server 8000'
$ curl -s http://127.0.0.1:18080/
<h1>hello from container</h1>
$ docker port what-is-docker-web
8000/tcp -> 127.0.0.1:18080
$ ss -ltnp | grep ':18080 '
LISTEN 0 4096 127.0.0.1:18080 0.0.0.0:* users:(("docker-proxy",pid=815089,fd=7))-p 8080:80のようにアドレスを省くと0.0.0.0で待ち受けます。dockerdは自分でiptablesのFORWARDチェーンにルールを入れるため、ufwでINPUTを閉じていても転送されたパケットは届きます(執筆時点の標準設定)。インターネットに面したサーバーでは127.0.0.1:を付けてリバースプロキシ経由にするか、/etc/docker/daemon.jsonの"ip": "127.0.0.1"で既定の公開アドレスを変えておきます。
仕組み②:cgroupが「使える量」を制限する
namespaceが「見えるもの」を分けるのに対し、cgroup(control group)は「使える量」を制限します。このホストはcgroup v2で、cgroupドライバーはsystemd(docker infoのCgroup Version: 2 / Cgroup Driver: systemd)なので、コンテナはsystem.slice配下のdocker-<コンテナID>.scopeという単位になります。--memory 256mは、そのscopeのmemory.maxに書き込まれます。
$ cat /proc/812101/cgroup
0::/system.slice/docker-06db7b43329c9229b6d5b8da3cbe3737445f4fc28e90f90c029e488ce0073175.scope
$ cat /sys/fs/cgroup/system.slice/docker-06db7b43329c...scope/memory.max
268435456
$ docker exec what-is-docker-sleep cat /proc/1/cgroup
0::/
$ docker exec what-is-docker-sleep cat /sys/fs/cgroup/memory.max
268435456
$ docker stats --no-stream --format 'table {{.Name}}\t{{.MemUsage}}\t{{.MemPerc}}' what-is-docker-sleep
NAME MEM USAGE / LIMIT MEM %
what-is-docker-sleep 560KiB / 256MiB 0.21%
$ docker run --rm --memory 256m --cpus 0.5 alpine:3 cat /sys/fs/cgroup/cpu.max
50000 100000268435456は256×1024×1024です。内側から見たcgroupのパスが/になっているのは、cgroup namespace(docker infoのSecurity Optionsにあるcgroupns)で自分のscopeを根に見せているためです。CPUは--cpus 0.5でcpu.maxが「100000マイクロ秒の周期のうち50000マイクロ秒まで」、つまり1コアの半分になります。
上限を超えるとどうなるかも実際に試せます。64MBの上限で、tailに200MBの入力を流し込みます(tailは末尾を出すために入力を全部メモリに溜めるので、確実に上限を踏みます)。
$ docker run --memory 64m --name what-is-docker-oom alpine:3 sh -c 'head -c 200m /dev/zero | tail'
Killed
$ echo $?
137
$ docker inspect --format 'OOMKilled={{.State.OOMKilled}} ExitCode={{.State.ExitCode}}' what-is-docker-oom
OOMKilled=true ExitCode=137終了コード137は128+9、つまりSIGKILLで止められたことを表し、docker inspectのOOMKilledがtrueになります。共有サーバーでは全コンテナに--memory(Composeならdeploy.resources.limits.memory)を付けておく、というのが筆者の決まりです。上限なしで暴走した1つのコンテナはホスト全体のOOM killerを呼び、そのとき止められる候補にはホストで動くMariaDBやnginxも含まれます。上限を付けておけば、cgroupの中で先に決着がつきます。
仕組み③:イメージはレイヤーの積み重ね、コンテナは薄い書き込み層
コンテナのルートは、overlayファイルシステムのマウントです。中から/proc/self/mountinfoを読むと、lowerdirとupperdirが見えます(パスは長いので一部を省略しています)。
$ docker run --rm --memory 256m alpine:3 sh -c 'grep " / " /proc/self/mountinfo'
561 521 0:61 / / rw,relatime - overlay overlay rw,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2099/fs:/var/lib/containerd/.../snapshots/425/fs,upperdir=/var/lib/containerd/.../snapshots/2100/fs,workdir=/var/lib/containerd/.../snapshots/2100/work,nouserxattrlowerdirにはイメージの各レイヤーが読み取り専用で重ねられ、upperdirがそのコンテナ専用の書き込み層です。読み取りは上の層から順に探し、書き込みは下の層のファイルをupperにコピーしてから行い(copy-up)、削除は「消した」という印(whiteout)をupperに置きます。イメージがどんなレイヤーでできているかはdocker historyで確認できます。
$ docker history alpine:3
IMAGE CREATED CREATED BY SIZE COMMENT
294b683cb724 6 days ago CMD ["/bin/sh"] 0B buildkit.dockerfile.v0
<missing> 6 days ago ADD alpine-minirootfs-3.24.2-x86_64.tar.gz /… 9.08MB buildkit.dockerfile.v0
$ docker image ls
IMAGE ID DISK USAGE CONTENT SIZE EXTRA
alpine:3 294b683cb724 13MB 3.94MB
ubuntu:24.04 008173c23f95 119MB 31.7MB
python:3.12-slim 2f17fc044b57 179MB 45.4MBalpine:3の履歴は「minirootfsのtar.gzを展開した層(9.08MB)」と「CMDを記録しただけの0Bの行」の2行ですが、ファイルシステムの層は1つだけです(docker image inspect --format '{{len .RootFS.Layers}}' alpine:3は1を返します)。docker image lsのCONTENT SIZEは取得時の圧縮済みサイズ、DISK USAGEはそれに展開済みのスナップショットを足した使用量で、3.94MB+9.08MB≒13MBと計算が合います。この2列の表示はDocker 29でcontainerdイメージストアが有効なときのもので(執筆時点)、従来のSIZE列1つの表示とは意味が違います。
「レイヤーは足し算しかできない」ことを、5MBのファイルを作って消すだけの2種類のDockerfileで確かめます。
$ cat Dockerfile
FROM alpine:3
RUN head -c 5m /dev/urandom > /big.bin
RUN rm /big.bin
$ docker build -t what-is-docker-layers:two-run .
$ docker history what-is-docker-layers:two-run
IMAGE CREATED CREATED BY SIZE
f59bc4993853 1 second ago /bin/sh -c rm /big.bin 4.1kB
f6d1ffed4544 1 second ago /bin/sh -c head -c 5m /dev/urandom > /big.bin 5.25MB
294b683cb724 6 days ago CMD ["/bin/sh"] 0B
<missing> 6 days ago ADD alpine-minirootfs-3.24.2-x86_64.tar.gz /… 9.08MB
$ docker image ls
alpine:3 294b683cb724 13MB 3.94MB
what-is-docker-layers:one-run f1923f36718b 12.9MB 3.85MB
what-is-docker-layers:two-run f59bc4993853 23.5MB 9.1MBRUNを2つに分けたtwo-runでは、rmの層は4.1kBのwhiteoutを足しただけで、5.25MBの層はそのまま残り、イメージは23.5MB、RootFS.Layersは3になりました。RUN head -c 5m /dev/urandom > /big.bin && rm /big.binと1つのRUNで作って消したone-runは12.9MBで、alpine:3とほぼ同じです。apt-getのキャッシュを同じRUNの中で消す慣習や、マルチステージビルドが必要になる理由がこれです(軽量化の手順はDockerfileの書き方の記事で扱っています)。なお、このホストのdocker buildは「DEPRECATED: The legacy builder is deprecated」と警告を出しました。Ubuntuのdocker.ioパッケージにはbuildxプラグインが含まれないためで、BuildKitでビルドするにはdocker-buildxパッケージかDocker公式リポジトリの導入が必要です。
コンテナ側の書き込み層はdocker diffで覗けます。sleep 600のコンテナでファイルを作り、書き換え、消してから見ると次のようになります。
$ docker exec what-is-docker-sleep sh -c 'echo memo > /tmp/note.txt; echo "# edited" >> /etc/profile; rm /etc/motd'
$ docker diff what-is-docker-sleep
C /tmp
A /tmp/note.txt
C /etc
C /etc/profile
D /etc/motd
$ docker run --rm --memory 256m alpine:3 ls -l /tmp/note.txt /etc/motd
ls: /tmp/note.txt: No such file or directory
-rw-r--r-- 1 root root 284 Mar 24 2026 /etc/motdAは追加、Cは変更、Dは削除で、いずれもupperdirにだけ記録されています。同じイメージから起動した別のコンテナには何も引き継がれず、docker rmすればupperdirごと消えます。残したいデータはボリュームに置きます。ボリュームはホストの/var/lib/docker/volumes/配下にある普通のディレクトリで、コンテナを消しても残ります。
$ docker volume create what-is-docker-data
$ docker run --rm --memory 256m -v what-is-docker-data:/data alpine:3 sh -c 'echo persisted > /data/note.txt'
$ docker run --rm --memory 256m -v what-is-docker-data:/data alpine:3 cat /data/note.txt
persisted
$ docker volume inspect --format '{{.Mountpoint}}' what-is-docker-data
/var/lib/docker/volumes/what-is-docker-data/_data整理すると、イメージは「読み取り専用のレイヤー+起動時の既定値(Cmd=[/bin/sh]やPATHなどのEnv)」、コンテナは「イメージ+書き込み層+実行時の設定(上限、ネットワーク、マウント)」です。docker inspectを先ほどのコンテナに向けると、Image=alpine:3、Memory=268435456、IP=172.17.0.3、Mounts=[]という組み合わせが1か所にまとまって見えます。
レジストリ・タグ・ダイジェスト:イメージはどこから来るか
$ docker pull alpine:3
3: Pulling from library/alpine
Digest: sha256:294b683cb724975bec92580e1e685676bd4b50bda910ddb8c51d4cabeaec77e6
Status: Image is up to date for alpine:3
docker.io/library/alpine:3alpine:3という短い名前は、docker.io/library/alpine:3(レジストリのホスト名/名前空間/リポジトリ:タグ)に補完されます。タグは付け替えられる名札で、3は執筆時点では3.24.2を指していますが(historyのADD行に出ていたminirootfs-3.24.2)、来月には別の中身になりえます。Digestのsha256:294b…は中身から計算した値なので変わりません。本番で「先月と同じものを動かしたい」なら、alpine:3.24のように細かいタグを使うかalpine@sha256:…とダイジェストで固定し、更新は自分の意思で行います。
docker manifest inspect alpine:3を実行すると、1つのタグの下にamd64・arm・arm64などアーキテクチャ別のマニフェストが並んだ「イメージインデックス」が返ります。architectureがunknown/unknownの項目はビルドの来歴情報(attestation)で、実行用ではありません。Raspberry PiとサーバーのPCで同じdocker pullが通るのは、この索引のおかげです。なお、Docker Hubには匿名・無料利用向けの取得回数制限があるので(執筆時点、具体値は公式ドキュメントで確認)、チームで使うならdocker loginしたうえで、社内レジストリやプルスルーキャッシュを検討します。
Dockerではないもの:VM・セキュリティ境界・バックアップ
標準設定のコンテナはセキュリティ境界ではない
userのnamespaceがホストと同じだったことを思い出してください。root権限を細かく分ける仕組みとしてcapability(root権限を41種類に分割したもの)があり、Dockerは標準で14種類に絞ります。
$ docker run --rm --memory 256m alpine:3 grep CapEff /proc/self/status
CapEff: 00000000a80425fb
$ grep CapEff /proc/self/status
CapEff: 000001ffffffffff
$ docker run --rm --memory 256m -v /etc/shadow:/shadow:ro alpine:3 sh -c 'ls -l /shadow; test -r /shadow && echo readable'
-rw-r----- 1 root shadow 1203 Sep 23 20:36 /shadow
readable0xa80425fbは立っているビットが14個、ホストの0x1ffffffffffは41個です。絞られてはいますが、uid 0であることは変わらないので、バインドマウントでホストのファイルを渡せばそのまま読めます。つまり、dockerコマンドを実行できる人(dockerグループのメンバー)は-v /:/hostと書くだけでホストのrootと同じことができます。監査のときにgetent group dockerで誰が入っているかを棚卸しする理由がこれで、Portainerのような管理UIにdocker.sockを渡す判断も同じ重さです(Portainerの記事で権限設計を扱っています)。緩和策はどれも1オプションで試せます。
$ docker run --rm --memory 256m --user 1000:1000 alpine:3 id
uid=1000 gid=1000 groups=1000
$ docker run --rm --memory 256m --cap-drop ALL alpine:3 grep CapEff /proc/self/status
CapEff: 0000000000000000
$ docker run --rm --memory 256m --read-only alpine:3 touch /etc/x
touch: /etc/x: Read-only file system実運用では、アプリの実行ユーザーを--user(Composeならuser:)で非rootにし、cap_drop: [ALL]と必要なcap_addだけを書き、書き込みが要らないならread_only: true、加えてsecurity_opt: [no-new-privileges:true]を付けます。--privilegedは本番ホストでは使いません。/proc/sysがホストのものになり、カーネルパラメータの変更が全コンテナと本番サービスに及ぶからです。もう一段強い分離が要るときはrootlessモード(dockerd自体を一般ユーザーで動かし、コンテナのrootをuser namespaceで別のuidに対応付ける)があります。執筆時点では1024未満のポートの公開やストレージ・ネットワーク機能に制約があり、この記事のホストでは検証していません。
バックアップの仕組みは含まれていない
イメージはレジストリから取り直せますが、書き込み層とボリュームの中身は誰も複製していません。docker rm -vは無名ボリュームまで消しますし、docker compose down -vは名前付きボリュームも消します。バックアップの対象は、名前付きボリューム(アプリを止めて/var/lib/docker/volumes/<名前>/_dataをtarで固めるか、DBならdocker execでpg_dumpやmariadb-dumpを取る)、compose.yamlと.env、それに使っているイメージのタグとダイジェストの記録、の3つです。このどれもDockerは自動では行いません。実際の手順はDocker Compose入門の更新とバックアップの節にあります。
標準のログドライバーjson-fileにはローテーションの設定がなく、/var/lib/docker/containers/<ID>/<ID>-json.logは容量の上限なしに増え続けます。/etc/docker/daemon.jsonに"log-opts": {"max-size": "10m", "max-file": "3"}を書いてdockerdを再起動してください。設定は再起動後に作られたコンテナから効くので、既存のコンテナは作り直しが必要です。
情シスの判断基準:コンテナが効く場面、VMやSaaSにすべき場面
ここまでの仕組みを踏まえると、「コンテナにするか」は技術の好みではなく次の条件で決まります。
| 状況 | 選ぶもの | 理由 |
|---|---|---|
| 製品の配布形態がコンテナイメージ(Wiki.js、Nextcloud、Portainer、GitLabなど) | コンテナ | イメージをそのまま使える。VMに入れ直すと更新のたびに手作業が増える |
| 同じサーバーで複数アプリを動かし、ランタイムの版が衝突する(PHP 8.1と8.3、Python 3.10と3.12) | コンテナ | 依存はイメージごとに閉じる。上限はcgroupで個別に付けられる |
| 検証環境を頻繁に作って捨てる | コンテナ | docker runからdocker rmまで数秒。ディスクはレイヤー共有で増えにくい |
| Windows Server、Active Directory、ベンダーサポートがOSやVMを指定 | VM | カーネルを共有するコンテナでは要件を満たせない |
| カーネルモジュールや特殊デバイスが必要 | VM | 本番ホストでモジュールを読み込む運用は他のサービスに影響する |
| 互いに信頼できない利用者・部門に環境を貸す | VM | 標準設定のコンテナはroot境界が共有される |
| 担当者1人で、イメージ更新・脆弱性対応・バックアップの当番を組めない | SaaS(既存のMicrosoft 365やGoogle Workspaceの範囲を優先) | コンテナは運用を減らさず、置き場所を変えるだけ |
| 監査で「誰がいつ何を変えたか」の証跡と責任分界が要る | SaaS、またはVM+既存の変更管理手順 | Dockerは変更履歴を自動では残さない。Composeファイルのgit管理が最低条件 |
少人数のチームが持つ1〜2台のサーバーでコンテナが割に合うのは、「1台に複数アプリ」「配布形態がイメージ」「作っては捨てる」のうち2つ以上に当てはまるときです。逆に、1つの業務システムが数年間ほぼ変更なしで動くだけなら、OSベンダーの更新に乗れるVMのほうが引き継ぎは簡単で、コンテナにする理由は薄くなります。
コンテナ化しても消えない運用責任も先に決めておきます。「Dockerで動く=運用が楽」ではなく、責任の置き場所が変わるだけです。
| 責任の範囲 | コンテナ化後も残る作業 | 見落としがちな点 |
|---|---|---|
| ホストOSのパッチ | aptによる更新と再起動の計画 | カーネル更新の再起動で全コンテナが止まる。VMのように1台ずつ、とはいかない |
| Docker Engineの更新 | aptでdocker.io(またはdocker-ce)を更新 | dockerdの再起動でコンテナも止まる。daemon.jsonの"live-restore": trueで回避できる |
| イメージとアプリの更新 | docker compose pull && docker compose up -dを定期に実行 | ベースイメージの脆弱性は自分で追う。イメージスキャナを定期実行に組み込む |
| データのバックアップ | 名前付きボリュームのtar、DBのダンプ、compose.yamlと.envの保管 | compose down -vやrm -vで消える。復元の練習を年1回は行う |
| アクセス権 | dockerグループの棚卸し、docker.sockを渡す相手の管理 | dockerグループはrootと同義。退職者の削除漏れが典型 |
| 監視とログ | --restart unless-stopped、docker stats、ログのローテーション | json-fileの肥大化、OOMKilledの見逃し |
稟議で問われるライセンスは、製品ごとに答えが違います。Linuxサーバー用のDocker EngineはApache License 2.0のオープンソースで、利用料はありません。一方、WindowsやmacOSの開発PCに入れるDocker Desktopは、執筆時点では個人利用・教育・小規模な組織は無償ですが、従業員数と年間売上の両方で定められた規模を超える企業では有料サブスクリプションが必要です。閾値と条件は変更されることがあるため、Docker公式のサブスクリプション条件で確認してから台数分を申請します。有料化を避けたいWindows環境では、WSL2の中にDocker Engineを直接入れる構成が代替になります。
Docker Engine・Docker Desktop・Podmanの比較
「Docker」と一口に言っても、サーバーに入れるDocker Engine、開発PC向けのDocker Desktop、そしてDocker互換のPodmanで性格が異なります。
| 項目 | Docker Engine | Docker Desktop | Podman |
|---|---|---|---|
| 動く場所 | Linuxサーバー、WSL2 | Windows・macOS・Linuxの開発PC(内部にLinux VMを持つ) | Linux(RHEL系では標準)。Windows/macOSはpodman machineというVM経由 |
| 常駐デーモン | dockerd(root) | Desktopが管理するVM内のdockerd | なし。各コンテナは実行したユーザーの子プロセス |
| rootless | 追加設定で可(執筆時点で機能制約あり) | VMの中で完結 | 既定でrootless |
| ライセンス(執筆時点) | Apache License 2.0 | 企業規模により有料サブスクリプション | Apache License 2.0 |
| Compose | docker composeプラグイン | 同梱 | podman compose、またはDocker互換ソケットでdocker composeを流用 |
| systemd連携 | --restartポリシー | 該当なし | Quadlet(.containerユニット)でsystemdが直接管理 |
| 向く場面 | 本番・検証サーバー、自宅サーバー | Windows/Macの開発PC、GUIで状態を見たい | rootless必須、RHEL系、systemd中心の運用 |
筆者の選び方は、Linuxサーバーと自宅サーバーはDocker Engine、RHEL系で運用ルールがsystemd中心のチームはPodman、Windows/Macの開発PCはライセンス条件を満たすならDocker Desktop、満たさないならWSL2+Docker EngineかPodman Desktopです。Podmanのコマンド体系はdockerとほぼ同じで、イメージもOCI形式で共通なので、後から乗り換えても手順書の書き直しは小さく済みます。
よくある質問
Dockerは何に使うの?
大きく3つです。1つ目はサーバーで複数のアプリを同居させること(PHP 8.1のWikiとPython 3.12のツールを同じ1台で、依存の衝突なしに動かす)。2つ目は開発・検証環境の再現で、docker runで作りdocker rmで捨てられます。3つ目はCIやKubernetesに載せるためのアプリの配布単位づくりです。Wiki.jsやNextcloudのように、配布の主形態がイメージになっている製品の導入にも使います。
AWSとDockerの違いは何ですか?
AWSはサーバー・ネットワーク・ストレージを貸すクラウド事業者で、Dockerはアプリを箱詰めして動かす仕組みです。層が違うので比較対象ではなく、組み合わせて使います。AWS上でDockerイメージを動かすサービスにはECSやEKS(Kubernetes)があり、自宅サーバーで作ったイメージをそのまま持ち込めます。逆に、AWSを使わず手元のLinuxサーバーだけでDockerを動かすこともできます。
Dockerは無料ですか?
Linux用のDocker EngineはApache License 2.0のオープンソースで無料です。Windows/macOS用のDocker Desktopは、執筆時点では個人・教育・小規模組織は無償、一定規模を超える企業は有料サブスクリプションが必要で、条件は公式サイトで確認してください。また、Docker Hubの無料枠にも取得回数などの制限があります。無償で揃えるなら、Linuxサーバーに直接Docker Engineを入れるか、Podmanを使います。
Dockerとコンテナの違いは何ですか?
コンテナは、Linuxカーネルのnamespaceとcgroupで隔離したプロセスという「仕組みそのもの」で、Dockerはそれを作って配布するための「ツール」の1つです。イメージ形式と実行方法はOCIとして標準化されているため、Dockerで作ったイメージはPodmanやcontainerd(Kubernetes)でも動きます。広めたのはDockerですが、Docker専用の技術ではありません。
まとめ
コンテナは別のOSではなく、namespaceで見えるものを分け、cgroupで使える量を絞り、overlayで重ねたイメージの上に薄い書き込み層を載せた、ホストの1プロセスです。だからVMより軽く、その代わりカーネルの境界はなく、rootの扱いとバックアップは自分で設計する必要があります。情シスの判断は、配布形態・同居させるアプリ数・作り直す頻度でコンテナかVMかを決め、更新とバックアップの当番を組めないならSaaSを選ぶ、の順です。次の一歩として、手元のLinux(またはWSL2)でdocker run --rm --memory 256m alpine:3 cat /proc/1/cgroupを実行し、この記事と同じ0::/が返ることを確かめてから、Composeで最初の3層構成を組んでください。
次に読む
- Docker Compose入門:自宅サーバーでWeb・DB・リバースプロキシをまとめて動かす:複数コンテナを1つのファイルで定義し、更新とバックアップまで運用する
- Dockerfileの書き方:マルチステージビルド・レイヤーキャッシュ・イメージ軽量化:この記事で見たレイヤーの足し算を、ビルド側で小さく抑える方法
- PortainerでDockerをWeb管理する:docker.sockの権限をどう扱うか、Web UIを入れる前に知っておく設計