動作確認環境
本記事は 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 builderとCOPY --from=builderの2行がマルチステージの核心- CGO を無効にした静的バイナリ + scratch ベースにすると 4.4MB まで縮小可能(実測)
- Python・Node.js アプリへの応用パターンも解説
- 実行ベースイメージの選び方(ubuntu vs alpine vs scratch)をサイズ実測で比較
目次
- マルチステージビルドとは
- 前提環境
- 実測比較:シングルステージ vs マルチステージ
- シングルステージ Dockerfile(問題のあるパターン)
- マルチステージ Dockerfile(ubuntu:24.04 実行ベース)
- さらに小さく:scratch ベース(4.4MB 実測)
- 実行ベースイメージの選び方
- Python・Node.js アプリへの応用
- よくあるエラーと解決策
- まとめ
マルチステージビルドとは
通常の Docker ビルドは1つの FROM 命令で始まり、そのまま本番イメージとして使います。問題は、コンパイラ・ビルドツール・テストライブラリといった「実行時には不要なもの」がイメージに残ってしまうことです。
マルチステージビルドは、Dockerfile の中に複数の FROM 命令を書くことで「ビルドステージ」と「実行ステージ」を分離する手法です。2017年にリリースされた Docker 17.05 で正式サポートされ、現在は標準的なプラクティスになっています。

核心となる構文は次の2行です:
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 でそのステージからファイルをコピーします。最終イメージには実行ステージの内容しか含まれません。
前提環境
本記事の実行環境は以下の通りです。
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通りの方法でビルドした実測データです。

数字にすると次の通りです:
| ビルド方法 | イメージサイズ(実測) | 削減率 | 実行ベース |
|---|---|---|---|
| シングルステージ | 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 コマンドはこうなります:

シングルステージ Dockerfile(問題のあるパターン)
まず「やってしまいがちな」シングルステージ Dockerfile から見ていきましょう。
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 を見てみましょう。
# ステージ 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か所だけです:
FROM golang:1.22-bullseye AS builder:ビルドステージに名前を付けるFROM ubuntu:24.04:実行ステージとして軽量な Ubuntu イメージを使うCOPY --from=builder /app/server .:ビルドステージからバイナリだけをコピー
また、CGO_ENABLED=0 を付けることで静的バイナリをビルドしています。こうすることで、glibc に依存しないバイナリになり、より多くのベースイメージで動かせます。
手順1:ファイルを用意する
$ 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:ビルドする
[+] 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:動作確認する
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 ライブラリを呼ぶとエラーになります。
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 でシンボルテーブルを除去し、バイナリをさらに小さくします。
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を別途コピーする必要がある - デバッグが難しいので、開発環境は別イメージを使うこと
実行ベースイメージの選び方
マルチステージの実行ステージで使うベースイメージを選ぶとき、サイズと利便性はトレードオフになります。実測値を見てみましょう。

| ベースイメージ | サイズ(実測) | 特徴 | 向いている用途 |
|---|---|---|---|
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 で必要なパッケージを追加できます。慣れてきたら alpine や scratch に挑戦するとよいでしょう。
Python・Node.js アプリへの応用
Go 以外の言語にも同じ考え方が使えます。
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 の例
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
ビルドステージと実行ステージのアーキテクチャが違うときに起きます。たとえばビルドステージで AMD64 バイナリを生成したのに実行ステージが ARM64 の場合などです。GOOS=linux GOARCH=amd64 のように、ビルドする際にターゲットアーキテクチャを明示しましょう。
no such file or directory(証明書エラー)
scratch ベースで HTTPS 通信する場合、CA証明書が存在しないため起きます。ビルドステージから証明書をコピーしましょう:
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 download → COPY . . の順にすることで、依存パッケージのダウンロードをキャッシュできます。
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 のインストール方法は で解説しています。VPS に Docker を立てて実際に運用してみたい方は、下記のVPS比較記事も参考にしてください。
| VPS | 最安プラン | vCPU / RAM | 東京リージョン | 日本語サポート | 評価 |
|---|---|---|---|---|---|
| Vultr | $5/月〜 | 1 / 1GB | あり | なし(英語) | ★★★★★ |
| DigitalOcean | $6/月〜 | 1 / 1GB | なし(最寄シンガポール) | なし(英語) | ★★★★☆ |
| Linode | $5/月〜 | 1 / 1GB | なし(最寄シンガポール) | なし(英語) | ★★★★☆ |
| ConoHa VPS | 836円/月〜 | 1 / 1GB | あり(東京・大阪) | あり | ★★★★☆ |




コメント