1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonプロジェクトのDocker化 — PHPプロジェクトとの構成の違いを比べた

1
Posted at

はじめに

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の.dockerignorevendor/を入れるのと同じく、.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不要」と割り切ったらスッキリした。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?