•10 min read

FastAPIとDockerマルチステージビルド:イメージサイズを70%削減

FastAPIとDockerマルチステージビルド:イメージサイズを70%削減

ほとんどのPython開発者は、シンプルなDockerfileから始めます。python:3.12をプルし、要件をインストールし、FastAPIアプリをコピーして、出荷します。

問題は何でしょうか?イメージが1.2GBになってしまいます。ビルドツール、Cコンパイラ、そして本番サーバーでは決して使わない開発ヘッダーが含まれています。デプロイ時にはプルに永遠の時間がかかり、攻撃対象領域を拡大します。

Pythonアプリケーションをデプロイするなら、マルチステージDockerビルドは必須です。

このガイドでは、同じDockerfileの3つのバージョンを、それぞれ前のバージョンを基にして説明します。

  1. クラシックマルチステージ — 基本的な修正
  2. BuildKitキャッシュマウント — CIでの高速リビルド
  3. uvベース — 最速の最新アプローチ
FastAPI Docker Multistage Builds
Audio Briefing
0:00 / 0:00

シングルステージPythonビルドが失敗する理由

Pythonは、psycopg2、cryptography、lxmlのような人気ライブラリのC拡張機能をビルドするためにコンパイラを必要とします。

pip installを実行すると、多くの場合、ソースコードをダウンロードしてコンパイルします。これを機能させるには、ベースとなるDockerイメージにbuild-essentialとpython3-devが必要です。その同じステージを本番環境に出荷すると、コンパイラも出荷することになります。

ハッカーは、本番Webサーバー上でコンパイラを見つけるのが大好きです。エクスプロイトのコンパイルが非常に簡単になるからです。

Why single-stage Python builds fail
Advertisement

バージョン1:クラシックマルチステージビルド

マルチステージビルドは、Dockerfileを個別のステージに分割します。

  1. ビルダー・ステージ: 重いベースイメージ。コンパイラをインストールし、仮想環境を構築します。
  2. ランタイム・ステージ: 軽量なスリムイメージ。コンパイルされたvenvとアプリケーションコードのみをコピーします。

コンパイラが本番環境に到達することはありません。

