Dockerマルチステージビルド実践:Go/Java/RustイメージをGBからMBへ劇的スリム化

650MBのイメージが引き起こした疑問
先週の金曜日の午後、K8sダッシュボードで5分経っても「Pulling image」のままのPodを見つめながら、私はイライラしていました。単純なSpring Bootアプリなのに、Dockerイメージにすると650MBもあるのです。チームの新入りインターン生に「なんでこのイメージこんなに大きいんですか?」と聞かれ、私は一瞬言葉に詰まりました。自分でも当たり前だと思って、真剣に考えたことがなかったのです。
その夜、私は徹底的に調べました。翌朝、同じアプリのイメージは89MBになっていました。デプロイ時間は5分から1分未満に短縮されました。インターン生の私を見る目が変わったのは言うまでもありません。
やったことは、Dockerfileの数行を変えただけ。このテクニックを「マルチステージビルド」と呼びます。
正直、最初は効果を疑っていました。我々のGoプロジェクトは295MB、Javaプロジェクトは平気で500MB超え。「Dockerイメージなんてそんなもんだろ」と思っていました。しかし、Rustのイメージを2GBから11MB——そう、GBからMBです——に圧縮した事例を見て、自分のやり方が根本的に間違っていたことに気づきました。
問題の核心はこうです:コンパイル言語はソースコードを実行ファイルにするためにコンパイラやビルドツールが必要ですが、実行時にはそれらは一切不要なのです。従来のDockerfileはMaven、Gradle、Goコンパイラを全部詰め込んでいました。引っ越しの時に、内装工事に使ったドリルやセメント袋まで新居に持ち込むようなものです。全く無駄ですよね。
今日はGo、Java、Rustの3つの実例を使って、マルチステージビルドでイメージを劇的に痩せさせる方法を紹介します。
- Goアプリ:295MB → 6.47MB (98%減)
- Java Spring Boot:650MB → 89MB (86%減)
- Rustアプリ:2GB → 11.2MB (99.4%減)
なぜあなたのイメージは肥満なのか?
まず、以前テストした実際のデータを見てください:
| 言語 | 従来のシングルステージ | マルチステージ | 削減率 |
|---|---|---|---|
| Goアプリ | 295 MB | 6.47 MB | 98% |
| Java Spring Boot | 650 MB | 89 MB | 86% |
| Rustアプリ | 2.1 GB | 11.2 MB | 99.4% |
初めてこの表を見たとき、Rustの2GBから11MBという数字は魔法かと思いました。
原因は明白です。コンパイル型言語はビルドツールを必要とします。
- Goコンパイラ:約300MB
- Maven + OpenJDK:約500MB
- Rustツールチェーン:約1.5GB
従来のDockerfile(やってはいけない例):
FROM golang:1.21
WORKDIR /app
COPY . .
RUN go build -o myapp
CMD ["./myapp"]これの何が問題か?ベースイメージの golang:1.21 (295MB) をそのまま使っていることです。コンパイル後も、コンパイラやデバッグツールがそのまま残っています。
弊害は容量だけではありません:
- プルが遅い:CI/CDで毎回650MBを転送していては、ネットワークが少し遅いだけで致命的です。
- セキュリティリスク:本番環境にソースコードやコンパイラがあるのは、攻撃者に武器を渡しているようなものです。
- キャッシュの無駄:コードを1行変えただけで、巨大なイメージ全体を再ビルドする必要があります。
マルチステージビルド:1つのDockerfile、2つの世界
Docker 17.05で導入されたマルチステージビルドの仕組みは単純です。「ビルド環境」と「実行環境」を分けるのです。
最もシンプルな例を見てみましょう:
# 第1ステージ:ビルド用
FROM golang:1.21 AS builder
WORKDIR /app
COPY . .
RUN go build -o myapp
# 第2ステージ:実行用
FROM alpine:3.18
WORKDIR /app
COPY --from=builder /app/myapp .
CMD ["./myapp"]ポイントは:
- 最初の
FROMにAS builderと名前をつける。 - 2つ目の
FROMで新しいベースイメージ(alpine)から開始。 COPY --from=builderで、ビルドした成果物だけをコピー。
これで、最初のステージにあった295MBのコンパイラたちはすべて破棄されます。残るのは数MBのAlpineと、あなたのアプリだけです。
Goアプリの究極スリム化
Goは静的リンクされたバイナリを生成できるため、最も劇的な効果が得られます。
最適化版 Dockerfile:
# ビルドステージ
FROM golang:1.21-alpine AS builder
WORKDIR /app
# 依存ファイルのコピーとダウンロード(キャッシュ活用)
COPY go.mod go.sum ./
RUN go mod download
# ソースコピーとビルド
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o myapp .
# 実行ステージ
FROM scratch
WORKDIR /app
COPY --from=builder /app/myapp .
EXPOSE 8080
CMD ["./myapp"]結果:6.47MB。
解説:
CGO_ENABLED=0:C言語ライブラリ依存を排除し、完全な静的バイナリを作成。-ldflags="-w -s":デバッグ情報とシンボルテーブルを削除し、サイズを縮小。FROM scratch:Dockerの「何もない」空イメージ。Goの静的バイナリはこれだけで動きます。
注意点:scratch にはシェルもタイムゾーンもCA証明書もありません。HTTPS通信が必要な場合は、gcr.io/distroless/static-debian11 を使うか、証明書を手動でコピーする必要があります。
Java/Spring Bootのエレガントな減量
JavaはJRE(Java Runtime Environment)が必要なので、Goほど小さくはなりませんが、それでも効果は絶大です。
最適化版 Dockerfile:
# ビルドステージ
FROM maven:3.8-openjdk-17-slim AS builder
WORKDIR /app
# pom.xmlのみコピーして依存解決
COPY pom.xml .
RUN mvn dependency:go-offline -B
# ソースを入れてパッケージング
COPY src ./src
RUN mvn package -DskipTests
# 実行ステージ
FROM openjdk:17-jre-slim
WORKDIR /app
# jarファイルのみコピー
COPY --from=builder /app/target/*.jar app.jar
# JVM最適化
ENV JAVA_OPTS="-Xms128m -Xmx512m -XX:+UseContainerSupport"
EXPOSE 8080
CMD java $JAVA_OPTS -jar app.jar結果:650MB → 89MB。
解説:
- JDK vs JRE:ビルドにはJDKが必要ですが、実行はJREで十分です。JREはJDKの半分以下のサイズです。
- Mavenキャッシュ:pom.xmlだけ先にコピーして依存をダウンロードすることで、ソースコード変更時に依存解決をスキップできます。
Rustアプリの最小構成
Rustのツールチェーンは巨大ですが、生成物は非常にコンパクトです。
最適化版 Dockerfile:
# ビルドステージ
FROM rust:1.75 AS builder
WORKDIR /app
# 依存関係のキャッシュハック
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
# 本番ビルド
COPY src ./src
RUN touch src/main.rs
RUN cargo build --release
# 実行ステージ
FROM gcr.io/distroless/cc-debian11
WORKDIR /app
COPY --from=builder /app/target/release/myapp .
CMD ["./myapp"]結果:2.1GB → 11.2MB。
解説:
Rustの依存コンパイルは時間がかかります。ダミーの main.rs を作って cargo build することで、依存ライブラリのコンパイル結果をキャッシュさせ、ソース変更時のビルド時間を劇的に短縮しています。
ベストプラクティスとよくある罠
.dockerignore を忘れない
.gitディレクトリやnode_modules、ローカルのtargetディレクトリはDockerコンテキストに送る必要はありません。.dockerignoreファイルでこれらを除外すると、ビルド開始が速くなります。ベースイメージのバージョン固定
golang:latestはやめましょう。いつの間にかバージョンが上がり、ビルドが壊れる原因になります。golang:1.21やgolang:1.21-alpineのように指定しましょう。Distrolessイメージの活用
Googleが提供するdistrolessイメージ(gcr.io/distroless/...)は、シェルやパッケージマネージャを含まない最小限の実行用イメージです。セキュリティ的に非常に堅牢でおすすめです。
基礎イメージ選択ガイド
どれを選べばいいかわからない?目安はこちら:
| イメージタイプ | サイズ | 内容 | メリット | デメリット | 推奨言語 |
|---|---|---|---|---|---|
| scratch | 0 MB | 完全に空 | 最小・最強のセキュリティ | シェルなし、デバッグ不可 | Go, Rust (静的ビルド) |
| distroless | 2-20 MB | ランタイムのみ | シェルなし、セキュア | デバッグ困難 | Go, Java, Rust, Node |
| alpine | 5-40 MB | musl libc, apk | 小さい、シェルあり | glibc互換性問題あり | 汎用 |
| slim | 70-120 MB | 最小Debian | glibcあり、互換性良 | 少し大きい | Java, Python |
まとめ:今日からできること
マルチステージビルドの核心は3点です:
- ビルドステージ:重装備のイメージでコンパイル
- 実行ステージ:最小限のイメージで実行
- COPY —from:成果物だけを持ち出す
これは技術的に難しいことではありませんが、意識の問題です。放置された巨大なイメージは、デプロイ速度とセキュリティを蝕みます。
アクションプラン:
- 手元のプロジェクトを1つ選び、マルチステージ化してみてください。
docker imagesでサイズを比較してニヤリとしてください。- もし劇的に減ったら、チームに自慢しましょう。
Dockerイメージのダイエットは、最もコストパフォーマンスの高い最適化の1つです。ぜひ試してみてください。
Dockerマルチステージビルド導入フロー
Go/Java/RustアプリのDockerイメージサイズを90%以上削減する具体的な手順
⏱️ Estimated time: 1 hr
- 1
Step1: マルチステージビルドの原理理解
基本概念:
• ビルドに必要なツール(コンパイラ等)と実行に必要な環境(ランタイム)を分ける
• 第1ステージで作ったバイナリやJarファイルだけを、第2ステージの軽量イメージにコピーする
• 結果として、ビルドツールを含まない超軽量イメージができる - 2
Step2: Go言語での実践
Dockerfile構成:
1. FROM golang:1.21 AS builder でビルド
2. CGO_ENABLED=0 で静的バイナリ作成
3. FROM scratch(またはalpine)で実行
4. COPY --from=builder でバイナリをコピー
結果:数百MB → 数MB - 3
Step3: Java/Spring Bootでの実践
Dockerfile構成:
1. FROM maven:3.8-jdk-17 AS builder でビルド
2. mvn package でJar作成
3. FROM openjdk:17-jre-slim で実行(JDKではなくJREを使うのがコツ)
4. COPY --from=builder でJarをコピー
結果:650MB → 89MB
FAQ
マルチステージビルドのメリットは何ですか?
Scratchイメージとは何ですか?
マルチステージビルドでデバッグはどうすればいいですか?
デバッグ時は docker build --target builder ... のようにターゲットを指定してビルドステージのイメージを作成し、そこに入って調査するのが有効です。
4 min read · 公開日: 2025年12月17日 · 更新日: 2026年1月22日
関連記事
Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践

Next.js ファイルアップロード完全ガイド:S3/Qiniu Cloud 署名付き URL 直接アップロード実践
Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド

Next.js Eコマース実践:カートと Stripe 決済の完全実装ガイド
Next.js ユニットテスト実践:Jest + React Testing Library 完全設定ガイド


コメント
GitHubアカウントでログインしてコメントできます