Docker デプロイ — コンテナ化とマルチステージビルド
Dockerは海上コンテナのようなもの - アプリケーションとそのすべての依存関係を標準コンテナにパッケージ化し, どの港 (サーバー)でもそのまま荷下ろして実行でき, 「自分の環境では動く」という問題を解決します。
1. 学ぶ内容
- マルチステージDockerfile:ビルドステージ (UV依存関係のインストール)vs ランタイムステージ (イメージの軽量化)
- Docker Compose オーケストレーション:FastAPI + PostgreSQL + Redis + Celery Worker の完全スタック
- 環境変数とシークレット管理:
.envファイルとDocker Secrets - ヘルスチェック:
HEALTHCHECKコマンドとFastAPI/healthエンドポイント - Aliceのシナリオ:Charlieが
docker compose up一発でPriceTrackerフルスタックサービスを起動
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) ビルドプロセス
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) 完全スタックアーキテクチャ
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でログを集約します。📖 まとめ
- マルチステージDockerfile:Builderステージで依存関係をインストールし, Runtimeステージでは結果のみをコピーして, イメージサイズを1.2 GBから200 MBに削減
- Docker Composeによるフルスタックオーケストレーション:API + PostgreSQL + Redis + Celery Worker + Flower
depends_on+healthcheckにより次の起動順序を保証:データベースが先に準備完了, 続いてAPI.envファイルで環境変数を管理, 本番環境ではDocker secretsを使用- ヘルスチェックはエンドポイントのデータベースとRedisの接続状況を確認,
HEALTHCHECKコマンドで自動検出
📝 練習問題
- 基本問題 (難易度 ⭐):PriceTrackerのシングルステージDockerfileを作成し, イメージをビルドしてコンテナを実行し,
/healthエンドポイントにアクセスできることを確認してください。ヒント:FROM python:3.12-slim+CMD ["uvicorn", ...] - 応用問題 (難易度 ⭐⭐):Dockerfileをマルチステージビルドに変更し, API, PostgreSQL, Redisの3つのサービスを含む
docker-compose.ymlを作成して,docker compose upで一発起動できるようにしてください。ヒント:AS builder+COPY --from=builder - チャレンジ (難易度 ⭐⭐⭐):Docker Composeオーケストレーションを完成させる - Celery WorkerとFlowerサービスを追加し, ヘルスチェックを設定し,
.envファイルにパスワードを設定し, 起動前にalembic upgrade headを実行するentrypoint.shを追加してください。ヒント:depends_on+condition: service_healthy+ entrypointスクリプト
---|



