「肥大化した」コンテナとデプロイ速度の悩み
GoアプリのDocker化を始めたばかりの頃、私はただパッケージ化して実行するだけでいいと思っていました。「教科書通り」のDockerfileを書き、FROM golang:latest を使い、コードをコピーして go build を実行しました。その結果、愕然としました。単純な Hello World アプリなのに、イメージのサイズが800MB近くもあったのです。
安価なVPSへのデプロイや、不安定な4G回線での実行を想像してみてください。その巨大なイメージをプルするのを待つのは、まさに苦行です。ストレージ容量を消費するだけでなく、CI/CDプロセスが理不尽に遅くなります。Goは超軽量なバイナリを作成できることで有名なはずなのに、この800MBはどこから来たのでしょうか?
原因の分析:なぜイメージはこれほど重いのか?
問題はコードにあるのではありません。選択した「ベースイメージ(base image)」にあります。DebianやUbuntuベースの golang:latest を使用すると、OS全体をコンテナに詰め込むことになります。
- 巨大なビルドツール群: イメージにはコンパイラ、デバッガ、出力先のOSに合わせた重いCライブラリが多数含まれています。実際、実行時(runtime)にはアプリはこれらを全く必要としません。
- 不要なパッケージ: システムユーティリティ、シェル、パッケージマネージャーなどが数百MBを占めていますが、使われることはありません。
- セキュリティの脆弱性: プリインストールされたソフトウェアが多いほど、攻撃対象領域(アタックサーフェス)が広がります。ハッカーは、権限昇格のために
curlやaptが最初から入っているコンテナを好みます。
「軽量化」戦略:アマチュアからプロ級まで
本番プロジェクトでの試行錯誤を経て、私は3段階の明確な最適化レベルに辿り着きました。
レベル1:Alpine Linux – 「お手軽」な解決策
デフォルトのイメージの代わりに、Alpineを使用しましょう。これは約5MBという超軽量なディストリビューションです。
FROM golang:1.22-alpine
WORKDIR /app
COPY . .
RUN go build -o main .
CMD ["./main"]
この変更だけで、イメージは約300MBまで削減されます。かなり改善されましたが、まだGo SDKが丸ごと入っているため、最適とは言えません。
レベル2:Multi-stage Build – 考え方の革命
これは私が最も気に入っているテクニックであり、現代のあらゆるプロジェクトの標準となっています。アイデアは非常にシンプルで、パッケージ化のプロセスを2つの独立したステージに分けるというものです。
- ステージ1 (Builder): 完全なイメージを使用して、コードをバイナリファイルにコンパイルします。
- ステージ2 (Runner): そのバイナリファイルだけを、実行用の超軽量イメージにコピーします。
これにより、ソースコード、キャッシュ、コンパイラ群は最終的なイメージから完全に排除されます。
Scratchイメージ – サイズ最適化の「ラスボス」
Alpineでも満足できない場合は、scratch を使いましょう。これは完全に空のイメージ(0バイト)です。Goは自己完結型の静的バイナリをビルドできるため、実行にOS環境を必要としません。
私が適用している本番環境向けの標準Dockerfile
以下は、安全性を確保しつつ、驚くほど軽量化した私の設定例です。
# ステージ1: バイナリのビルド
FROM golang:1.22-alpine AS builder
# 必要な補助ライブラリのインストール
RUN apk update && apk add --no-cache git ca-certificates tzdata
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# サイズを最適化するフラグを付けてビルド
# CGO_ENABLED=0: 完全に静的なバイナリを作成
# -ldflags="-w -s": デバッグ情報を削除し、サイズをさらに約20%削減
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /app/server ./main.go
# ステージ2: 超軽量な実行環境
FROM scratch
# セキュリティ証明書とタイムゾーンをコピー
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /usr/share/zoneinfo /usr/share/zoneinfo
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
結果は? 最終的なイメージはわずか 12MB になりました。イメージのプルにかかる時間は、分単位から秒単位に変わります。
Scratchを使用する際に避けるべき「落とし穴」
scratch は強力ですが、実行時のエラー処理の経験がないと罠に嵌まりやすいです。
1. HTTPS接続エラー
scratch は完全に空であるため、CA証明書(CA Certificates)が含まれていません。アプリがサードパーティのAPI(StripeやAWSなど)を呼び出す必要がある場合、すぐにSSLエラーが発生します。上の例のように、builderステージから ca-certificates.crt ファイルをコピーするのを忘れないでください。
2. 暗闇の中でのデバッグ
scratch には ls も bash も curl もありません。docker exec で中に入ってファイルを確認することはできません。
私のテクニックは、すべてをJSON形式でstdoutに出力することです。システムログからエラーを確認する必要があるときは、Toolcraft의 JSON Formatterを使用して読みやすく整形しています。これは、本番コンテナにデバッグツールを無理やりインストールするよりも、はるかに高速でスマートです。
6ヶ月間の実践による成果
イメージの最適化は単なる数字遊びではありません。会社のマイクロサービス群に適用した結果、3つの大きなメリットを実感しました。
- 爆速デプロイ: コードのコミットからサーバーでアプリが動作するまでの時間が、5分から1分未満に短縮されました。
- コスト削減: AWS ECRのストレージ使用量が大幅に減り、月々のストレージコストを節約できました。
- 絶対的な安心感: シェルがないということは、万が一コンテナに侵入されたとしても、ハッカーが破壊活動を行うための基本的なツールを失うことを意味します。
Goを使用しているなら、今すぐイメージサイズの削減を試してみてください。最初は証明書の扱いに戸惑うかもしれませんが、信じてください、その結果は非常に価値のあるものです。

