あるジュニアデベロッパーが、PythonのDockerイメージを1.2GBから85MBに削減し、CIパイプラインの実行時間を11分から90秒へと短縮しました。イメージが小さくなると、ダウンロードが速くなり、ストレージコストが抑えられ、セキュリティ上の脆弱性も減少します。

なぜイメージサイズが重要なのか

コンテナがプルされるたびに、レジストリはイメージ全体をストリーミングします。85MBのレイヤーなら数秒で済みますが、1.2GBのレイヤーだと、ネットワーク環境によっては数分かかることもあります。また、ビルドエージェントはフルイメージのアップロードとキャッシュを行う必要があり、これがCIの時間増加やクラウドストレージ料金の増大につながります。パッケージが増えるほど潜在的な脆弱性が増えるため、ベースを削ることで攻撃対象領域(アタックサーフェス)を縮小できます。

肥大化の原因

  • gcc などのビルドツールが、アプリを実行するのと同じステージでインストールされていると、最終的なイメージに残ってしまいます。
  • パッケージマネージャーのキャッシュ(例:aptpip のキャッシュ)がディスク上に残り、デフォルトではクリーンアップされません。
  • すべての RUN 命令は新しい読み取り専用レイヤーを作成します。レイヤー間でファイルが重複すると、サイズが蓄積していきます。
  • ubuntu:latest のような大きなベースイメージにはフルOSが含まれており、最小限のPythonランタイムが必要とするものよりもはるかに巨大です。

3ステップの削減プロセス

ステップ ベースイメージ ビルド手法 結果のサイズ
1 標準的なPython (full) シングルステージ、すべてのツールが含まれる 1.18 GB
2 python:slim マルチステージ:gccを含むbuilder、最終ステージでコンパイル済みパッケージのみをコピー 210 MB
3 python:alpine Alpine Linux上でのマルチステージ(Alpine自体が非常に軽量) 85 MB

ステップ 1 – ベースライン

デフォルトのPythonイメージから始めたところ、デベロッパーは1.18GBのアーティファクトを得ました。このイメージには、フルDebianスタック、開発用ヘッダー、およびpipキャッシュが含まれていました。

ステップ 2 – builderを用いたslim

python:slim に切り替えることでOSのフットプリントは削減されましたが、ビルドツールは残ったままでした。builder ステージを追加することで、gcc、make、その他のコンパイル時の依存関係をインストール・コンパイルし、その後削除することが可能になりました。最終ステージでは COPY --from=builder を使用して、コンパイル済みのwheelとランタイムファイルのみを取り込み、サイズを210MBまで落としました。

ステップ 3 – Alpineの勝利

Alpine Linuxはmusl libcとbusyboxをベースに構築されています。Alpine上でマルチステージのパターンを繰り返した結果、85MBのイメージが生成されました。これはオリジナルから93%の削減となります。デベロッパーによると、Kubernetesでのイメージプルは数秒で完了し、CIジョブは90秒で終了したとのことです。

実用上のインパクト

  • ストレージコストの削減 – レジストリのストレージ容量が縮小します。
  • セキュリティの向上 – パッケージが少なければ、追跡すべきCVEも少なくなります。Alpineイメージには、Pythonランタイムとアプリケーションコードのみが含まれています。
  • CIの高速化 – パイプラインの実行時間が11分から90秒に短縮されました。

Dockerfileを軽量化するための実践的なヒント

  • :latest タグの使用を避け、ニーズに合った :slim:alpine バリアントを選択してください。
  • マルチステージビルドを活用しましょう。コンパイル用の専用の builder ステージと、実際に必要なアーティファクトのみを受け取る runtime ステージに分けます。
  • COPY --from=builder /path/to/installed /path/in/final を使用して、選択的にコピーします。
  • 依存関係のインストールがソースコードのコピーより前に行われるようにコマンドの順序を整理します。これにより、レイヤーキャッシュを最大限に活用できます。
  • rm -rf /var/lib/apt/lists/* ~/.cache/pip のように、明示的にキャッシュを削除します。

Alpineを採用する場合、アプリでDNS解決の問題が発生したときは nss パッケージを追加してください。pipでインストールする際は、ローカルにインストールされたスクリプトが実行時に見つかるよう、PATH の先頭に /root/.local/bin を追加してください。

注意事項

Alpineのmusl libcは、glibc向けにコンパイルされたバイナリwheelと衝突し、実行時エラーを引き起こす可能性があります。そのような場合は、Alpineのbuilder内でwheelを再ビルドするか、slim ベースにフォールバックしてください。DNSの信頼性を確保するために nss パッケージを追加することは、小さなコストで済みます。

次に注目すべきこと

  • 既存のイメージをスキャンし、マルチステージへの書き換え候補となる大きなレイヤーがないか確認しましょう。
  • CIログを監視して、アップロードおよびダウンロード時間からイメージの肥大化を検知しましょう。
  • 選択したベースディストリビューションの脆弱性レポートに注意を払いましょう。

教訓は明白です。規律あるDockerfile、軽量なベース、そしてbuilderステージを活用することで、Pythonイメージを桁違いに縮小でき、機能を損なうことなく、目に見えるスピードとコストのメリットをもたらすことができます。