The Multistage Solution
# Stage 1: Builder
FROM python:3.12-slim AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    libpq-dev \
    && rm -rf /var/lib/apt/lists/*

# Create a self-contained virtual environment
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Stage 2: Runtime
FROM python:3.12-slim

RUN groupadd -r appuser && useradd -r -g appuser appuser

WORKDIR /app

# Runtime-only native lib (no compiler)
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    && rm -rf /var/lib/apt/lists/*

# Transfer the fully-built venv — no build tools come with it
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY . .
RUN chown -R appuser:appuser /app
USER appuser

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

結果: 約1.2GBのイメージではなく、約150MBのイメージになります。

Alpine vs. Slim — PythonにはAlpineを使用しない

Alpineはglibcの代わりにmusl libcを使用します。多くのPythonパッケージ(numpy、pandas、cryptography)は、glibc専用のプリコンパイル済みホイールを出荷しています。Alpineでは、pipはすべてソースからコンパイルする必要があり、ビルドが劇的に遅くなり、頻繁に不明なCエラーが発生します。

常にpython:X.Y-slimを使用してください。これはDebianベースで、glibcを使用しており、本番環境に十分なほど小さいです。

非ルートが重要な理由

Dockerはデフォルトでコンテナをrootとして実行します。コンテナ内でルートアクセスを持つFastAPIプロセスが侵害されると、任意のファイルへの書き込み、ツールのインストール、共有ボリュームを介した他のコンテナへのピボットが可能になります。USER appuserの行は、プロセスが開始する前に低特権ユーザーに切り替えます。これは、PodSecurityAdmissionが強制されるKubernetes環境で必要です。

バージョン2:BuildKitキャッシュマウント(高速CIリビルド)

クラシックなマルチステージビルドでは、requirements.txtが少しでも変更されると、ビルドのたびにすべてのpipパッケージを再ダウンロードします。BuildKitキャッシュマウントはこれを解決します。

# syntax=docker/dockerfile:1.4
FROM python:3.12-slim AS builder

RUN apt-get update && apt-get install -y --no-install-recommends \
    build-essential libpq-dev \
    && rm -rf /var/lib/apt/lists/*

RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .

# --mount=type=cache persists pip's HTTP cache across builds
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

FROM python:3.12-slim
RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends libpq5 \
    && rm -rf /var/lib/apt/lists/*
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY . .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

BuildKitはDocker 23以降でデフォルトで有効になっています。古いバージョンでは、ビルド前にDOCKER_BUILDKIT=1を設定してください。

CIへの影響: ウォームキャッシュのあるGitHub Actionsでは、以前は4分30秒かかっていたフルリビルドが約45秒に短縮されます。これは、pipがコンパイル済みホイールをダウンロードしてコンパイルする代わりに、キャッシュマウントから直接読み取るためです。

バージョン3:uv — 最速の最新アプローチ

uvは、10〜100倍高速なRustベースのpip代替品です。Astralは、これを簡単に統合できるDockerイメージ(ghcr.io/astral-sh/uv)を提供しています。

# syntax=docker/dockerfile:1.4
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder

WORKDIR /app

# Install dependencies with uv — uses BuildKit cache automatically
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    uv sync --frozen --no-install-project --no-dev

# Copy application code and install the project itself
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev

# Runtime stage — no uv needed here
FROM python:3.12-slim

RUN groupadd -r appuser && useradd -r -g appuser appuser
WORKDIR /app

COPY --from=builder /app/.venv /app/.venv
COPY --from=builder /app /app

RUN chown -R appuser:appuser /app
USER appuser

ENV PATH="/app/.venv/bin:$PATH"
EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

これはrequirements.txtの代わりにpyproject.toml + uv.lockを使用しており、環境間で再現性のある固定されたビルドを提供します。

移行: まだrequirements.txtを使用している場合は、pyproject.tomlとuv.lockを生成するためにuv initとuv add $(cat requirements.txt)を実行してください。

Advertisement

イメージサイズの比較

アプローチベースイメージイメージサイズコールドビルドウォームリビルド
シングルステージpython:3.12~1.2 GB4分30秒4分30秒
マルチステージ(クラシック)python:3.12-slim~150 MB2分15秒2分15秒
マルチステージ + BuildKitキャッシュpython:3.12-slim~150 MB2分15秒~45秒
uvマルチステージ + BuildKituv:python3.12~95 MB~60秒~12秒

uvアプローチは、CI/CDのイテレーション速度とデプロイ時間にとって最も重要な2つの指標である、最小のイメージと最速のリビルドの両方を実現します。

本番環境でのUvicornワーカー

上記の例のCMDは、単一のUvicornワーカーを起動します。複数のCPUコアを持つ本番環境では、Gunicornをプロセスマネージャーとして使用します。

CMD ["gunicorn", "main:app", \
     "--workers", "4", \
     "--worker-class", "uvicorn.workers.UvicornWorker", \
     "--bind", "0.0.0.0:8000", \
     "--timeout", "120", \
     "--access-logfile", "-"]

または、Uvicornの組み込み--workersフラグを使用します(よりシンプルですが、Gunicornの方がグレースフルリスタートをうまく処理します)。

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

一般的なルールとして、I/OバウンドなFastAPIアプリには--workers = 2 × CPU cores + 1を使用します。CPUバウンドなワークロードには、ワーカー数をCPU数と正確に一致させます。

GitHub Actions CI統合

# .github/workflows/build.yml
name: Build and Push Docker Image

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
          # GitHub Actions cache backend — persists BuildKit layers between runs
          cache-from: type=gha
          cache-to: type=gha,mode=max

cache-from: type=gha / cache-to: type=gha,mode=maxのペアは、GitHub ActionsネイティブのBuildKitキャッシュです。uvのキャッシュマウントと組み合わせることで、アプリケーションコードのみが変更された場合(依存関係の変更がない場合)、CIリビルドはほぼ瞬時に行われます。

いつ何を使うべきか

シナリオ推奨事項
pyproject.toml を使用する新規プロジェクトuv マルチステージ + BuildKit
requirements.txt を使用するレガシープロジェクトクラシックマルチステージ、BuildKitキャッシュを追加
厳格なセキュリティポリシーを持つ Kubernetesどちらでも可、非ルートユーザーを確保
マルチアーキテクチャビルド (ARM + x86)buildx に --platform linux/amd64,linux/arm64 を追加

build-essentialを本番環境に出荷するのはやめましょう。クラシックなマルチステージアプローチでさえ、イメージを87%削減し、攻撃対象領域を排除します。uvに切り替えることで、さらに迅速に目標を達成できます。

こちらもおすすめ

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement
BigQuery + Cloud Run: 本番向けのサーバーレスデータ取込パイプライン構築
gcp

BigQuery + Cloud Run: 本番向けのサーバーレスデータ取込パイプライン構築

Google Cloud 上でサーバーレスなデータ取込を本番品質で構築する実践ガイド。BigQuery Storage Write API、パーティショニングとクラスタリングの設計、Cloud Run 上の非同期 FastAPI レシーバ、Terraform による IaC 全体、実測に基づくコスト分析、そして深夜3時に呼ばれる障害モードまで扱います。

Read more