GoアプリのDocker化:イメージサイズを800MBから10MBに削減する極意

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

「肥大化した」コンテナとデプロイ速度の悩み

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を占めていますが、使われることはありません。
  • セキュリティの脆弱性: プリインストールされたソフトウェアが多いほど、攻撃対象領域(アタックサーフェス)が広がります。ハッカーは、権限昇格のために curlapt が最初から入っているコンテナを好みます。

「軽量化」戦略:アマチュアからプロ級まで

本番プロジェクトでの試行錯誤を経て、私は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 には lsbashcurl もありません。docker exec で中に入ってファイルを確認することはできません。

私のテクニックは、すべてをJSON形式でstdoutに出力することです。システムログからエラーを確認する必要があるときは、Toolcraft의 JSON Formatterを使用して読みやすく整形しています。これは、本番コンテナにデバッグツールを無理やりインストールするよりも、はるかに高速でスマートです。

6ヶ月間の実践による成果

イメージの最適化は単なる数字遊びではありません。会社のマイクロサービス群に適用した結果、3つの大きなメリットを実感しました。

  • 爆速デプロイ: コードのコミットからサーバーでアプリが動作するまでの時間が、5分から1分未満に短縮されました。
  • コスト削減: AWS ECRのストレージ使用量が大幅に減り、月々のストレージコストを節約できました。
  • 絶対的な安心感: シェルがないということは、万が一コンテナに侵入されたとしても、ハッカーが破壊活動を行うための基本的なツールを失うことを意味します。

Goを使用しているなら、今すぐイメージサイズの削減を試してみてください。最初は証明書の扱いに戸惑うかもしれませんが、信じてください、その結果は非常に価値のあるものです。

Share: