ReDevLab
DevOps 15分で読める

Dockerマルチステージビルドでイメージサイズを96%削減する方法

Dockerマルチステージビルドの基本構文から言語別実装例、ベースイメージ選択、BuildKit活用、頻出エラー対処まで、イメージサイズ最適化の全手順を解説します。

編集部
Dockerマルチステージビルドでイメージサイズを96%削減する方法

Dockerマルチステージビルドを活用すれば、イメージサイズを最大96%削減できます。肥大化の原因は、ビルド専用のコンパイラやSDKが本番イメージに残ることです。

実際の削減事例を見てみましょう。Goアプリでは729MBから26.2MBへ。PythonのFlaskアプリでは1.45GBから636MBへ縮小しています。

この記事では以下の内容を手順つきで解説します。マルチステージビルドの基本構文、ベースイメージの選定、BuildKitによる最適化、頻出エラーの対処法です。

マルチステージビルドの仕組みとイメージ肥大化の原因

Dockerイメージが数百MBから数GBに膨れ上がる原因は、ビルドに使った開発ツールがそのまま本番イメージに残ることです。マルチステージビルドを使えば、この問題を根本から解消できます。

シングルステージビルドでビルドツールが本番に残留する問題

シングルステージのDockerfileでは、1つのFROM命令ですべてを処理します。コンパイラやヘッダファイルなど、ビルド専用ツールも最終イメージに残ります。

たとえばGoアプリの場合を考えてみましょう。golangベースイメージにはGo SDK全体が含まれます。サイズは約700MB超です。本番で必要なのはコンパイル済みバイナリだけです。SDKごとデプロイしている状態になります。

Pythonでも同様の問題が起きます。gccg++はC拡張ライブラリのビルドに必要です。しかしビルド完了後の本番では不要です。こうした不要ファイルの蓄積が肥大化の根本原因です。

複数FROM命令とCOPY —from構文による解決アプローチ

マルチステージビルドでは、Dockerfile内に複数のFROM命令を記述します。各FROMが新しいステージを開始します。COPY --from構文で前のステージから成果物だけをコピーできます(Docker公式ドキュメント)。

基本的な構造は以下のとおりです。

# ステージ1: ビルド環境
FROM golang:1.23 AS builder
WORKDIR /app
COPY . .
RUN go build -o main .

# ステージ2: 本番環境(ビルドツールを含まない)
FROM gcr.io/distroless/static-debian12
COPY --from=builder /app/main /
CMD ["/main"]

ポイントはAS builderでステージに名前をつける点です。最終ステージではCOPY --from=builderでバイナリだけを取り出します。ビルドツールやソースコードは最終イメージに残りません。

実測データ — Go: 729MB→26MB、Python: 1.45GB→636MB

マルチステージビルドの効果は、実測データで明確に確認できます。

言語最適化前最適化後削減率
Go729MB26.2MB約96%
Python(Flask)1.45GB636MB約56%

Goでは静的バイナリをDistrolessイメージにコピーします。これで約96%の削減を達成しています(Zenn記事)。Go特有のシングルバイナリ生成がこの劇的な削減を可能にしています。

一方、PythonではPromptflow + Flask構成で約810MBの削減に成功しています(SIOS Tech Lab)。C拡張ライブラリのランタイム依存があるため、Goほどの削減率にはなりません。それでも56%の削減はCI/CD転送時間の短縮に大きく効きます。

前提条件と環境構築

マルチステージビルドを始める前に、Docker環境の確認とツールの準備を済ませましょう。ここでの設定漏れはビルドエラーの原因になります。

Docker Engine・BuildKitのバージョン要件と有効化確認

マルチステージビルドにはDocker Engine 17.05以上が必要です。cache mountsを活用するにはBuildKitも必要です。Docker Engine 23.0以降ではBuildKitがデフォルトです。23.0未満の場合は環境変数で有効化してください。

  1. Dockerバージョンを確認する
# macOS / Linux
docker version --format '{{.Server.Version}}'
# Windows PowerShell
docker version --format '{{.Server.Version}}'

出力が 23.0 以上ならBuildKitは有効です。

  1. 23.0未満の場合、BuildKitを有効化する
# macOS / Linux(現在のシェルセッションで有効化)
export DOCKER_BUILDKIT=1
# Windows PowerShell
$env:DOCKER_BUILDKIT=1
  1. BuildKitの動作を確認する
# macOS / Linux
docker buildx version
# Windows PowerShell
docker buildx version

バージョン情報が表示されれば準備完了です。Docker Desktopユーザーは23.0未満でもBuildKitが有効です。

diveツールのインストールとサンプルアプリの準備

docker historyではレイヤーサイズは把握できます。しかしどのファイルが容量を圧迫しているかはわかりません。diveはファイルレベルで浪費を可視化するツールです。

  1. diveをインストールする
