マルチステージビルドでPythonアプリケーションを軽量化

PSSLの佐々木です。

今回は、Dockerのマルチステージビルドを使ってPythonアプリケーションのサイズを削減する方法を解説します。

JavaやGoのようなコンパイル言語であればビルド時と実行が明確に分かれており、実行時にはバイナリだけあればよいのでマルチステージビルドと相性よく組み合わせて容量を削減できるというのは非常にわかりやすいと思いますが、PythonやRubyのよなインタプリター言語の場合には効果があるのかないのかいまいちピンとこなかったので自身の検証もかねてブログにまとめました。

そもそもマルチステージビルドって何?

マルチステージビルドとは、1つのDockerfile内で複数の段階(ステージ)に分けてイメージを構築する手法です。

従来の問題

  • Pythonパッケージをビルドするためにgccやg++が必要
  • しかし、実行時にはビルドツールは不要
  • でも、従来は最終イメージにビルドツールが残ってしまう

マルチステージビルドの解決策

  1. ビルドステージ:必要なパッケージをコンパイル・ビルド
  2. 実行ステージ:ビルド結果だけを持ってきて軽量なイメージを作成

実際のDockerfileで比較してみよう

❌ 従来のシングルステージビルド

# シングルステージビルド用Dockerfile
FROM python:3.13-slim

