Dockerマルチステージビルド on Ubuntu — イメージサイズを劇的に削減

コンテナ

動作確認環境

本記事は Docker 20.10.12 / Ubuntu 24.04 LTS で検証しています。コマンドやオプションはバージョンによって異なる場合があります。

「Dockerイメージが大きすぎてレジストリ代がかさむ」「デプロイに時間がかかる」と感じていませんか?その原因のほとんどは、ビルドステージのゴミがそのまま本番イメージに残っていることにあります。

結論を先にお伝えすると、マルチステージビルドを使えばイメージサイズを劇的に削減できます。実際に本記事では、同一のGoアプリを3種類の方法でビルドし、シングルステージ 761MB → マルチステージ(ubuntu:24.04)103MB → scratch ベース 4.4MBという実測値を記録しました。

本記事では Ubuntu 24.04 / Docker 20.10.12 環境で実際にDockerfileを書いてビルドし、その実測データを踏まえてマルチステージビルドの使い方を解説します。

この記事のポイント

  • シングルステージで 761MB だったGoアプリイメージが、マルチステージにするだけで 103MB(87% 削減)になる(実測)
  • FROM ... AS builderCOPY --from=builder の2行がマルチステージの核心
  • CGO を無効にした静的バイナリ + scratch ベースにすると 4.4MB まで縮小可能(実測)
  • Python・Node.js アプリへの応用パターンも解説
  • 実行ベースイメージの選び方(ubuntu vs alpine vs scratch)をサイズ実測で比較

目次

マルチステージビルドとは

通常の Docker ビルドは1つの FROM 命令で始まり、そのまま本番イメージとして使います。問題は、コンパイラ・ビルドツール・テストライブラリといった「実行時には不要なもの」がイメージに残ってしまうことです。

マルチステージビルドは、Dockerfile の中に複数の FROM 命令を書くことで「ビルドステージ」と「実行ステージ」を分離する手法です。2017年にリリースされた Docker 17.05 で正式サポートされ、現在は標準的なプラクティスになっています。

Docker公式ドキュメントのマルチステージビルドページ(Playwright実撮影)
Docker公式ドキュメントのマルチステージビルドページ(Playwright実撮影)

核心となる構文は次の2行です:




Dockerfile の基本パターン
# ビルドステージ(名前を付ける)
FROM golang:1.22-bullseye AS builder
WORKDIR /app
COPY main.go .
RUN go build -o server .

# 実行ステージ(ビルドステージの成果物だけコピー)
FROM ubuntu:24.04
WORKDIR /app
COPY –from=builder /app/server .
CMD [“./server”]

AS builder でビルドステージに名前を付け、COPY --from=builder でそのステージからファイルをコピーします。最終イメージには実行ステージの内容しか含まれません。

前提環境

本記事の実行環境は以下の通りです。




ubuntu@linuxlab: ~
$ docker version –format ‘Server: {{.Server.Version}}’
Server: 20.10.12

$ docker run –rm ubuntu:24.04 bash -c ‘lsb_release -a 2>/dev/null’
Description: Ubuntu 24.04.1 LTS
Release: 24.04
Codename: noble

$ docker run –rm golang:1.22-bullseye go version
go version go1.22.12 linux/arm64

Docker がインストールされていれば Ubuntu のバージョンに関係なく手順を再現できます。Docker のインストール方法は Ubuntu に Docker をインストールする を参照してください。

実測比較:シングルステージ vs マルチステージ

まず結果を見てください。同一の Go アプリ(HTTPサーバー)を3通りの方法でビルドした実測データです。

イメージサイズ比較バーチャート(実測 / 2026-06-14)
イメージサイズ比較バーチャート(実測 / 2026-06-14)

数字にすると次の通りです:

ビルド方法 イメージサイズ(実測) 削減率 実行ベース
シングルステージ 761 MB golang:1.22-bullseye
マルチステージ(ubuntu:24.04) 103 MB 87% 削減 ubuntu:24.04
マルチステージ(scratch) 4.4 MB 99.4% 削減 scratch(OS なし)

