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-2docker run nginx を叩いた裏で、内部的に pullcreatestart が走っていた。その 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.log

1. イメージ一覧と詳細

# ローカルにあるイメージ一覧
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 HubDocker社最大の公開レジストリ。docker pull nginx のデフォルト先
GitHub Container Registry (ghcr.io)GitHubGitHub Actions と統合しやすい
Amazon ECRAWSAWS環境で使うならこれ
Google Container Registry / Artifact RegistryGoogleGCP環境用
QuayRed HatOSS 向け、セキュリティスキャンが強い
プライベートレジストリ自社運用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 Registry
  • 123456789.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-200MBDebian の最小構成開発ツール無しで動かす
-alpine5-50MBAlpine Linux ベース本番、軽量化重視
distroless数十MBOSコマンドすら無いセキュリティ重視
scratch0MBGo バイナリ等を直接置く時

alpine が小さい理由

  • 標準C ライブラリが glibc ではなく musl(軽量)
  • パッケージマネージャ apk が小さい
  • 余計なものが入っていない

alpine の落とし穴

  • musl libc 起因の 互換性問題: 一部のネイティブモジュール(特に 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. セッション録画を終了

exit

2. 今日の発見

- 「イメージとコンテナは別物」の理解度(1-5):
- レイヤーって結局何だった?を自分の言葉で:
- Copy-on-Write を聞いたことある別の文脈(あれば):
- alpine と slim、自分の用途ならどっち?:
- 詰まったところ:

やらかし事例: イメージ運用の落とし穴

事例1: :latest で「ある日突然動かない」

FROM node:latest でビルドしていたチームが、Node が v18 → v20 にメジャー更新された朝、CIが全部赤くなった。latest は時限爆弾node:20.10.0-alpine のように固定する。

事例2: レイヤー順序を意識せずビルドが毎回フル

COPY . /appRUN npm install の順だと、ソース1文字変更で npm install までキャッシュ無効。正解は COPY package*.json ./RUN npm installCOPY . /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~80MBglibc、apt、なんでも入るデバッグ用、レガシー移植
debian:12-slim~30MBglibc、軽い、aptバランス型、本番でも安心
alpine:3.19~7MBmusl libc、apk最小、ただし glibc 依存ハマり注意
gcr.io/distroless/static~2MBシェルすら無い、Go バイナリ向け本番最小、攻撃面ゼロ
scratch0MB何も無い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 をしっかり区別する。