あるジュニアデベロッパーが、PythonのDockerイメージを1.2GBから85MBに削減し、CIパイプラインの実行時間を11分から90秒へと短縮しました。イメージが小さくなると、ダウンロードが速くなり、ストレージコストが抑えられ、セキュリティ上の脆弱性も減少します。
なぜイメージサイズが重要なのか
コンテナがプルされるたびに、レジストリはイメージ全体をストリーミングします。85MBのレイヤーなら数秒で済みますが、1.2GBのレイヤーだと、ネットワーク環境によっては数分かかることもあります。また、ビルドエージェントはフルイメージのアップロードとキャッシュを行う必要があり、これがCIの時間増加やクラウドストレージ料金の増大につながります。パッケージが増えるほど潜在的な脆弱性が増えるため、ベースを削ることで攻撃対象領域(アタックサーフェス)を縮小できます。
肥大化の原因
- gcc などのビルドツールが、アプリを実行するのと同じステージでインストールされていると、最終的なイメージに残ってしまいます。
- パッケージマネージャーのキャッシュ(例:
aptやpipのキャッシュ)がディスク上に残り、デフォルトではクリーンアップされません。 - すべての
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イメージを桁違いに縮小でき、機能を損なうことなく、目に見えるスピードとコストのメリットをもたらすことができます。
