Docker マルチステージビルドでイメージサイズを削る際の落とし穴
要点
マルチステージビルドはイメージサイズを削減する有効な手法だが、レイヤーキャッシュの構造を理解せずに使うと、ビルド時間の悪化という別の代償を払うことになる。
サイズが膨らむ典型的な原因
単一ステージの Dockerfile では、ビルドツールや中間成果物がそのまま最終イメージに残る。Docker 公式ドキュメントの multi-stage builds の章が示す通り、マルチステージビルドはビルド用ステージと実行用ステージを分離し、実行に不要なファイルを最終イメージから排除する仕組みである。
キャッシュを壊す典型例
ただし、ステージを分けただけでは効果が半減することがある。よくある失敗は、依存関係のインストールとアプリケーションコードのコピーを同じレイヤーにまとめてしまうことだ。コードが一行変わるたびに依存関係の再インストールが走り、BuildKit のキャッシュが機能しなくなる。依存定義ファイルだけを先にコピーしてインストールを実行し、その後にアプリケーションコードをコピーする順序にすることで、コード変更時も依存関係のレイヤーはキャッシュから再利用される。
ベースイメージと distroless の選択
実行ステージのベースイメージを distroless にすると、シェルやパッケージマネージャーが含まれないためサイズと攻撃面が縮小する。distroless プロジェクトの公表資料でも、実行時に不要なツール群を排除することが主要な設計目標として挙げられている。一方で、コンテナ内でのデバッグ手段が制限されるという運用上のトレードオフがあり、ログ出力の設計を先に固めておく必要がある。CI のキャッシュ設計とビルドイメージの設計は独立した問題に見えて、実際には同じキャッシュ肥大という根を持つことが多い。インフラをセルフホストする場合はレジストリのストレージコストも無視できない要素になる。
サイズ削減とビルド時間の両方を測定しながら、レイヤー順序とベースイメージを個別に最適化していくのが、遠回りに見えて最も確実な方法である。