# macOS(Homebrew)
brew install dive
# Linux(Debian/Ubuntu)
wget https://github.com/wagoodman/dive/releases/download/v0.12.0/dive_0.12.0_linux_amd64.deb
sudo apt install ./dive_0.12.0_linux_amd64.deb
# Windows PowerShell(Scoop)
scoop install dive
  1. 動作確認として既存イメージを分析する
# macOS / Linux
dive golang:1.22
# Windows PowerShell
dive golang:1.22

レイヤー一覧とファイルツリーが対話的に表示されます。自分のプロジェクトのイメージもこの方法で分析しておきましょう。後の改善効果を定量的に比較できます。

本記事のコード例は以下の環境で動作確認済みです。Docker Engine 27.x、BuildKit v0.17.x、dive v0.12.0。

実践手順 — マルチステージビルドの実装とベースイメージ選択

マルチステージビルドの基本は3ステップです。ビルドステージを定義し、成果物を本番へコピーし、ベースイメージを選びます。言語別の実装例と選定基準を紹介します。

ステップ1: ビルドステージの定義とAS句による命名

Dockerfileに複数のFROM命令を書きます。それぞれが独立したステージです。AS句で名前を付けておきましょう。後続ステージから成果物を取り出せます。

以下はGoアプリケーションの基本構成です。

# ビルドステージ:コンパイラやソースコードを含む
FROM golang:1.23 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/main .

# 本番ステージ:バイナリだけをコピー
FROM gcr.io/distroless/static:nonroot
COPY --from=builder /app/main /main
ENTRYPOINT ["/main"]

CGO_ENABLED=0で静的バイナリを生成するのがポイントです。本番ステージにCランタイムが不要になります。この構成でGoアプリは729MBから約26MBへ96%削減できます(Docker公式)。

ステップ2: 言語別Dockerfile実装例(Go・Python・Node.js)

Python(C拡張ライブラリを含む場合)

C拡張はビルドステージでコンパイルし、wheelだけを本番へ持ち込みます。

FROM python:3.12-slim AS builder
WORKDIR /app
RUN apt-get update && apt-get install -y gcc g++ --no-install-recommends
COPY requirements.txt .
RUN pip wheel --no-cache-dir --wheel-dir /wheels -r requirements.txt

FROM python:3.12-slim
WORKDIR /app
COPY --from=builder /wheels /wheels
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
COPY . .
CMD ["python", "main.py"]

この手法で1.45GBから636MBへ約56%の削減が可能です(SIOS Tech Lab)。

Node.js(ビルド成果物をnginxで配信)

FROM node:20-slim AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --no-audit
COPY . .
RUN npm run build

FROM nginx:1.25-alpine-slim
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80

ビルドと実行は以下のコマンドです。

# macOS / Linux
docker build -t myapp:latest .
# Windows PowerShell
docker build -t myapp:latest .

ステップ3: ベースイメージ選択 — Alpine・Slim・Distrolessの比較と使い分け

ベースイメージはサイズだけで判断すると痛い目に遭います。inductorのブログが指摘するとおりです。Alpineのmusl-libcはPythonやNode.jsで互換性問題を起こすことがあります。

イメージサイズ目安Cライブラリ推奨用途
Alpine約7MBmusl-libcGo静的バイナリ、シェル操作不要な場面
Debian Slim約75MBglibcPython・Node.js・Ruby全般
Distroless static約2MBなしGo静的バイナリ(最小攻撃面)
Distroless base約21MBglibcJava・動的リンクが必要なアプリ

迷ったらまずDebian Slimを選ぶのが安全です。サイズを追求する段階でDistrolessへ移行しましょう。Distrolessはシェルすら含まないため、セキュリティ面でも優れています。

BuildKit活用とイメージ分析による高度な最適化

Docker Engine 23.0以降、BuildKitがデフォルトです。ここではBuildKitの実践機能とイメージ分析ツールを解説します。

cache mountsと—targetフラグによるビルド高速化

--mount=type=cacheでキャッシュを永続化できます。依存関係のインストール速度が大幅に向上します(参考)。

以下はPythonプロジェクトでの設定例です。

# syntax=docker/dockerfile:1
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install --prefix=/install -r requirements.txt

FROM python:3.12-slim AS production
COPY --from=builder /install /usr/local
COPY . .
CMD ["python", "main.py"]

--targetで特定ステージだけをビルドできます。CI/CDで段階的に実行する際に便利です。

# macOS / Linux — テストステージのみ実行
docker build --target builder -t myapp:test .
# Windows PowerShell — テストステージのみ実行
docker build --target builder -t myapp:test .

diveによるレイヤー解析と.dockerignoreの最適化

docker historyではレイヤーサイズしかわかりません。diveならファイル単位で容量を特定できます。

# macOS / Linux — diveでイメージを分析
dive myapp:latest
# Windows PowerShell — diveでイメージを分析
dive myapp:latest

不要ファイルが見つかったら.dockerignoreで除外します。.envや秘密鍵の混入防止にも直結します。セキュリティ面でも必須の設定です。

# .dockerignore — 最低限設定すべきパターン
.git
.env
*.pem
node_modules
__pycache__
*.log

CI/CD連携 — GitHub Actionsでの段階的ビルド設定例

GitHub Actionsではレジストリキャッシュで高速化できます。テストと本番ビルドを分離した構成例を示します。

# .github/workflows/build.yml
name: Build and Push
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/setup-buildx-action@v3
      - uses: docker/build-push-action@v6
        with:
          context: .
          target: production
          push: true
          tags: ghcr.io/myorg/myapp:latest
          cache-from: type=registry,ref=ghcr.io/myorg/myapp:cache
          cache-to: type=registry,ref=ghcr.io/myorg/myapp:cache,mode=max

cache-fromcache-toでレジストリキャッシュを指定します。mode=maxは全中間レイヤーをキャッシュする設定です。ステージが多いほど効果を発揮します。

トラブルシューティング:頻出エラーと解決策

マルチステージビルドでよく遭遇するエラーと対処法をまとめます。

COPY —fromでファイルが見つからない

最も多いエラーです。以下のメッセージが出ます。

failed to compute cache key: "/app/main" not found

原因はビルドステージでの出力パスとCOPY --fromのパスが不一致なことです。解決策を示します。

# NG: パスが一致していない
RUN go build -o main .
COPY --from=builder /app/main /main

# OK: 絶対パスで揃える
RUN go build -o /app/main .
COPY --from=builder /app/main /main

WORKDIRの設定も確認してください。ステージごとに独立しています。

Alpine上でのセグメンテーションフォールト

Pythonのnumpyなど、glibc依存のライブラリをAlpine上で実行すると発生します。

Segmentation fault (core dumped)

Alpineはmusl-libcを使用しています。glibcとの互換性がない場合があります。解決策はDebian Slimベースへの切り替えです。

# NG: Alpine + glibc依存ライブラリ
FROM python:3.12-alpine

# OK: Debian Slim
FROM python:3.12-slim

BuildKitのcache mountが効かない

--mount=type=cacheが無視される場合があります。

# BuildKitが有効か確認
docker buildx version

Dockerfileの先頭に以下が必要です。

# syntax=docker/dockerfile:1

この行がないとcache mountsが無視されます。Docker Engine 23.0未満では環境変数も必要です。

# macOS / Linux
export DOCKER_BUILDKIT=1
# Windows PowerShell
$env:DOCKER_BUILDKIT=1

マルチプラットフォームビルドの型エラー

ARM(Apple Silicon)とx86で異なるエラーが出る場合があります。CGO有効時に特に発生しやすいです。

# macOS / Linux — プラットフォームを明示してビルド
docker buildx build --platform linux/amd64 -t myapp .
# Windows PowerShell
docker buildx build --platform linux/amd64 -t myapp .

Go言語の場合はCGO_ENABLED=0で静的バイナリにするのが確実です。

まとめ:明日から始めるイメージ最適化ロードマップ

マルチステージビルドの導入は、一度にすべてを完璧にする必要はありません。段階的に進めることで、確実に成果を積み上げられます。

ステップ1:現状を数値で把握する

まずdocker historyとdiveでイメージを分析してください。どのレイヤーが容量を圧迫しているか特定しましょう。この分析だけで「ビルドツールが500MB以上残っていた」と気づくケースが大半です。

ステップ2:最もサイズの大きいイメージから着手する

全サービスを一斉に書き換える必要はありません。最も肥大化しているイメージから着手しましょう。GoやRustはDistrolessとの組み合わせで90%超の削減が見込めます。PythonやNode.jsはDebian Slimへの移行から始めるのが安全です。

ステップ3:CI/CDパイプラインにキャッシュ戦略を組み込む

ローカルでの最適化が完了したらCI/CDに展開しましょう。cache mountsとレジストリキャッシュを導入してください。ビルド時間の短縮はデプロイ頻度の向上に直結します。--targetで各フェーズを独立して実行できます。

発展的な取り組み

マルチステージビルドに慣れたら、次の技術にも挑戦しましょう。kanikoはDocker daemonなしでビルドできるツールです。Kubernetes上での利用に適しています。

docker buildxによるマルチアーキテクチャビルドも検討してみてください。ARM/x86の同時対応が可能です。Apple SiliconやAWS Gravitonの普及が進む今、活用の幅が広がっています。

イメージサイズの削減は転送時間の短縮に直結します。ストレージコストやセキュリティリスクの低減にもつながります。まずはdiveで現状のイメージを1つ分析してみてください。

参考文献

s

この記事を書いた人

数学科出身のWebエンジニア

共有: