はじめに
FastAPIのプロジェクトがある程度形になってきたので、Docker化した。
Dockerfileやcompose.yamlの書き方自体はPHPプロジェクトと大きく変わらないが、仮想環境(venv)をどう扱うかで迷った。PHPにはvenvという概念がないので、ここだけ設計を考える必要があった。
また、PythonはPHPと違ってビルトインのWebサーバーがないので、uvicornのプロセス管理についても整理した。
PHPプロジェクトとの構成比較
まず全体像を把握するために並べて比較した。
# PHPプロジェクト(Laravel)
myapp/
├── Dockerfile
├── compose.yaml
├── nginx/
│ └── default.conf
├── php/
│ └── php.ini
├── app/
│ ├── Http/
│ └── Models/
├── composer.json
└── composer.lock
# Pythonプロジェクト(FastAPI)
myapp/
├── Dockerfile
├── compose.yaml
├── pyproject.toml # または requirements.txt
├── src/
│ └── myapp/
│ ├── main.py
│ ├── routers/
│ └── models/
└── tests/
PHPはNginx + PHP-FPMの2コンテナ構成が定番だが、FastAPIはuvicornがHTTPサーバーも兼ねるので1コンテナで完結できる。シンプル。
Dockerfile
PHPプロジェクトのDockerfile(参考)
# PHP(Laravel)
FROM php:8.3-fpm
RUN apt-get update && apt-get install -y \
libpq-dev \
&& docker-php-ext-install pdo pdo_pgsql
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html
COPY . .
RUN composer install --no-dev --optimize-autoloader
CMD ["php-fpm"]
PythonプロジェクトのDockerfile
# Python(FastAPI)
FROM python:3.12-slim
WORKDIR /app
# 依存関係をインストール
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# アプリのコードをコピー
COPY . .
# 非rootユーザーで実行(セキュリティ)
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.myapp.main:app", "--host", "0.0.0.0", "--port", "8000"]
PHPと比べてシンプル。Nginxが不要な分、コンテナが1つ少ない。
venvはDockerの中で使うか
ここで最初に迷った。
# パターン① — venvなし(Dockerの中だけで使うなら不要)
RUN pip install --no-cache-dir -r requirements.txt
# パターン② — venvあり(Dockerの中でもvenvを使う)
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
RUN pip install --no-cache-dir -r requirements.txt
結論から言うとDockerコンテナの中ではvenvは不要な場合が多い。コンテナ自体が隔離環境なので、systemのPython環境にインストールしても他のプロジェクトに影響しない。
ただしマルチステージビルドを使う場合は、venvごとコピーすると綺麗に書ける。
マルチステージビルド
本番用イメージを小さくするためにマルチステージビルドを使う。
# ---- ビルドステージ ----
FROM python:3.12-slim AS builder
WORKDIR /app
# venvを作ってそこにインストール
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ---- 本番ステージ ----
FROM python:3.12-slim AS production
WORKDIR /app
# ビルドステージのvenvだけコピー
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"
# アプリのコードをコピー
COPY src/ ./src/
# 非rootユーザーで実行
RUN useradd --create-home appuser
USER appuser
EXPOSE 8000
CMD ["uvicorn", "src.myapp.main:app", "--host", "0.0.0.0", "--port", "8000"]
ビルドステージにはpipのキャッシュやビルドツールが残るが、本番ステージにはvenvの中身とアプリのコードだけコピーされるのでイメージが小さくなる。
# イメージサイズの比較
docker images
# シングルステージ: ~250MB
# マルチステージ: ~180MB
compose.yaml
PHPプロジェクトのcompose.yaml(参考)
# PHP(Laravel)
services:
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- .:/var/www/html
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
php:
build: .
volumes:
- .:/var/www/html
environment:
- APP_ENV=local
depends_on:
- db
db:
image: postgres:16
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
PythonプロジェクトのDockerCompose
# Python(FastAPI)
services:
api:
build:
context: .
target: production # マルチステージのどのステージを使うか
ports:
- "8000:8000"
env_file:
- .env
volumes:
- .:/app # 開発時はコードをマウント
depends_on:
db:
condition: service_healthy # DBが起動してから
db:
image: postgres:16
environment:
POSTGRES_DB: mydb
POSTGRES_USER: user
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U user -d mydb"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:
PHPと比べてNginxコンテナがない分シンプル。service_healthyを使うとDBが起動完了してからAPIが起動するので、接続エラーが出にくい。
開発環境と本番環境の切り替え
compose.override.yaml
開発環境用の設定をcompose.override.yamlに分ける。
# compose.override.yaml(開発環境用、Gitignoreに入れる)
services:
api:
build:
target: builder # 開発はbuilderステージを使う
command: uvicorn src.myapp.main:app --host 0.0.0.0 --port 8000 --reload
volumes:
- .:/app # ホットリロードのためにマウント
environment:
- DEBUG=true
# 開発環境(compose.yaml + compose.override.yamlが自動で合成される)
docker compose up
# 本番環境(overrideを使わない)
docker compose -f compose.yaml up
--reloadオプションをつけるとコードを変更したときに自動でリロードされる。開発時だけつけて本番はつけない、という使い分けをoverrideファイルで実現できる。
環境変数の管理
# 開発環境
.env # ← Gitignore
.env.example # ← Gitに含める
# compose.yamlでenv_fileを指定
env_file:
- .env
# または environment で直接書く(シークレット以外)
services:
api:
environment:
APP_ENV: production
LOG_LEVEL: INFO
# DATABASE_URLなど機密情報はenv_fileで
uvicornのプロセス管理
PHPはPHP-FPMがプロセス管理してくれるが、uvicornは自前で考える必要がある。
開発環境
# シングルプロセス、ホットリロードあり
uvicorn src.myapp.main:app --reload
本番環境
# マルチプロセス(CPUコア数に合わせる)
uvicorn src.myapp.main:app --host 0.0.0.0 --port 8000 --workers 4
ただし本番ではgunicornと組み合わせるほうが一般的。
pip install gunicorn
# gunicorn + uvicornワーカー
gunicorn src.myapp.main:app \
--workers 4 \
--worker-class uvicorn.workers.UvicornWorker \
--bind 0.0.0.0:8000
# Dockerfile(本番用)
CMD ["gunicorn", "src.myapp.main:app",
"--workers", "4",
"--worker-class", "uvicorn.workers.UvicornWorker",
"--bind", "0.0.0.0:8000"]
PHPのPHP-FPMがワーカープロセスを管理するのと同じ役割をgunicornが担う。
ヘルスチェックエンドポイント
Dockerのヘルスチェックに使うエンドポイントをFastAPI側に用意する。
# main.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health_check():
return {"status": "ok"}
# Dockerfile
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# compose.yaml
services:
api:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
.dockerignore
# .dockerignore
.git
.gitignore
.env
.env.*
.venv
__pycache__
*.pyc
*.pyo
.pytest_cache
.mypy_cache
.ruff_cache
tests/
docs/
README.md
compose.override.yaml
PHPの.dockerignoreにvendor/を入れるのと同じく、.venv/と__pycache__は必ず入れる。ビルドコンテキストが小さくなってビルドが速くなる。
PHPプロジェクトとの構成比較まとめ
| 項目 | PHP(Laravel) | Python(FastAPI) |
|---|---|---|
| Webサーバー | Nginx必須 | uvicorn(単体で動く) |
| コンテナ数 | Nginx + PHP-FPM + DB | API + DB |
| 依存管理 | composer.json / composer.lock | requirements.txt / pyproject.toml |
| プロセス管理 | PHP-FPM | gunicorn + uvicorn |
| 仮想環境 | 不要 | venvは不要(コンテナが隔離環境) |
| ホットリロード | php-fpm(自動) |
--reloadオプション |
pyproject.tomlでの依存管理
requirements.txtよりpyproject.tomlを使うプロジェクトも増えている。
# pyproject.toml
[project]
name = "myapp"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.110.0",
"uvicorn[standard]>=0.29.0",
"pydantic-settings>=2.2.0",
"sqlalchemy>=2.0.0",
"psycopg2-binary>=2.9.0",
]
[project.optional-dependencies]
dev = [
"pytest>=8.0.0",
"pytest-cov>=5.0.0",
"mypy>=1.9.0",
"ruff>=0.3.0",
]
# pipでインストール
pip install -e .
pip install -e ".[dev]"
Poetryを使うとlock fileで再現性が担保できる。
pip install poetry
poetry install
composer.lockに相当するのがpoetry.lock。チームで同じ環境を再現するならrequirements.txtよりpyproject.toml + Poetryのほうが管理しやすい。
まとめ
- Pythonコンテナ内ではvenvは不要(コンテナ自体が隔離環境)
- マルチステージビルドでvenvをコピーするとイメージが小さくなる
- 本番はgunicorn + uvicornワーカーでプロセス管理
- Nginxが不要な分PHPより構成がシンプル
-
compose.override.yamlで開発/本番の設定を分ける
PHPプロジェクトのDocker化の経験があれば、概念は同じなので移行しやすかった。venvの扱いだけがPHPにない概念で少し迷ったが、「コンテナの中ではvenv不要」と割り切ったらスッキリした。