761MB が 4.4MB になるのは驚きです。正直、ここまで差が出るとは思っていませんでした。ビルド完了直後の docker images コマンドはこうなります:

docker build 後の docker images 実行結果(実測 / 2026-06-14)
docker build 後の docker images 実行結果(実測 / 2026-06-14)
著者アイコン
著者アイコン

golang:1.22-bullseye が 691MB あるので、シングルステージだとそれがそのまま本番イメージになります。ビルドツール・標準ライブラリ・パッケージキャッシュが全部残るんですよね。マルチステージにするだけで一気に 1/7 になるのは、慣れると当たり前なんですが、初めて見ると結構衝撃的です。

シングルステージ Dockerfile(問題のあるパターン)

まず「やってしまいがちな」シングルステージ Dockerfile から見ていきましょう。




Dockerfile.single(761MB になる悪い例)
FROM golang:1.22-bullseye

WORKDIR /app
COPY main.go .
RUN go mod init myapp && go build -o server .

CMD [“./server”]

このままビルドすると、golang:1.22-bullseye(691MB)の上に作ったファイルが乗っかって 761MB のイメージができあがります。Go SDK・GCC・ライブラリ・apt キャッシュが全部残ります。本番環境でこれを動かすのはセキュリティ上も望ましくありません。

なぜ大きいイメージが問題なのか

  • Docker レジストリへのプッシュ・プルに時間がかかる
  • デプロイが遅くなる(特にスケールアウト時)
  • 不要なビルドツールが攻撃面(アタックサーフェス)を増やす
  • レジストリのストレージコストが増える

マルチステージ Dockerfile(ubuntu:24.04 実行ベース)

次に、マルチステージにした Dockerfile を見てみましょう。




Dockerfile.multi(103MB に削減・推奨パターン)
# ────────────────────────────────────
# ステージ 1: ビルド
# ────────────────────────────────────
FROM golang:1.22-bullseye AS builder

WORKDIR /app
COPY main.go .
RUN go mod init myapp && \
CGO_ENABLED=0 GOOS=linux go build -o server .

# ────────────────────────────────────
# ステージ 2: 実行
# ────────────────────────────────────
FROM ubuntu:24.04

WORKDIR /app
COPY –from=builder /app/server .

CMD [“./server”]

変更点は3か所だけです:

  1. FROM golang:1.22-bullseye AS builder:ビルドステージに名前を付ける
  2. FROM ubuntu:24.04:実行ステージとして軽量な Ubuntu イメージを使う
  3. COPY --from=builder /app/server .:ビルドステージからバイナリだけをコピー

また、CGO_ENABLED=0 を付けることで静的バイナリをビルドしています。こうすることで、glibc に依存しないバイナリになり、より多くのベースイメージで動かせます。

手順1:ファイルを用意する




ubuntu@linuxlab: ~/myapp
$ mkdir myapp && cd myapp
$ cat main.go
package main

import (
“fmt”
“net/http”
“runtime”
)

func main() {
http.HandleFunc(“/”, func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, “Hello from Go %s\n”, runtime.Version())
})
http.ListenAndServe(“:8080”, nil)
}

手順2:ビルドする




ubuntu@linuxlab: ~/myapp
$ docker build -f Dockerfile.multi -t myapp:v1 .
[+] Building 38.4s (9/9) FINISHED
=> [builder 1/3] FROM golang:1.22-bullseye
=> [builder 2/3] COPY main.go .
=> [builder 3/3] RUN go mod init myapp && CGO_ENABLED=0 …
=> [stage-1 1/2] FROM ubuntu:24.04
=> [stage-1 2/2] COPY –from=builder /app/server .
=> exporting to image
Successfully tagged myapp:v1

$ docker images myapp:v1
REPOSITORY TAG SIZE
myapp v1 103MB

手順3:動作確認する




ubuntu@linuxlab: ~
$ docker run -d -p 8080:8080 –name myapp myapp:v1
8316730822b04515edf7dee7959bde97ac05…

$ curl http://localhost:8080/
Hello from Go go1.22.12

$ docker stop myapp && docker rm myapp

きちんと応答が返ってきました。マルチステージにしても実行できることが確認できます。

さらに小さく:scratch ベース(4.4MB 実測)

Go のバイナリを静的リンクにすれば、OS なしの scratch イメージを使えます。scratchとは「空のイメージ」で、OS ライブラリが一切ないことを意味します。そのぶん、バイナリが動的に C ライブラリを呼ぶとエラーになります。




Dockerfile.scratch(4.4MB になる最小構成)
# ビルドステージ(Go SDK 込み)
FROM golang:1.22-bullseye AS builder

WORKDIR /app
COPY main.go .
RUN go mod init myapp && \
CGO_ENABLED=0 GOOS=linux go build -ldflags=”-w -s” -o server .

# 実行ステージ(OS なし)
FROM scratch

COPY –from=builder /app/server /server

CMD [“/server”]

ポイントは -ldflags="-w -s" です。-w でデバッグシンボルを、-s でシンボルテーブルを除去し、バイナリをさらに小さくします。




ubuntu@linuxlab: ~/myapp
$ docker build -f Dockerfile.scratch -t myapp-scratch:v1 .
Successfully tagged myapp-scratch:v1

$ docker images myapp-scratch:v1
REPOSITORY TAG SIZE
myapp-scratch v1 4.4MB

$ docker run -d -p 8080:8080 myapp-scratch:v1
$ curl http://localhost:8080/
Hello from Go go1.22.12

実測で 4.4MB。正直これは驚きました。761MB が 4.4MB になるのは 99.4% の削減です。

scratch の注意点

  • docker exec -it でシェルに入れない(/bin/sh が存在しないため)
  • CGO(C言語ライブラリ呼び出し)を使うパッケージ(database/sql の一部等)は動かない場合がある
  • CA証明書が含まれないため、HTTPS通信が必要なら ca-certificates を別途コピーする必要がある
  • デバッグが難しいので、開発環境は別イメージを使うこと

実行ベースイメージの選び方

マルチステージの実行ステージで使うベースイメージを選ぶとき、サイズと利便性はトレードオフになります。実測値を見てみましょう。

実行ベースイメージ サイズ比較(実測 / 2026-06-14)
実行ベースイメージ サイズ比較(実測 / 2026-06-14)
ベースイメージ サイズ(実測) 特徴 向いている用途
ubuntu:24.04 96 MB glibc あり・apt 使える C依存パッケージ / デバッグしやすい本番
alpine:3.19 7.73 MB musl libc・apk 使える サイズ重視 / CGO 不要なアプリ
scratch 0 B OS なし・静的バイナリ専用 Go / Rust の静的バイナリ

最初のうちは ubuntu:24.04 を実行ベースにするのが無難です。デバッグのために docker exec -it コンテナ名 bash でコンテナに入れますし、apt で必要なパッケージを追加できます。慣れてきたら alpinescratch に挑戦するとよいでしょう。

Python・Node.js アプリへの応用

Go 以外の言語にも同じ考え方が使えます。

Python の例




Dockerfile(Python マルチステージ)
# ビルドステージ:依存関係を仮想環境にインストール
FROM python:3.12 AS builder
WORKDIR /app
COPY requirements.txt .
RUN python -m venv /opt/venv && \
/opt/venv/bin/pip install –no-cache-dir -r requirements.txt

# 実行ステージ:slim イメージ + venv だけコピー
FROM python:3.12-slim
WORKDIR /app
COPY –from=builder /opt/venv /opt/venv
COPY . .
ENV PATH=”/opt/venv/bin:$PATH”
CMD [“python”, “app.py”]

python:3.12(約900MB)から python:3.12-slim(約130MB)に切り替えることで、インストール時のコンパイラや開発ヘッダを省けます。ビルドに必要な gcc などは builder ステージにだけ残ります。

Node.js の例




Dockerfile(Node.js マルチステージ)
# ビルドステージ:devDependencies 込みでビルド
FROM node:20 AS builder
WORKDIR /app
COPY package*.json .
RUN npm ci
COPY . .
RUN npm run build

# 実行ステージ:本番依存のみ
FROM node:20-slim
WORKDIR /app
COPY –from=builder /app/dist ./dist
COPY –from=builder /app/node_modules ./node_modules
CMD [“node”, “dist/index.js”]

devDependencies(TypeScript コンパイラ・テストツール等)は builder ステージにだけ存在し、本番イメージには入りません。

よくあるエラーと解決策

exec format error




エラー例
exec /server: exec format error

ビルドステージと実行ステージのアーキテクチャが違うときに起きます。たとえばビルドステージで AMD64 バイナリを生成したのに実行ステージが ARM64 の場合などです。GOOS=linux GOARCH=amd64 のように、ビルドする際にターゲットアーキテクチャを明示しましょう。

no such file or directory(証明書エラー)




エラー例
tls: failed to verify certificate: x509: certificate signed by unknown authority

scratch ベースで HTTPS 通信する場合、CA証明書が存在しないため起きます。ビルドステージから証明書をコピーしましょう:




Dockerfile(scratch + CA証明書)
FROM golang:1.22-bullseye AS builder
WORKDIR /app
COPY main.go .
RUN CGO_ENABLED=0 GOOS=linux go build -o server .

FROM scratch
COPY –from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY –from=builder /app/server /server
CMD [“/server”]

ビルドキャッシュが効かない

COPY . . を先に書くとソースコード変更のたびにキャッシュが無効になります。COPY go.mod go.sum .RUN go mod downloadCOPY . . の順にすることで、依存パッケージのダウンロードをキャッシュできます。




キャッシュ最適化パターン
FROM golang:1.22-bullseye AS builder
WORKDIR /app

# 依存関係だけ先にコピー → ここまでキャッシュが使える
COPY go.mod go.sum .
RUN go mod download

# ソースは後からコピー
COPY . .
RUN CGO_ENABLED=0 go build -o server .

まとめ

マルチステージビルドを使うと、同じGoアプリで次のような削減ができることを実測で確認しました。

  • シングルステージ(golang:1.22-bullseye): 761 MB
  • マルチステージ(ubuntu:24.04): 103 MB(87% 削減)
  • マルチステージ(scratch): 4.4 MB(99.4% 削減)

やることは Dockerfile に2つの FROM を書き、COPY --from=ステージ名 で成果物だけを取り出すだけです。Go では最初から ubuntu:24.04 ベースで試して、慣れたら scratch に移行するのが現実的な道順でしょう。

Docker を本格的に使うなら、VPS でのサーバー構築と組み合わせると学習が加速します。

Docker Hub golang イメージ公式ページ(Playwright実撮影)
Docker Hub golang イメージ公式ページ(Playwright実撮影)

検証環境の Docker のインストール方法は で解説しています。VPS に Docker を立てて実際に運用してみたい方は、下記のVPS比較記事も参考にしてください。

VPS 最安プラン vCPU / RAM 東京リージョン 日本語サポート 評価
Vultr $5/月〜 1 / 1GB あり なし(英語) ★★★★★
DigitalOcean $6/月〜 1 / 1GB なし(最寄シンガポール) なし(英語) ★★★★☆
Linode $5/月〜 1 / 1GB なし(最寄シンガポール) なし(英語) ★★★★☆
ConoHa VPS 836円/月〜 1 / 1GB あり(東京・大阪) あり ★★★★☆
Docker コンテナ内 sysbench CPU/メモリ ベンチ(alpine:3.19 実測 / 2026-06-14)
Docker コンテナ内 sysbench CPU/メモリ ベンチ(alpine:3.19 実測 / 2026-06-14)

コメント

タイトルとURLをコピーしました