# ビルド時に必要なパッケージをインストール
RUN apt-get update && apt-get install -y \\
    gcc \\
    g++ \\
    && rm -rf /var/lib/apt/lists/*

# 非rootユーザーを作成(ホームディレクトリも作成)
RUN groupadd -r appuser && useradd -r -g appuser -m appuser

# 作業ディレクトリを作成
WORKDIR /app

# 依存関係ファイルをコピー
COPY requirements.txt .

# 依存関係をインストール
RUN pip install --no-cache-dir --upgrade pip && \\
    pip install --no-cache-dir -r requirements.txt

# アプリケーションコードをコピー
COPY . .

# 不要なファイルとディレクトリを削除
RUN find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true && \\
    find . -type f -name "*.pyc" -delete && \\
    rm -rf venv/ && \\
    rm -rf .git/ && \\
    rm -rf .pytest_cache/ && \\
    rm -rf *.egg-info/ && \\
    rm -rf .coverage && \\
    rm -rf htmlcov/

# アプリケーションディレクトリの所有者を変更
RUN chown -R appuser:appuser /app

# promptflowが使用するディレクトリを作成
RUN mkdir -p /home/appuser/.promptflow && \\
    chown -R appuser:appuser /home/appuser/.promptflow

# 非rootユーザーに切り替え
USER appuser

# 環境変数でホームディレクトリを明示的に設定
ENV HOME=/home/appuser

# ポートを公開
EXPOSE 5000

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
    CMD python -c "import urllib.request; urllib.request.urlopen('<http://localhost:5000/api/health>')" || exit 1

# アプリケーションを実行
CMD ["python", "app.py"]

✅ マルチステージビルド

# マルチステージビルド用Dockerfile
# ステージ1: ビルドステージ
FROM python:3.13-slim AS builder

# ビルド時に必要なパッケージをインストール
RUN apt-get update && apt-get install -y \\
    gcc \\
    g++ \\
    && rm -rf /var/lib/apt/lists/*

# 作業ディレクトリを作成
WORKDIR /build

# 依存関係ファイルをコピー
COPY requirements.txt .

# 依存関係をインストール(グローバルにインストール)
RUN pip install --no-cache-dir --upgrade pip && \\
    pip install --no-cache-dir -r requirements.txt

# アプリケーションコードをコピー
COPY . .

# 不要なファイルとディレクトリを削除
RUN find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true && \\
    find . -type f -name "*.pyc" -delete && \\
    rm -rf venv/ && \\
    rm -rf .git/ && \\
    rm -rf .pytest_cache/ && \\
    rm -rf *.egg-info/ && \\
    rm -rf .coverage && \\
    rm -rf htmlcov/

# ステージ2: 実行ステージ
FROM python:3.13-slim AS runtime

# 実行時に必要な最小限のパッケージをインストール
RUN apt-get update && apt-get install -y \\
    && rm -rf /var/lib/apt/lists/*

# 非rootユーザーを作成(ホームディレクトリも作成)
RUN groupadd -r appuser && useradd -r -g appuser -m appuser

# 作業ディレクトリを作成
WORKDIR /app

# ビルドステージからPythonパッケージをコピー
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin

# アプリケーションコードをコピー
COPY --from=builder /build/app.py .
COPY --from=builder /build/connection.yaml .
COPY --from=builder /build/flows ./flows/
COPY --from=builder /build/services ./services/

# アプリケーションディレクトリの所有者を変更
RUN chown -R appuser:appuser /app

# promptflowが使用するディレクトリを作成
RUN mkdir -p /home/appuser/.promptflow && \\
    chown -R appuser:appuser /home/appuser/.promptflow

# 非rootユーザーに切り替え
USER appuser

# 環境変数でホームディレクトリを明示的に設定
ENV HOME=/home/appuser

# ポートを公開
EXPOSE 5000

# ヘルスチェック
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
    CMD python -c "import urllib.request; urllib.request.urlopen('<http://localhost:5000/api/health>')" || exit 1

# アプリケーションを実行
CMD ["python", "app.py"]

マルチステージビルドの3つのメリット

1. サイズ削減

不要なビルドツールや中間ファイルが除去されるため、イメージサイズが大幅に小さくなります。

2. セキュリティ向上

実行時に不要なツール(gcc、コンパイラなど)が含まれないため、攻撃面が減少します。

3. デプロイ高速化

イメージが軽量になることで、レジストリへのpush/pullが高速化されます。

実装のポイント

AS句でステージに名前をつける

FROM python:3.13-slim AS builder  # ← 「builder」という名前
FROM python:3.13-slim AS runtime  # ← 「runtime」という名前

COPY –fromで必要なファイルだけを移動

# builderステージからsite-packagesだけをコピー
COPY --from=builder /usr/local/lib/python3.13/site-packages /usr/local/lib/python3.13/site-packages

不要ファイルの削除

RUN find . -name "__pycache__" -exec rm -rf {} + && \\
    find . -name "*.pyc" -delete

開発環境での使い方

実際にマルチステージビルドを開発で使う場合は、Docker Composeと組み合わせるのがおすすめです。

services:
  app:
    build: .  # マルチステージDockerfileを使用
    ports:
      - "5000:5000"
    volumes:
      - .:/app  # 開発時はコードをマウント
    environment:
      - FLASK_DEBUG=1

実際にどれくらい効果があるの?

筆者が実際にPythonアプリケーション(promptflowやFlaskを使用)で試した結果がこちらです:

REPOSITORY                    TAG      IMAGE ID       CREATED         SIZE
my-app-single                latest   99aa62f1267a   1 minute ago    1.45GB
my-app-multi                 latest   27b56911a385   6 minutes ago   636MB

約810MB(56%)の削減に成功しています。

この差は特に以下のような場面で威力を発揮します:

  • CI/CDパイプライン:デプロイ時間の短縮
  • 本番環境:ストレージコストの削減
  • 開発チーム:イメージ共有の高速化

なぜPythonでもマルチステージビルドの効果があるのか

記事の頭でも気になっていたインタプリター言語でもなぜマルチステージビルドで容量を削減できるのか少し調べてみました。

Pythonのエコシステムでは結構コンパイル処理が発生しているようで、C / C++で書かれている部分を含んでいることが要因の一つでした。

具体的には以下のようなライブラリはC拡張コンパイルをしているようです。

numpy      # 線形代数計算(BLAS/LAPACK)
pandas     # データ処理の高速化
pillow     # 画像処理
lxml       # XML/HTMLパーサー
psycopg2   # PostgreSQLドライバー
cryptography # 暗号化ライブラリ
uwsgi      # WSGIサーバー

それによって作成される以下のような不要なファイルやコンパイルツールを含めずにイメージを作成することができるためマルチステービルドの恩恵をしっかりと受けられているのだと思います。

  • gcc/g++とその依存関係
  • apt-getのキャッシュ:
  • ビルド時の一時ファイル
  • その他のビルドツール

まとめ

マルチステージビルドは、少しの工夫で大きな効果を得られる優秀な技術です。コンパイル言語ほどではないですが、Pythonのようなインタプリター言語でもマルチステージビルドの効果を確認することができました。

またアプリケーション実行に最小の要素が何なのかわからないと構築が難しいですが、明らかに必要のないものを含めない状態でイメージを作成するだけでも効果があるので積極的に取り入れて段階的に最適化させていくのが良いかなと思いました。

ではまた

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です