1-3. イメージとレイヤー - Copy-on-Write の仕組み
所要時間: 35-45分 ゴール: イメージとコンテナの違い、レイヤー構造、なぜビルドキャッシュが効くかを理解する コミット内容: 操作ログを
~/log/docker_day03.logに保存。docker imagesの出力を眺めて構造を観察する
このレッスンのゴール
- イメージとコンテナの違いを「クラスとインスタンス」のアナロジーで言える
- レイヤー構造を「Dockerfile の各命令=1レイヤー」と認識している
- Copy-on-Write がビルドキャッシュとサイズ削減の両方に効く理由を語れる
-
:latestを本番で使ってはいけない理由を即答できる - alpine / slim / distroless の使い分け基準を持っている
なぜ学ぶか(実務悩みベース)
- 「ビルドが30分かかる」がレイヤー設計で5分になる
- 「イメージ 1GB あるんですけど」がレイヤー解剖で 80MB に縮む
- 本番で
:latest使っていて翌朝に静かに壊れる事故を避けられる - ECR / GHCR の課金(イメージ容量)を抑えられる
前章とのつながり
1-2 で docker run nginx を叩いた裏で、内部的に pull → create → start が走っていた。その pull で何をダウンロードしていたかを今回解剖する。「イメージ=tarballの集合」と知れば、run の挙動が完全に見通せる。
大前提: 「イメージ」と「コンテナ」は別物
前回 docker run nginx でコンテナが起動した。ここで何が起きていたか:
docker pull (内部) → nginx の「イメージ」をダウンロード
↓
docker create → イメージから「コンテナ」を作成(まだ動いてない)
↓
docker start → コンテナを起動(プロセス開始)
docker run はこれを一発でやる便利コマンド。区別すると:
| 用語 | 例えるなら |
|---|---|
| イメージ (image) | クラス(設計図、読み取り専用) |
| コンテナ (container) | インスタンス(実体、起動して動く) |
なぜこの区別が重要か
1つのイメージから 複数のコンテナを同時に作れる。
nginxイメージ1個から、web1, web2, web3 という3つのコンテナを起動する、みたいな運用は普通。また、イメージ自体は変更されない(読み取り専用)。コンテナで何か書き込んでも、それはコンテナ層に書き込まれていて、イメージは元のまま。
この回はイメージを 解剖 する。「ただのzipファイルでしょ?」と思っていると、本番でハマる。実態は 「レイヤーの積み重ね」 で、これがビルドキャッシュやサイズ最適化の鍵を握っている。
セッション①: イメージとレイヤー(25分)
0. 録画スタート
mkdir -p ~/log
script ~/log/docker_day03.log1. イメージ一覧と詳細
# ローカルにあるイメージ一覧
docker images
# 詳細を JSON で(パイプして less で読む)
docker image inspect nginx | less
# 履歴(このイメージがどんな手順で作られたか)
docker image history nginx
docker imagesの見方表示される列:
列 意味 REPOSITORY イメージ名( nginx,postgresなど)TAG バージョンタグ( latest,16,1.25-alpine)IMAGE ID 一意のID(中身のハッシュ) CREATED このイメージがビルドされた日時 SIZE ディスク使用量 SIZE が膨らみがち:
node:20だと 1GB 近い、postgres:16も 400MB 超。 後で alpine 系を選ぶ理由がここに繋がる。
2. レイヤーの構造を覗く
docker image history nginx:latestこんな感じの出力が出る:
IMAGE CREATED CREATED BY SIZE
abc123def 2 days ago CMD ["nginx" "-g" "daemon off;"] 0B
<missing> 2 days ago STOPSIGNAL SIGQUIT 0B
<missing> 2 days ago EXPOSE map[80/tcp:{}] 0B
<missing> 2 days ago ENTRYPOINT ["/docker-entrypoint.sh"] 0B
<missing> 2 days ago COPY 30-tune-worker-processes.sh ... 4.62kB
<missing> 2 days ago RUN /bin/sh -c set -x && groupadd --syst... 64.4MB
<missing> 2 days ago ENV NGINX_VERSION=1.25.3 0B
<missing> 2 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:1234... in / 80.4MB
イメージは「レイヤー」の積み重ね
Dockerイメージは 複数のレイヤー(差分の集まり) で構成される。各レイヤーは Dockerfile の1命令(FROM / RUN / COPY など)に対応する。
各レイヤーは:
- 読み取り専用
- ハッシュで識別(中身が同じなら ID も同じ)
- 複数のイメージで共有可能
上の
nginxの例だと: 「Debian のベースイメージ層」+「nginx をインストールした層」+「設定スクリプトをコピーした層」+「メタデータ(CMD など)」が積み重なっている。
<missing>は壊れているわけではない: 古い形式の表示で、「個別のIDは隠してある」というだけ。中身は存在する。
3. Copy-on-Write(CoW)で軽くなる仕組み
イメージ (読み取り専用、レイヤーの積み重ね)
┌────────────────────────────────┐
│ Layer N: CMD ["nginx"] │
│ Layer N-1: COPY ... │
│ Layer N-2: RUN apt install ... │
│ Layer 1: FROM debian (ベース) │
└────────────────────────────────┘
↑
│ 共有!
│
┌──────┴──────┐
│ │
┌────┴───┐ ┌────┴───┐
│ web1 │ │ web2 │
│ (書込層)│ │ (書込層)│ ← コンテナごとの書き込み専用層
└────────┘ └────────┘
Copy-on-Write の本質
「書き込まれた時だけコピーする」 という仕組み。
- コンテナ起動時: イメージのレイヤーは 共有(コピーしない)
- コンテナ内でファイルを変更: 変更されたファイルだけ「コンテナ層」にコピーして書き換える
- 削除した時もこの層に「削除マーク」がつくだけ。イメージは無傷
メリット
- コンテナ起動が速い: イメージのコピーが要らないから(数百ms で起動できる理由)
- ディスク効率が良い: 同じイメージから10個コンテナ作っても、追加で使うのは10個分の書込層だけ
- イメージは不変: 何度コンテナを作り直しても元に戻る
ユースケース
- 開発で何度もコンテナを作って捨てる → 毎回クリーンな状態から始まる
- 本番で複数のコンテナを同じイメージから起動 → メモリ・ディスクを節約
CoW の落とし穴
- コンテナを消すと書込層も消える: コンテナ内のデータは揮発する。永続化したいデータはボリュームへ(後の章)
- 書き込みが多いと遅くなる: ファイルごとのコピー処理が走るため。大量書き込みアプリ(DB等)はボリュームを使うのが正解
- イメージ層を変えるとキャッシュが切れる: 後述
4. ビルドキャッシュの仕組み(次回への布石)
# 同じイメージを2回 pull してみる
docker pull alpine:3.19
docker pull alpine:3.19 # 2回目は「Already exists」だらけで一瞬レイヤー共有によるキャッシュ
pull時: 既にローカルにあるレイヤーはスキップ。差分だけダウンロード。build時: 同じ命令の結果は再利用。同じ Dockerfile を再ビルドすると一瞬で終わる。これが Docker のビルドが「2回目から爆速になる」理由。詳しくは 次回の Dockerfile で扱う。
セッション②: レジストリと命名規則(20分)
5. Docker Hub と pull / push / tag
# 明示的に pull
docker pull nginx:1.25-alpine
# タグを付け替える(別名を作る)
docker tag nginx:1.25-alpine myname/nginx:test
# 確認
docker images | grep nginxレジストリ(registry)とは
コンテナイメージを保存・配布するサーバー。GitHubのコード版がDocker Hubのイメージ版、と例えると分かりやすい。
代表的なレジストリ:
レジストリ 提供 特徴 Docker Hub Docker社 最大の公開レジストリ。 docker pull nginxのデフォルト先GitHub Container Registry (ghcr.io) GitHub GitHub Actions と統合しやすい Amazon ECR AWS AWS環境で使うならこれ Google Container Registry / Artifact Registry GCP環境用 Quay Red Hat OSS 向け、セキュリティスキャンが強い プライベートレジストリ 自社運用 Harbor、Nexus などで自前構築 公開リポジトリと private リポジトリ: Docker Hub は公開なら無料、privateは制限あり。実務では会社が ECR や GHCR を使うケースが多い。
イメージの命名規則
[レジストリ/]ユーザー名/リポジトリ名:タグ例:
nginx→ Docker Hub の公式library/nginx:latest(省略形)nginx:1.25-alpine→ 公式の特定バージョンmyname/myapp:v1.0→ Docker Hub の myname ユーザーのリポジトリghcr.io/myorg/myapp:latest→ GitHub Container Registry123456789.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:v1.0→ AWS ECRデフォルトの省略
- レジストリ省略 → Docker Hub
- ユーザー名省略 →
library/(公式イメージのみ)- タグ省略 →
:latest
:latestタグの罠
:latestは時間とともに中身が変わる。今日 pull したnginx:latestと1ヶ月後のnginx:latestは別物- 本番では 必ずバージョンを固定 する:
nginx:1.25.3-alpineのように- 開発でも
latestを多用すると「自分のMacでは動くけど他人のMacでは動かない」が再発(コンテナ化で消したはずの問題が戻ってくる)
6. push の概要(実演は次回)
# Docker Hub にログイン
docker login
# タグを Docker Hub の自分のリポジトリ名に合わせる
docker tag myimage:v1 yourname/myimage:v1
# push
docker push yourname/myimage:v1詳細は 1-5. ビルドと配布 で実演する。今は流れだけ:
ローカル
↓ docker login (認証)
↓ docker tag (名前を合わせる)
↓ docker push (アップロード)
レジストリ
↓ docker pull (他のマシンで)
別のマシン
↓ docker run (起動)
7. イメージサイズが膨らむ理由
# 公式イメージのサイズ比較(数値は時期により多少変わる)
docker pull node:20
docker pull node:20-alpine
docker pull node:20-slim
docker images node例えば:
REPOSITORY TAG SIZE
node 20 1.1GB
node 20-slim 230MB
node 20-alpine 180MB
サイズの違いは「ベースイメージ」の違い
ベース サイズ 中身 使い所 フル(Debian/Ubuntu系) 数百MB〜1GB 開発ツール全部入り デバッグしやすい、開発用 -slim50-200MB Debian の最小構成 開発ツール無しで動かす -alpine5-50MB Alpine Linux ベース 本番、軽量化重視 distroless数十MB OSコマンドすら無い セキュリティ重視 scratch0MB 空 Go バイナリ等を直接置く時 alpine が小さい理由
- 標準C ライブラリが
glibcではなくmusl(軽量)- パッケージマネージャ
apkが小さい- 余計なものが入っていない
alpine の落とし穴
musllibc 起因の 互換性問題: 一部のネイティブモジュール(特に Node.js の native addon、Python の wheel)が glibc 前提で alpine だと動かないglibcがいる場合は-slimを選ぶ- 何でもかんでも alpine」は危険。動作確認してから採用
8. 「コンテナを書き換えてイメージにする」もできる
# nginx を起動して、適当にカスタマイズ
docker run -d --name customizing nginx
docker exec customizing sh -c "echo 'Hello custom' > /usr/share/nginx/html/index.html"
# このコンテナの状態をイメージ化(commit)
docker commit customizing my-nginx:v1
docker images | grep my-nginx
# 確認
docker run -d --name testing -p 8082:80 my-nginx:v1
curl http://localhost:8082
# 片付け
docker stop customizing testing
docker rm customizing testing
docker rmi my-nginx:v1
docker commitの使い所と注意動いているコンテナの状態をイメージ化するコマンド。
使い所
- 試行錯誤で作った環境を一旦保存
- 障害調査で「再現できる状態」を残しておく
アンチパターン
- 本番イメージを
commitで作るのは厳禁。再現性がない、手順が記録されない、レイヤーが汚れる- 本番イメージは Dockerfile からビルド する(次回)
commitを覚えるのは「Dockerfile こそが正道である」ということを理解するため。
9. お片付け
# 未使用のイメージ削除
docker image prune
# タグ無しの「dangling」イメージだけ消す(安全)
docker image prune
# 使われていないイメージを全部消す(要注意)
docker image prune -a
# 1個ずつ
docker rmi <イメージID>
docker image prune -aの威力「現在コンテナで使われていない」イメージを 全部 消す。Mac のディスクを開放したい時に便利だが、また pull し直しになる。
「未使用」の判定は「現在
docker ps -aに存在するコンテナが参照しているか」。つまり停止中のコンテナが参照していれば残るが、コンテナが無ければ消される。
練習課題
# 1. 軽量な alpine と、大きい ubuntu を pull してサイズを比較
docker pull alpine
docker pull ubuntu
docker images | grep -E "alpine|ubuntu"
# 2. それぞれ起動してファイルシステムを覗く
docker run --rm alpine ls /
docker run --rm ubuntu ls /
# (中身がほぼ同じであることを確認)
# 3. nginx の history を眺める
docker image history nginx --no-trunc | less
# (各レイヤーの命令を読んでみる)
# 4. 同じイメージから2つコンテナを起動
docker run -d --name w1 -p 8001:80 nginx
docker run -d --name w2 -p 8002:80 nginx
docker ps
# (イメージは1個でも、コンテナは複数作れる)
# 5. w1 の中で index.html を書き換える
docker exec w1 sh -c "echo '<h1>w1 です</h1>' > /usr/share/nginx/html/index.html"
# 6. w1 と w2 を curl で比較
curl http://localhost:8001/ # 書き換えた内容
curl http://localhost:8002/ # 元のまま
# (CoW の証拠: w1 への変更が w2 に波及しない)
# 7. 片付け
docker rm -f w1 w2締め: 振り返り(10分)
1. セッション録画を終了
exit2. 今日の発見
- 「イメージとコンテナは別物」の理解度(1-5):
- レイヤーって結局何だった?を自分の言葉で:
- Copy-on-Write を聞いたことある別の文脈(あれば):
- alpine と slim、自分の用途ならどっち?:
- 詰まったところ:
やらかし事例: イメージ運用の落とし穴
事例1:
:latestで「ある日突然動かない」
FROM node:latestでビルドしていたチームが、Node が v18 → v20 にメジャー更新された朝、CIが全部赤くなった。latest は時限爆弾。node:20.10.0-alpineのように固定する。
事例2: レイヤー順序を意識せずビルドが毎回フル
COPY . /app→RUN npm installの順だと、ソース1文字変更でnpm installまでキャッシュ無効。正解はCOPY package*.json ./→RUN npm install→COPY . /appの順。
事例3: alpine で「musl 由来の謎エラー」
alpine は glibc ではなく musl libc。Python の binary wheel が動かない・Node のネイティブモジュールがビルドエラー、などが起きる。「とりあえず alpine」を疑う癖 を持つ。
事例4:
docker commitで作ったイメージを本番投入手で
docker execして変更しdocker commitでイメージ化 → 何が入っているか不明な「ブラックボックス」が完成。必ず Dockerfile に手順を残す。
物語: なぜ Copy-on-Write は革命だったか
VM の世界では、新しいインスタンスを作るたびに OSイメージを丸ごとコピー していた。10GB のOSイメージから10台のVMを立てると100GB。当然遅い。
UnionFS(オーバーレイFS)と Copy-on-Write の発想は逆だった。「変更がない限り、共有していい」。10台のコンテナが同じ nginx イメージから起動しても、ディスク上に nginx の本体は1個しかない。各コンテナの「書込層」だけが個別に積まれる。
これが効くのは:
- 複数コンテナ同時起動: メモリ・ディスク・ネットワーク全てが省エネ
- イメージ pull の差分転送: 既にあるレイヤーは pull しなおさない
- ビルドキャッシュ: 同じ命令+同じ入力なら、結果のレイヤーを再利用
「ちょっとした変更で全部やり直し」だった世界が、「変わった部分だけ」になった。これが Docker の経済的価値の本体。
対比表: イメージサイズと選択基準
| ベース | サイズ目安 | 特徴 | 向き |
|---|---|---|---|
ubuntu:24.04 | ~80MB | glibc、apt、なんでも入る | デバッグ用、レガシー移植 |
debian:12-slim | ~30MB | glibc、軽い、apt | バランス型、本番でも安心 |
alpine:3.19 | ~7MB | musl libc、apk | 最小、ただし glibc 依存ハマり注意 |
gcr.io/distroless/static | ~2MB | シェルすら無い、Go バイナリ向け | 本番最小、攻撃面ゼロ |
scratch | 0MB | 何も無い | Go の static バイナリ専用 |
自己評価チェックリスト
知識レベル
- イメージとコンテナの違いを1分で説明できる
- Copy-on-Write が「ビルドキャッシュ」「同時起動」「pull差分」の3つに効くと言える
-
:latestを本番禁止する理由を即答できる - alpine の落とし穴(musl)を1つ言える
実行レベル
-
docker imagesでローカルのイメージを確認した -
docker image historyでレイヤー構造を見た - Copy-on-Write を「同じイメージから複数コンテナを作る練習」で体感した
- イメージの命名規則
registry/repo:tagが分かった
メタ認知
- 自分のチーム/個人プロジェクトで
latestを使っている箇所を洗い出した - 次に作る Dockerfile で意識すべきポイントを3つ書き出した
次のレッスンとのつながり
ここで掴んだ「レイヤー=Dockerfile の命令1個分」という見方が、次の 1-4 で どの命令の順序で書くとキャッシュが効くか の設計判断に直結する。
詰まった時のチートシート
| やりたいこと | コマンド |
|---|---|
| イメージ一覧 | docker images |
| イメージの詳細 | docker image inspect <イメージ> |
| レイヤー履歴 | docker image history <イメージ> |
| イメージ取得 | docker pull <イメージ>:<タグ> |
| タグ付け | docker tag <元> <新タグ> |
| イメージ削除 | docker rmi <イメージ> |
| 未使用イメージ一掃 | docker image prune -a |
| コンテナをイメージ化 | docker commit <コンテナ> <新イメージ> |
| ログイン | docker login |
| アップロード | docker push <イメージ> |
「実務OK」基準
- 「イメージとコンテナは違う」と即答できる
- レイヤー構造を「Dockerfile の各命令 = 1レイヤー」と認識している
:latestを本番で使わないルールを腹落ちしている- alpine と slim を選ぶ基準を持っている
ここまで来たら、自分でイメージを「作る」段階に入れる。次回は Dockerfile で、自分のアプリをコンテナ化する手順を覚える。
次のレッスン
1-4. Dockerfile - イメージを自分で作る へ。
各命令(FROM, RUN, COPY, CMD, ENTRYPOINT など)の意味と、混乱しがちな CMD vs ENTRYPOINT、COPY vs ADD、ARG vs ENV をしっかり区別する。