404 Not Found

404 Not Found


nginx

Docker デプロイ — コンテナ化とマルチステージビルド

Dockerは海上コンテナのようなもの - アプリケーションとそのすべての依存関係を標準コンテナにパッケージ化し, どの港 (サーバー)でもそのまま荷下ろして実行でき, 「自分の環境では動く」という問題を解決します。

1. 学ぶ内容


2. Aliceの実話

(1) 悩み:環境差異によるデプロイ失敗

Charlieがローカルで設定したPriceTrackerを本番サーバーにデプロイした際, Pythonバージョンの不一致, libpqライブラリの欠落, UVの未インストール, PostgreSQL接続設定の差異などの問題に直面し, 毎回2時間かけて手動でトラブルシューティングしなければなりませんでした。さらに悪いことに, FastAPI, PostgreSQL, Redis, Celeryの4つのサービスを個別に起動・管理する必要があり, 起動順序は複雑で相互依存していました。

(2) Docker Composeによる解決策

Dockerfileはアプリケーションコンテナを定義し, Docker Composeはすべてのサービスとその依存関係をオーケストレーションします。docker compose upコマンド1つでサービススタック全体を起動でき, 環境の一貫性はイメージによって保証されます。

(3) 成果

デプロイ時間は2時間の手動作業からワンクリック30秒に短縮され, ローカル開発環境は本番と同一になったため, Charlieはもう「自分のサーバーでは動く」と言う必要がなくなりました。


3. マルチステージDockerfile

(1) ビルドプロセス

100%
flowchart LR
    A[Builder Stage] -->|Copy installed deps| B[Runtime Stage]
    
    subgraph Builder
        A1[UV install deps] --> A2[Compile wheels]
    end
    
    subgraph Runtime
        B1[Python slim image] --> B2[Copy app code]
        B2 --> B3[Copy deps from builder]
        B3 --> B4[Run uvicorn]
    end

(1) ▶ サンプル:マルチステージDockerfile

DOCKERFILE
# === Stage 1: Builder ===
FROM ghcr.io/astral-sh/uv:python3.12-bookworm-slim AS builder

WORKDIR /app

# 依存関係ファイルを先にコピー (キャッシュレイヤー)
COPY pyproject.toml uv.lock ./

# 仮想環境に依存関係をインストール
RUN uv sync --frozen --no-dev --no-install-project

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

# === Stage 2: Runtime ===
FROM python:3.12-slim-bookworm AS runtime

WORKDIR /app

