Dockerマルチステージビルド完全ガイド|イメージサイズを1/10に削減する実践テクニック
「Goの静的バイナリなのに、なぜイメージが1GBもあるの?」「Node.jsアプリのビルドに必要なgccやpython3が本番イメージに残っている」——こうした問題の解決策が マルチステージビルドです。
マルチステージビルドを使うと、ビルド用と実行用のステージを分離でき、本番イメージに必要なものだけを残せます。Go なら 900MB → 15MB、Node.js なら 1.2GB → 150MB といった劇的な削減が可能です。この記事では仕組みから実践パターンまで徹底解説します。
目次
- なぜマルチステージビルドが必要か
- 基本構文:FROM … AS … と COPY –from
- 実践① Goアプリ(900MB→15MB)
- 実践② Node.jsアプリ(1.2GB→180MB)
- 実践③ Pythonアプリ
- ステージの命名と参照テクニック
- ビルドターゲット(–target)で途中ステージを取り出す
- 外部イメージからの
COPY --from - マルチステージの落とし穴
- まとめ
1. なぜマルチステージビルドが必要か
アプリの ビルド時と 実行時で必要なツールは違います。
| フェーズ | 必要なもの | 例 |
|---|---|---|
| ビルド時 | コンパイラ・ビルドツール・開発ライブラリ・devDependencies | gcc、make、npm、webpack、TypeScript、テストフレームワーク |
| 実行時 | 最終成果物とランタイムのみ | バイナリ、main.js、python app.py、nginx |
シングルステージで書くと、この 両方が最終イメージに混ざります。結果、イメージは肥大化し、セキュリティリスク(攻撃者が利用できるツールが残る)も増えます。
マルチステージの発想
「ビルド作業は別の大きなイメージで行い、完成品だけをスリムなイメージにコピーする」——これがマルチステージビルドの本質です。
2. 基本構文:FROM … AS … と COPY –from
マルチステージビルドは、1つのDockerfileに複数の FROM を書くだけで実現できます。
# Stage 1: ビルダー(名前を "builder" と付ける)
FROM golang:1.22-alpine AS builder
WORKDIR /src
COPY . .
RUN go build -o /bin/app
# Stage 2: 本番(最終イメージになる)
FROM alpine:3.19
COPY --from=builder /bin/app /usr/local/bin/app
CMD ["app"]
ポイントは2つ:
FROM <image> AS <name>:ステージに名前を付けるCOPY --from=<name> <src> <dst>:別ステージからファイルをコピー
docker build が最終的に出力するイメージは、Dockerfileの一番下の FROM のステージだけです。途中のステージはビルドの過程で使われるだけで、最終成果物には含まれません。
3. 実践① Goアプリ(900MB→15MB)
Goは静的バイナリにコンパイルできる言語のため、マルチステージの効果が最も劇的です。
シングルステージ版(悪い例)
FROM golang:1.22
WORKDIR /app
COPY . .
RUN go build -o /app/server
CMD ["/app/server"]
# サイズ: 約 900MB(Go toolchain 全部含む)
マルチステージ版(良い例)
# syntax=docker/dockerfile:1.4
# ===== ビルドステージ =====
FROM golang:1.22-alpine AS builder
WORKDIR /src
# 依存ダウンロード(レイヤーキャッシュ活用)
COPY go.mod go.sum ./
RUN go mod download
# ソースコピー&静的ビルド
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /bin/server
# ===== 実行ステージ =====
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /bin/server /usr/local/bin/server
EXPOSE 8080
CMD ["server"]
# サイズ: 約 15MB(60分の1)
CGO_ENABLED=0 の意味Go のビルド時にCGO(Cライブラリ呼び出し)を無効化すると、完全な静的バイナリが生成されます。これにより、alpine(musl libc)でも glibc でも同じバイナリが動きます。さらに軽くしたい場合は
scratch(空の最小イメージ)も使えます。
4. 実践② Node.jsアプリ(1.2GB→180MB)
Node.js は実行時にもランタイムが必要ですが、devDependencies(TypeScript・webpack・テストツール)を本番から除外できます。
# ===== ビルドステージ =====
FROM node:20-alpine AS builder
WORKDIR /app
# devDependencies も含めてインストール(ビルドに必要)
COPY package.json package-lock.json ./
RUN npm ci
# ソースコピー&ビルド(TypeScript → JS 変換など)
COPY . .
RUN npm run build
# ===== 本番依存ステージ =====
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# ===== 実行ステージ =====
FROM node:20-alpine
WORKDIR /app
# ビルド成果物(dist)と本番依存のみコピー
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
COPY package.json ./
EXPOSE 3000
CMD ["node", "dist/server.js"]
# サイズ: 約 180MB(1.2GB → 1/7)
このパターンでは 3つのステージを使っています:
builder:TypeScript/webpackのビルド(devDep使用)deps:本番用依存のみを別ステージでインストール- 最終:
distとnode_modules(本番版)だけ持ってくる
同じ
node_modules でも、npm ci と npm ci --omit=dev では中身が違います。ビルドには前者が必要、実行には後者で十分。分けないと本番イメージに不要なdevDependenciesが混入します。
5. 実践③ Pythonアプリ
Pythonはコンパイル不要ですが、gccを要求するパッケージ(numpy、psycopg2等)をビルドする場面で効果があります。
# ===== ビルドステージ =====
FROM python:3.12-slim AS builder
WORKDIR /app
# gcc などのビルドツールをインストール
RUN apt-get update && apt-get install -y --no-install-recommends \
gcc \
libpq-dev \
&& rm -rf /var/lib/apt/lists/*
COPY requirements.txt .
# wheel を作って /wheels に出力
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt
# ===== 実行ステージ =====
FROM python:3.12-slim
WORKDIR /app
# 実行に必要な最小限のライブラリ
RUN apt-get update && apt-get install -y --no-install-recommends \
libpq5 \
&& rm -rf /var/lib/apt/lists/*
# ビルド済み wheel を使ってインストール(gcc 不要)
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/*
COPY . .
CMD ["python", "app.py"]
ビルドステージで pip wheel を使い、コンパイル済みの .whl ファイルを生成 → 実行ステージでは gcc 不要でインストール、という戦略です。最終イメージから gcc・libpq-dev(開発用ヘッダ)が除外されます。
6. ステージの命名と参照テクニック
ステージは何個でも書ける
FROM node:20 AS frontend-builder
# フロントエンド(React)ビルド
RUN npm run build
FROM golang:1.22 AS backend-builder
# バックエンド(Go)ビルド
RUN go build -o /api
FROM alpine:3.19 AS runtime
COPY --from=frontend-builder /app/dist /www
COPY --from=backend-builder /api /usr/local/bin/
# フロント・バックを1つのイメージにまとめる
前のステージを相対参照できる
ステージ名を付け忘れた場合、番号(0, 1, 2…)で参照できます。
FROM golang:1.22
RUN go build -o /app
FROM alpine
COPY --from=0 /app /app # ステージ0を参照
# ただし名前を付けた方が可読性が高い
7. ビルドターゲット(–target)で途中ステージを取り出す
docker build --target <stage> で、任意のステージまでビルドして終了できます。開発環境・テスト環境・本番環境を1つのDockerfileで管理できる強力な機能です。
# ===== 共通ベース =====
FROM node:20-alpine AS base
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# ===== 開発(ホットリロード対応)=====
FROM base AS development
COPY . .
CMD ["npm", "run", "dev"]
# ===== テスト(テスト実行して失敗したらビルド停止)=====
FROM base AS test
COPY . .
RUN npm run lint && npm run test
# ===== 本番(最適化ビルド)=====
FROM base AS production
COPY . .
RUN npm run build && npm prune --omit=dev
CMD ["node", "dist/server.js"]
# 開発環境でビルド
docker build --target development -t myapp:dev .
# テストだけ実行(失敗したら exit 1)
docker build --target test -t myapp:test .
# 本番ビルド(省略時は最後のステージ)
docker build --target production -t myapp:prod .
# または
docker build -t myapp:prod .
--target test で CI でテストを走らせ、成功したら --target production で本番イメージをビルド、という流れが定石です。詳しくは第12章「CI/CDパイプラインへの組み込み」で扱います。
8. 外部イメージからの COPY --from
COPY --from の参照先は、Dockerfile 内のステージだけでなく 既存のイメージ名も指定できます。
FROM alpine:3.19
# 公式 nginx イメージからバイナリを拝借
COPY --from=nginx:1.25-alpine /usr/sbin/nginx /usr/sbin/nginx
COPY --from=nginx:1.25-alpine /etc/nginx /etc/nginx
# Go のバイナリを外部ビルド済みイメージから持ってくる例
COPY --from=registry.example.com/go-builder:v1.0 /bin/app /usr/local/bin/
これは ビルドキャッシュを別リポジトリで管理したい時や、公式イメージのビルド済みバイナリを使い回したい時に便利です。
9. マルチステージの落とし穴
| 落とし穴 | 原因 | 対策 |
|---|---|---|
| COPY –from で 動的リンクのバイナリが動かない | ビルド環境と実行環境の glibc/musl が違う | Goなら CGO_ENABLED=0、Cなら静的ビルド |
| alpine と debian を混在 | libc が違う(musl vs glibc) | ベースを揃える or 静的バイナリにする |
| 環境変数が引き継がれない | ENV は ステージごとに再定義必要 |
必要な環境変数は実行ステージでも定義 |
| ユーザー・権限が引き継がれない | USER もステージごと |
実行ステージで USER を改めて指定 |
ARG が見えない |
ARG は定義したステージ内でのみ有効 |
各ステージの先頭で再宣言 |
| .dockerignore を使っていない | ビルドコンテキストが大きいと遅い | 3-3 .dockerignoreで対処 |
マルチステージでは、ステージごとに独立してキャッシュが評価されます。あるステージの
COPY が壊れても、他のステージには影響しません。これを利用して並列性の高いDockerfileを書けば、BuildKitはステージを並列ビルドできます。
10. まとめ
マルチステージビルドは、「ビルドと実行を分離する」というシンプルな発想ですが、その効果はイメージサイズ・セキュリティ・CI効率すべてに及ぶ、本番運用の必須テクニックです。
| 観点 | シングルステージ | マルチステージ |
|---|---|---|
| 最終サイズ(Go) | 約 900MB | 約 15MB |
| 最終サイズ(Node.js) | 約 1.2GB | 約 180MB |
| ビルドツールの残存 | あり(セキュリティリスク) | なし |
| 開発/本番の切り替え | Dockerfile を別に書く必要あり | –target で切替 |
| 並列ビルド | 不可 | BuildKitが自動並列化 |
| Dockerfile の行数 | 少ない | やや増える |
3-3「.dockerignore の活用」では、ビルドコンテキストを絞ってビルド時間をさらに短縮するテクニックを解説します。マルチステージと組み合わせることで、本番Dockerfileの完成度が一段上がります。
参考リンク
- Multi-stage builds(Docker公式) — マルチステージビルドの公式仕様と使用例。
- Dockerfile best practices — マルチステージを含むDockerfile全般のベストプラクティス。
- scratch イメージ — Goの静的バイナリを載せる最小イメージ(0バイト)。



コメント