•8 min read

DockerにおけるAstral uv: マルチステージビルド、BuildKitキャッシュ、高速CI

DockerにおけるAstral uv: マルチステージビルド、BuildKitキャッシュ、高速CI

Pythonコンテナを構築するすべての開発者は、Dockerレイヤーの無効化によるフィードバックループの遅さを経験しています。アプリケーションコードを1行変更し、docker buildを実行すると、PyTorch、Pandas、または重いC拡張機能をPyPIから再ダウンロードするために、ターミナルが4分間も費やすのを目にすることになります。

AstralのRustベースのパッケージマネージャーuvとDocker BuildKitキャッシュマウントを使用すれば、再ビルド時間を2秒未満に短縮できます。

ビルド比較の概要:

Audio Briefing
0:00 / 0:00
戦略コールドビルド時間コードのみの再ビルド最終イメージサイズCVE攻撃対象領域
pip install -r reqs.txt約65秒約45秒(無効化された場合)約850MB高(コンパイラが残る)
poetry install約110秒約60秒約920MB高(poetryランタイムが残る)
uvマルチステージ + BuildKit約4.5秒約1.2秒約140MB最小限(純粋な仮想環境)
Optimized Python Docker Builds with uv

従来のPython Dockerfileの3つの欠点

ほとんどのプロダクションDockerfileには、構造的なパフォーマンスの問題が3つあります。

  1. コンパイラが最終イメージに残る: シングルステージのDockerfile内でapt-get install gcc build-essentialを実行すると、コンパイラ、ヘッダーファイル、パッケージマネージャーのキャッシュがプロダクションコンテナ内に残り、イメージサイズが1GBを超えて肥大化します。
  2. レイヤーキャッシュのスラッシング: 依存関係をインストールする前にCOPY . .を配置すると、コミットごとに依存関係キャッシュが無効になります。
  3. CIでの永続的なホイールキャッシュがない: 新しいGitHub Actionsランナーでdocker buildを実行すると、プルリクエストごとにPyPIからすべてのホイールが再ダウンロードされます。

解決策は、公式のスタンドアロンuvバイナリとBuildKit永続キャッシュマウントを使用したクリーンなマルチステージパイプラインです。


Advertisement

フェーズ1: スタンドアロンのuvバイナリの取得

RUN pip install uvを実行する(Pythonが必要で不要なレイヤーを追加する)代わりに、コンパイル済みのRustバイナリをAstralの公式イメージから直接コピーします。

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

# Copy the standalone uv binary directly (sub-millisecond copy)
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

これにより、ビルドオーバーヘッドなしでuvがすぐに利用可能になります。


フェーズ2: 2段階の依存関係同期

ソースコードの編集によって依存関係のダウンロードが再実行されるのを防ぐため、インストールを2つの異なるステージに分割します。

WORKDIR /app

# Enable bytecode compilation and standalone copy linking
ENV UV_COMPILE_BYTECODE=1
ENV UV_LINK_MODE=copy

# Step A: Copy ONLY lockfiles first
COPY pyproject.toml uv.lock ./

# Step B: Install dependencies without installing the root project
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev --no-install-project

--no-install-projectに注目してください。これはuvに対し、アプリケーションのソースコードがまだ存在しないことを前提として、仮想環境を構築し、すべてのサードパーティの依存関係をダウンロードするように指示します。

次に、ソースコードをコピーして最終同期を実行します。

# Step C: Copy application source code
COPY src/ ./src/

# Step D: Fast sync to install your project into the existing virtualenv
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev

src/main.pyを編集すると、DockerはステップBのキャッシュされたレイヤーを再利用します。すべてのホイールがすでに.venvに存在するため、ステップDは300ミリ秒未満で実行されます。


フェーズ3: 軽量なプロダクションランナー

ビルダー段階には、uv、パッケージメタデータ、および中間ビルドツールが含まれています。これらはどれもプロダクションには不要です。

ステージ2では、クリーンなpython:3.12-slimベースから開始し、非ルートユーザーを作成し、.venvディレクトリのみをコピーします。

FROM python:3.12-slim AS runner

# Create non-root system user
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

WORKDIR /app

# Copy the pre-built virtual environment from builder
COPY --from=builder --chown=appuser:appgroup /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appgroup /app/src /app/src

# Put virtualenv binaries at the front of PATH
ENV PATH="/app/.venv/bin:$PATH"

USER appuser

EXPOSE 8000

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

PATHが/app/.venv/binを指しているため、uvicornを実行すると、python -mやアクティベーションスクリプトを必要とせずに、仮想環境内のバイナリが直接実行されます。

このコンテナ内で実行するASGIスタックを選択する場合は、FastAPI vs Litestar: Performance & Memoryに関するベンチマーク比較をご覧ください。プロダクション負荷の下で非同期ワーカーがOOMクラッシュなしで安定稼働するようにするには、プロダクションでの非同期Pythonメモリリークのプロファイリングに関するガイドと組み合わせてください。


Advertisement

完全なコピペ可能なプロダクションDockerfile

以下は、プロダクション対応の完全で堅牢なマルチステージDockerfileです。

# syntax=docker/dockerfile:1.7
# Stage 1: Build virtual environment
FROM python:3.12-slim AS builder

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

WORKDIR /app

# Optimize uv execution inside containers
ENV UV_COMPILE_BYTECODE=1 \
    UV_LINK_MODE=copy

# 1. Cache third-party dependencies independently of source code
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev --no-install-project

# 2. Copy source code and perform final project sync
COPY . .
RUN --mount=type=cache,target=/root/.cache/uv \
    uv sync --frozen --no-dev

# Stage 2: Final minimal production runtime
FROM python:3.12-slim AS runner

RUN groupadd -r appuser && useradd -r -g appuser -d /app -s /sbin/nologin appuser

WORKDIR /app

# Copy only the compiled virtualenv and application source
COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv
COPY --from=builder --chown=appuser:appuser /app/src /app/src

ENV PATH="/app/.venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

USER appuser

EXPOSE 8000

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

GitHub ActionsでのBuildKitキャッシュマウントの永続化

ローカルマシンでは、Docker BuildKitはビルド間で/root/.cache/uvを自動的にキャッシュします。一時的なGitHub Actionsランナーでは、ランナーが終了するとそのキャッシュは消滅します。

CI実行間でuvキャッシュを永続化するには、DockerのGitHub Actionsキャッシュバックエンドを設定します(リモートキャッシュタイプとレジストリエクスポーターについては、Docker BuildKitキャッシュマウントとマルチステージ最適化に関する詳細な解説をご覧ください)。

name: Docker Build & Push

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: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          cache-from: type=gha
          cache-to: type=gha,mode=max

cache-to: type=gha,mode=maxを追加することで、BuildKitは永続的な/root/.cache/uvマウントをGitHubのキャッシュストレージにエクスポートします。その後のPRビルドでは、ネットワークに触れることなくキャッシュされたホイールを直接プルします。


経験則

通常のアプリケーション開発中にDockerビルドが依存関係の解決に5秒以上費やしている場合、レイヤーキャッシュの境界が早すぎます。

ロックファイルのみをコピーし、--no-install-projectで同期し、/root/.cache/uvをマウントし、最終的なランナーには.venvディレクトリのみをコピーしてください。

こちらもおすすめ

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