# ランタイムのシステム依存関係をインストール
RUN apt-get update && \
    apt-get install -y --no-install-recommends libpq5 && \
    rm -rf /var/lib/apt/lists/*

# Builderから仮想環境をコピー
COPY --from=builder /app/.venv /app/.venv

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

# 環境変数の設定
ENV PATH="/app/.venv/bin:$PATH" \
    PYTHONDONTWRITEBYTECODE=1 \
    PYTHONUNBUFFERED=1

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

# アプリケーションの実行
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]

出力:

TEXT
// 実行成功

(2) イメージサイズの比較

方式 イメージサイズ 説明
シングルステージ (Python 3.12) ~1.2 GB ビルドツールとキャッシュを含む
マルチステージ (Python: 3.12-slim) ~200 MB ランタイム依存関係のみ
Alpine (python:3.12-alpine) ~80MB より小さいが互換性の問題あり

4. Docker Compose オーケストレーション

(1) 完全スタックアーキテクチャ

100%
flowchart TD
    Nginx[Nginx Reverse Proxy] --> API1[FastAPI Worker 1]
    Nginx --> API2[FastAPI Worker 2]
    API1 --> PG[(PostgreSQL)]
    API2 --> PG
    API1 --> Redis[(Redis)]
    API2 --> Redis
    API1 --> Broker[Redis Broker]
    API2 --> Broker
    Broker --> CW1[Celery Worker 1]
    Broker --> CW2[Celery Worker 2]
    CW1 --> PG
    CW2 --> PG
    CW1 --> Redis
    CW2 --> Redis
    Flower[Flower Monitor] --> Broker

(1) ▶ サンプル:docker-compose.yml

YAML
version: "3.8"

services:
  api:
    build:
      context: .
      dockerfile: docker/Dockerfile
    ports:
      - "8000:8000"
    environment:
      - DATABASE_URL=postgresql+asyncpg://pricetracker:${DB_PASSWORD}@postgres:5432/pricetracker
      - REDIS_URL=redis://redis:6379/0
      - CELERY_BROKER_URL=redis://redis:6379/1
      - SECRET_KEY=${SECRET_KEY}
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
      interval: 30s
      timeout: 10s
      retries: 3

  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: pricetracker
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: pricetracker
    volumes:
      - postgres_data:/var/lib/postgresql/data
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U pricetracker"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  celery-worker:
    build:
      context: .
      dockerfile: docker/Dockerfile
    command: celery -A app.core.celery_app worker --loglevel=info --concurrency=4
    environment:
      - DATABASE_URL=postgresql+asyncpg://pricetracker:${DB_PASSWORD}@postgres:5432/pricetracker
      - REDIS_URL=redis://redis:6379/0
      - CELERY_BROKER_URL=redis://redis:6379/1
    depends_on:
      - redis
      - postgres

  flower:
    build:
      context: .
      dockerfile: docker/Dockerfile
    command: celery -A app.core.celery_app flower --port=5555
    ports:
      - "5555:5555"
    depends_on:
      - redis

volumes:
  postgres_data:
  redis_data:

出力:

TEXT
CONTAINER ID   IMAGE          STATUS         PORTS
abc123         nginx:latest   Up 2 hours     0.0.0.0:80->80/tcp

(2) ▶ サンプル:.envファイル

INI
# .env - Docker Composeはこれを自動的に読み取ります
DB_PASSWORD=change-me-in-production
SECRET_KEY=your-very-long-random-secret-key-at-least-32-chars
CELERY_BROKER_URL=redis://redis:6379/1

出力:

TEXT
// 実行成功

5. ヘルスチェックと起動順序

(1) depends_on + healthcheck

Docker Composeのdepends_on + condition: service_healthyにより, 正しいサービス起動順序が保証されます:PostgreSQLとRedisが先に準備完了し, その後FastAPIが起動します。

サービス 起動順序 ヘルスチェック
PostgreSQL 1 (最初) pg_isready -U pricetracker
Redis 1 (最初) redis-cli ping
FastAPI 2 (PG+Redis準備完了後) GET /health
Celery Worker 3 (RedisとPostgreSQL準備完了後) 内部ハートビート

(1) ▶ サンプル:FastAPIヘルスチェックエンドポイント

PYTHON
@app.get("/health")
async def health_check(db: AsyncSession = Depends(get_db), redis: Redis = Depends(get_redis)):
    # データベース接続の確認
    try:
        await db.execute(select(1))
        db_status = "healthy"
    except Exception:
        db_status = "unhealthy"

    # Redis接続の確認
    try:
        await redis.ping()
        redis_status = "healthy"
    except Exception:
        redis_status = "unhealthy"

    overall = "healthy" if db_status == "healthy" and redis_status == "healthy" else "unhealthy"
    return {
        "status": overall,
        "database": db_status,
        "redis": redis_status,
    }

出力:

TEXT
# 関数定義成功

❓ よくある質問

Q なぜマルチステージビルドを使うのですか?
A ビルドステージでは完全なPythonイメージ (コンパイラを含む)を使用し, ランタイムではスリムイメージを使用します。最終イメージにはビルドツールが含まれず, サイズは5〜10分の1になり, 攻撃面も小さくなります。
Q Docker Composeは本番環境に適していますか?
A 中小規模プロジェクトには適しています。大規模な本番環境ではKubernetesやDocker Swarmを使用してください。Composeは開発, テスト, 小規模デプロイに適しています。
Q データベースマイグレーションはどう処理しますか?
A APIコンテナ起動前にマイグレーションを実行します。initコンテナを追加するか, entrypoint.shでuvicorn起動前にalembic upgrade headを実行します。
Q シークレットはどのように安全に受け渡しますか?
A 開発では.envファイルを使用し (.gitignoreに追加), 本番ではDocker secretsやK8s Secretsを使用します。ハードコードやGitへのコミットは絶対に避けてください。
Q Worker数はどのように設定しますか?
A 式:(2 x CPUコア数) + 1。4コアサーバーの場合は9ワーカーに設定します。ただし, メモリ制限も考慮する必要があります。各ワーカーは50〜100 MBを使用します。
Q コンテナログはどう確認しますか?
A docker compose logs apiでFastAPIログを確認, docker compose logs -fでリアルタイム監視します。本番環境ではELK/Lokiでログを集約します。

📖 まとめ


📝 練習問題

  1. 基本問題 (難易度 ⭐):PriceTrackerのシングルステージDockerfileを作成し, イメージをビルドしてコンテナを実行し, /healthエンドポイントにアクセスできることを確認してください。ヒント:FROM python:3.12-slim + CMD ["uvicorn", ...]
  2. 応用問題 (難易度 ⭐⭐):Dockerfileをマルチステージビルドに変更し, API, PostgreSQL, Redisの3つのサービスを含むdocker-compose.ymlを作成して, docker compose upで一発起動できるようにしてください。ヒント:AS builder + COPY --from=builder
  3. チャレンジ (難易度 ⭐⭐⭐):Docker Composeオーケストレーションを完成させる - Celery WorkerとFlowerサービスを追加し, ヘルスチェックを設定し, .envファイルにパスワードを設定し, 起動前にalembic upgrade headを実行するentrypoint.shを追加してください。ヒント:depends_on + condition: service_healthy + entrypointスクリプト

---|

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%