1-5. ビルドと配布 - マルチステージ・push
所要時間: 40-50分(がっつりなら2セッション分) ゴール:
docker buildの挙動を理解し、マルチステージで本番イメージを小さくし、レジストリに push できる コミット内容: マルチステージ版 Dockerfile を~/learn/docker/day05/に保存
このレッスンのゴール
- ビルドコンテキストの正体(CLI → デーモンへの転送)を語れる
- マルチステージビルドで本番イメージを最小化できる
- arm64 / amd64 のクロスプラットフォーム対応で
--platformを使える - レジストリへの login → push → pull の流れを止まらず一巡できる
- BuildKit と従来ビルダーの違いを最低限把握している
なぜ学ぶか(実務悩みベース)
- 「ビルドが30分かかる」「イメージが2GBある」を解決したい
- Apple Silicon Mac で作ったイメージが本番(x86_64)で動かない問題
- 同僚にイメージを渡す手段が「Slackにtar.gz」になっていて非効率
- GHCR / ECR / Docker Hub の使い分け、認証エラーの初動
前章とのつながり
1-4 で Dockerfile が書けるようになった。今回は 書いた Dockerfile をどうビルドして、どう運ぶか の運用編。マルチステージビルドは「ビルド時の重い依存を本番イメージに含めない」設計判断で、レイヤー知識(1-3)と Dockerfile 命令(1-4)の合わせ技。
大前提: 「ビルドして動かす」だけで終わらない
前回は Dockerfile の書き方をやった。今日は ビルドの挙動を深く理解して、配布まで やる。
実務で次の困りごとに当たる:
- ビルドが遅い: 不要なファイルが大量にデーモンに送られている → ビルドコンテキストの理解が必要
- イメージが巨大: ビルドツール(コンパイラ、開発依存)まで入ってる → マルチステージビルドで解決
- 他のマシンで動かしたい: 同僚にイメージを渡す、本番にデプロイ → push & pull
これらは1個の Dockerfile を書くだけでは見えてこない、ビルドと配布のワークフロー の話。
セッション①: ビルドの仕組みとビルドコンテキスト(25分)
0. 作業準備
mkdir -p ~/log ~/learn/docker/day05
cd ~/learn/docker/day05
script ~/log/docker_day05.log1. docker build の基本
docker build -t myapp:v1 .最後の . は何かを掘り下げる。
ビルドコンテキストの本質
docker buildの最後の引数(.や./やpath/to/dir)は 「ビルドコンテキスト」のパス を指す。ビルドコンテキスト = そのディレクトリ以下のファイルすべて が Docker デーモンに送られる範囲。
$ docker build -t myapp:v1 . ↓ Docker CLI [tar で固める] . 配下を全部 ↓ ネットワーク経由(Mac の場合は VM 経由) Docker デーモン ↓ Dockerfile を読んで処理 イメージ完成重要な勘違い:
COPYで参照できるのは ビルドコンテキスト内のファイルだけ。COPY ../some_file.txtのように 外を参照することはできない。これは「コンテキスト = デーモンに送られた範囲」というルールから来る制限。
ビルドコンテキストの落とし穴
.をプロジェクトルートにすると、全部送られる:node_modules/(数GB)や.git/(履歴全部)も含まれる- 結果として:
- ビルドが遅い: 送信時間で数十秒~数分
- デーモン側でディスク逼迫
COPY . .で全部入ってしまう: イメージサイズ爆増解決策: 必ず
.dockerignoreを書く。前回のレッスン参照。
2. ビルドコンテキストを観察する
# 巨大なファイルを作って試す
dd if=/dev/zero of=bigfile.bin bs=1M count=100 # 100MBのダミー
cat > Dockerfile <<'EOF'
FROM alpine
COPY hello.txt /
EOF
echo "hi" > hello.txt
# ビルド(最初の "Sending build context" を見る)
docker build -t test:1 .
# Sending build context to Docker daemon 100MB のような表示が出る
# .dockerignore で除外
echo "bigfile.bin" > .dockerignore
# 再ビルド(送信量が激減)
docker build -t test:2 .
# 後片付け
rm bigfile.bin
docker rmi test:1 test:2
.dockerignoreの威力同じビルドでもコンテキストサイズが 100MB → 数KB に。本番プロジェクトでは
node_modules/や.git/を除外するだけで体感速度がガラッと変わる。
3. ビルドキャッシュをコントロールする
# 標準ビルド
docker build -t cachetest:v1 .
# 同じものを再ビルド → 全部キャッシュ使用、一瞬で終わる
docker build -t cachetest:v1 .
# キャッシュ無効化
docker build --no-cache -t cachetest:v2 .
# 特定の命令から再実行したい → ファイル変更で対応
# (キャッシュは「上から見て初めて差分が出たレイヤー」以降が再実行される)キャッシュが切れる条件
以下のいずれかが起きるとそのレイヤーから再実行:
- 命令の文字列が変わった:
RUN apt install curl→RUN apt install curl wget- COPY するファイルの内容が変わった:
COPY app.js .で app.js のハッシュが変化- ARG / 環境変数が変わった: 値が変わると後続が再実行
キャッシュ最適化のコツ:
- 変わりにくいもの(依存定義)を先に COPY
- 変わりやすいもの(ソースコード)を後に COPY
前回の Node 例で
package.jsonを先に COPY していたのはこれ。
--no-cacheの使い所
- 「キャッシュが壊れている気がする」時のリセット
- CI で「毎回クリーンなビルド」したい時
RUN apt updateで古いパッケージリストを掴んだ疑いがある時普段はキャッシュを活用する方が圧倒的に速い。
セッション②: マルチステージビルド(25分)
4. なぜマルチステージが必要か
問題: コンパイル言語(Go、Rust、Java、TypeScript)の本番イメージは、ビルドツールが入ったままだと巨大 になる。
例: Go アプリ
- ビルド時: Go コンパイラ(800MB)+ 依存ライブラリ
- 実行時: コンパイル済みバイナリ(10MB)だけで動く
→ 実行に必要のないものを 本番イメージから消したい
マルチステージビルドの本質
1つの Dockerfile に 複数の
FROMを書き、前のステージから成果物だけを取り出す 仕組み。┌─────────────────────┐ ┌─────────────────────┐ │ Stage 1: builder │ │ Stage 2: runtime │ │ (ビルド環境) │ │ (本番イメージ) │ │ │ │ │ │ - Go compiler │ │ - 軽量ベース │ │ - 依存ライブラリ │ → │ - バイナリだけ │ │ - ソースコード │ │ │ │ - 生成: app バイナリ│ │ CMD ["./app"] │ └─────────────────────┘ └─────────────────────┘ (捨てる) (こっちが配布される)
5. Go アプリの実例(マルチステージなし vs あり)
# main.go を準備
cat > main.go <<'EOF'
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello from Go in container")
})
http.ListenAndServe(":8080", nil)
}
EOF
# go.mod も必要
cat > go.mod <<'EOF'
module hello
go 1.22
EOFBefore: ナイーブな Dockerfile
# Dockerfile.naive
FROM golang:1.22-alpine
WORKDIR /app
COPY . .
RUN go build -o app main.go
CMD ["./app"]docker build -f Dockerfile.naive -t hello-go:naive .
docker images hello-go
# サイズ: 約 350MB(golang イメージ全部入り)After: マルチステージ
# Dockerfile(マルチステージ)
# ===== ビルドステージ =====
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY go.mod ./
# 依存があれば go.sum も。今回はライブラリなしなのでスキップ
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app main.go
# ===== ランタイムステージ =====
FROM alpine:3.19
WORKDIR /app
# builder ステージから成果物だけコピー
COPY --from=builder /out/app /app/app
EXPOSE 8080
CMD ["/app/app"]docker build -t hello-go:multi .
docker images hello-go
# サイズ: 約 15MB(alpine + バイナリだけ)マルチステージのインパクト
同じアプリで 350MB → 15MB。実に 20倍以上の差。
本番では:
- デプロイが速くなる: イメージのダウンロード時間が短縮
- セキュリティ向上: 不要なコンパイラ・ツールが入っていないので攻撃面が減る
- コスト削減: レジストリの保管容量、転送量
マルチステージの命令解説
FROM golang:1.22-alpine AS builderステージに名前
builderを付ける。ASで命名。COPY --from=builder /out/app /app/app別ステージ(
builder)から/out/appをコピー。これで最終イメージにバイナリだけが残る。
6. もっと小さく: scratch / distroless
# 究極の軽量化: scratch(中身ゼロ)
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /out/app main.go
FROM scratch
COPY --from=builder /out/app /app
EXPOSE 8080
CMD ["/app"]docker build -t hello-go:scratch .
docker images hello-go
# サイズ: 約 6-7MB(バイナリだけ!)
scratchとdistroless
scratch: 何も入っていない空のイメージ。Goのような静的バイナリ言語と相性最高distroless: Google が提供する「OSコマンドが入っていない」イメージ。シェルすら無いので攻撃が困難。Java/Python など glibc が必要な言語向け落とし穴:
scratchベースだとdocker exec -it bashが できない(bash が入っていない)。デバッグは困難。 → 開発用と本番用で Dockerfile を分けるか、distroless の debug 版を使うのが定石。
7. アーキテクチャに注意(M1/M2 Mac の罠)
# Apple Silicon Mac でビルドすると arm64 のイメージになる
docker build -t hello-go:multi .
docker image inspect hello-go:multi | grep Architecture
# "Architecture": "arm64"
# 本番が x86_64 (amd64) Linux なら、これではダメ
# クロスビルド: プラットフォームを明示
docker build --platform linux/amd64 -t hello-go:multi-amd64 .Apple Silicon と本番 Linux のアーキ違い
- Apple Silicon Mac:
arm64(aarch64)- 多くのクラウドの汎用インスタンス:
x86_64(amd64)- AWS Graviton 等は arm64 で安いが、まだ少数派
対策
--platform linux/amd64でクロスビルド- CI(GitHub Actions等)の Linux amd64 でビルドする運用
docker buildxでマルチアーキビルド(linux/amd64,linux/arm64両対応イメージを1コマンドで)
セッション③: レジストリへの push(15-20分)
8. Docker Hub に push する流れ
# 1. Docker Hub でアカウント作成(事前に)
# https://hub.docker.com/
# 2. ログイン
docker login
# Username, Password を入力
# 3. イメージにタグを付ける(命名規則: ユーザー名/リポ名:タグ)
docker tag hello-go:multi yourname/hello-go:v1
# 4. push
docker push yourname/hello-go:v1push の仕組み
Docker は レイヤー単位で push する。既にレジストリにあるレイヤー(ベースイメージなど)はスキップされて、自分が追加したレイヤーだけアップロードされる。
結果: 本体は数十MBあっても、push されるのは差分の数MBで済むことが多い。
9. push したイメージを別の場所で pull
# 別マシン or 自分のローカルから(一度ローカルを消す)
docker rmi yourname/hello-go:v1
# pull
docker pull yourname/hello-go:v1
# 動かす
docker run -d -p 8080:8080 --name go-app yourname/hello-go:v1
curl http://localhost:8080/これで「配布完了」
イメージを push さえすれば、世界中の Docker が動くマシンで
pull → runするだけで同じアプリが動く。「環境差異」問題が解消される瞬間。
10. 配布のシナリオ
実務でのイメージ配布パターン
シナリオ①: 同僚にローカルで試させる
- Docker Hub の private リポジトリに push → 共有
- もしくは
docker save image > image.tarでファイル化、docker load < image.tarで取り込みシナリオ②: 本番サーバーにデプロイ
- ローカル or CI でイメージビルド
- AWS ECR / GHCR / GCR にタグ付けして push
- 本番サーバー or Kubernetes が pull して run
シナリオ③: CI/CD で自動化
- GitHub Actions: コードを push → 自動ビルド → ECR に push → ECS/EKS デプロイ
- 「Dockerfile を書いたら、後は自動」が現代の理想形
11. プライベートレジストリ(ECR の例)
# AWS CLI でログイン情報取得 → docker login
aws ecr get-login-password --region ap-northeast-1 | \
docker login --username AWS \
--password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com
# タグ付け
docker tag hello-go:multi \
123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/hello-go:v1
# push
docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/hello-go:v1ECR を選ぶ理由
- AWS リソース(ECS、EKS、Lambda)と統合されている
- IAM で細かい権限制御
- private がデフォルト(社内利用向け)
- 同一リージョンの pull は転送料金が安い
練習課題
cd ~/learn/docker/day05
# 1. 既に作った Go アプリのマルチステージ Dockerfile をベースに、以下を試す:
# - scratch ベースで作り直す
# - サイズを docker images で比較
# 2. --no-cache でビルドしてみる
docker build --no-cache -t hello-go:nocache .
# 3. --platform linux/amd64 でクロスビルド
docker build --platform linux/amd64 -t hello-go:amd64 .
docker image inspect hello-go:amd64 | grep -i arch
# 4. Docker Hub にアカウントがあれば push を試す(無ければスキップ)
# docker tag hello-go:multi yourname/hello-go:v1
# docker login
# docker push yourname/hello-go:v1
# 5. 自作イメージを起動してみる
docker run -d -p 8080:8080 --name myapp hello-go:multi
curl http://localhost:8080/
# 6. お片付け
docker rm -f myapp
docker image prune締め: 振り返り(10分)
1. セッション録画を終了
exit2. 今日の発見
- マルチステージで何 MB → 何 MB に減った?:
- ビルドコンテキストの理解、腹落ちした?:
- --platform を意識する場面、思い当たる?:
- 本番デプロイのフロー、自分の言葉で書いてみる:
- 詰まったところ:
やらかし事例: ビルドと配布の地雷
事例1: 巨大ビルドコンテキスト
.gitやnode_modulesを.dockerignoreで除外せず、docker buildの最初に「Sending build context 2.3GB」。Mac だと VM 経由でさらに遅い。
事例2: マルチステージ忘れで「ビルドツール混入」
Go の本番イメージに
golang:1.22のままアプリを乗せ、コンパイラ + 開発依存込みで 1GB 超え。本番に gcc が居る = 攻撃面。
事例3: Apple Silicon で作って x86 本番にデプロイ
Mac (arm64) でビルドしたイメージを EC2 (amd64) で起動 →
exec format error。--platform linux/amd64を CI で明示するか、buildx でマルチアーキビルド。
事例4: ECR の認証トークン期限切れ
docker loginのトークンは12時間で切れる。CI 用の credential helper を使うか、ジョブの先頭で必ずaws ecr get-login-passwordを叩く。
事例5: タグ無し push で全部
latestCI から
docker push myappだけだと:latestで上書き。本番が突然「謎の最新」を pull する事故。:git-shaや:v1.2.3で固定。
対比表: ビルドと配布の手段
| 手段 | 場面 | メリット | 注意点 |
|---|---|---|---|
docker save / load | エアギャップ環境、ちょい共有 | レジストリ不要 | 容量大、差分転送なし |
| Docker Hub (public) | OSS 配布 | 無料、知名度高 | rate limit、private は有料 |
| GHCR | GitHub と統合した CI/CD | private 無料、GH OIDC 連携 | GitHub 依存 |
| ECR | AWS ワークロード | IAM 連携、同一リージョン安い | 認証トークン12h |
| GCR / Artifact Registry | GCP ワークロード | Cloud Run / GKE 統合 | GCP 依存 |
| 自前レジストリ | 完全コントロール | プライバシー最強 | 運用負担あり |
自己評価チェックリスト
知識レベル
- ビルドコンテキストとは何か、なぜ大きいと遅いかを説明できる
- マルチステージビルドの動機と仕組みを語れる
-
--platformがいつ必要かを判断できる - レジストリの認証 / タグ運用の地雷を3つ言える
実行レベル
-
docker buildのビルドコンテキストの仕組みを説明できる -
.dockerignoreの効果を体感した - マルチステージビルドで Go アプリを 10MB 台にできた
-
--platform linux/amd64でクロスビルドした - (任意)Docker Hub に push できた
- キャッシュが切れる条件を理解した
メタ認知
- 自分の主戦場(言語・クラウド)で使うレジストリを決めた
- CI/CD で意識する3つのポイントを書き出した
詰まった時のチートシート
| やりたいこと | コマンド |
|---|---|
| ビルド | docker build -t name:tag . |
| Dockerfile ファイル名指定 | docker build -f path/to/Dockerfile -t name . |
| キャッシュ無効化 | docker build --no-cache ... |
| アーキ指定 | docker build --platform linux/amd64 ... |
| ビルド引数 | docker build --build-arg KEY=VAL ... |
| イメージにタグ付け | docker tag <元> <新タグ> |
| ログイン | docker login |
| アップロード | docker push <イメージ> |
| ダウンロード | docker pull <イメージ> |
| ファイル化 | docker save <イメージ> > image.tar |
| ファイルから取込 | docker load < image.tar |
「実務OK」基準
- マルチステージビルドで本番イメージを最小化できる
- ビルドコンテキストと
.dockerignoreを必ず意識する - アーキの違い(arm64 vs amd64)を意識して
--platformを使える - イメージを push して別マシンで pull できる流れを知っている
次のレッスン
コンテナはエフェメラル(揮発的)なので、データを残すには別の仕組みが必要。bind mount / named volume / tmpfs の使い分け、パーミッション地獄の対処を学ぶ。