Python Web アプリの本番デプロイ — gunicorn、uvicorn、systemd、Docker

読了 6分

python manage.py runserveruvicorn main:app --reload で開発を終えると、これをそのままサーバーに載せたい誘惑に駆られます。だめです。開発サーバーはシングルプロセスでデバッグの利便性を優先して作られており、同時アクセスをさばけず、落ちても勝手には復活しません。この記事では、Python の Web アプリを本番環境に載せる標準構成を最初から最後まで整理します。

全体像 — 4 つの層 #

本番構成は、役割の分かれた 4 つの層として理解すると明確です。

全体構成
クライアント
  → nginx(リバースプロキシ: TLS、静的ファイル、バッファリング)
    → gunicorn(プロセスマネージャー: 複数ワーカーを管理)
      → ワーカープロセス(uvicorn worker または sync worker: アプリを実行)
        → アプリケーション(Django、FastAPI、Flask)

各層がなぜ必要なのかが、この記事の骨格です。ひとつずつ降りていきます。

WSGI と ASGI — アプリとサーバーの間の規約 #

Python の Web サーバーとフレームワークの間には標準インターフェースがあります。

  • WSGI: 同期の規約です。Django(従来構成)や Flask がここに属し、サーバーは gunicorn の sync ワーカーが標準です。
  • ASGI: 非同期の規約です。FastAPI、Starlette、非同期 Django がここに属し、サーバーは uvicorn が代表です。

自分のアプリがどちら側かでサーバーの選択が分かれます。迷ったら、フレームワークのドキュメントのデプロイ節に書いてあるほうが正解です。

gunicorn + uvicorn ワーカー — 事実上の標準の組み合わせ #

uvicorn 単体でもサービスは可能ですが、プロセス管理(死んだワーカーの再起動、ワーカー数の管理、graceful な再起動)は gunicorn のほうが成熟しています。そこで ASGI アプリの標準構成は、gunicorn をプロセスマネージャーに、uvicorn をワーカークラスに使うことです。

実行
# FastAPI (ASGI)
gunicorn main:app \
  --worker-class uvicorn.workers.UvicornWorker \
  --workers 4 \
  --bind 127.0.0.1:8000 \
  --timeout 60 \
  --graceful-timeout 30

# Django/Flask (WSGI) ならワーカークラスなしで
gunicorn myproject.wsgi:application --workers 4 --bind 127.0.0.1:8000

ワーカー数は古くからの経験則 CPU コア数 × 2 + 1 から始めます。ただしこれは出発点であって正解ではありません。CPU 中心の処理ならコア数近くまで減らし、I/O 待ちの長いアプリなら少し増やしつつ、ワーカーの分だけメモリを消費することと、DB のコネクションプールがワーカー数だけ増えること(SQLAlchemy 2.0 #2 で扱った計算)を一緒に見る必要があります。負荷テストで確定させるのが原則です。

systemd — プロセスを OS に任せます #

ターミナルで gunicorn を立ち上げて SSH を切れば、プロセスも一緒に消えます。nohup や screen はその場しのぎです。Linux サーバーでは、プロセスのライフサイクルは systemd に任せるのが標準です。

/etc/systemd/system/myapp.service
# /etc/systemd/system/myapp.service
[Unit]
Description=myapp gunicorn service
After=network.target

[Service]
User=myapp
WorkingDirectory=/srv/myapp
Environment="DATABASE_URL=postgresql://..."
ExecStart=/srv/myapp/.venv/bin/gunicorn main:app \
  --worker-class uvicorn.workers.UvicornWorker --workers 4 \
  --bind 127.0.0.1:8000
Restart=always
RestartSec=3

[Install]
WantedBy=multi-user.target
サービス管理
sudo systemctl enable --now myapp   # 起動時の自動開始 + 即時開始
sudo systemctl status myapp         # 状態確認
sudo systemctl restart myapp        # デプロイ後の再起動
journalctl -u myapp -f              # ログ確認

Restart=always の 1 行が「プロセスが死んだら自動で復活させる」を解決します。仮想環境の中の gunicorn を絶対パスで指定すること、シークレットはユニットファイルではなく EnvironmentFile= に分離することが実務のコツです。

nginx — アプリサーバーの前に立つ理由 #

gunicorn を 80 番ポートに直接さらさず、前に nginx を置くのには具体的な理由があります。

  • TLS 終端: 証明書の管理と HTTPS の処理を nginx が引き受けます。
  • 静的ファイル: CSS、JS、画像を Python のワーカーで配信するのは無駄です。nginx が直接配信します。
  • バッファリング: 遅いクライアントがレスポンスを受け取り終わるまで Python のワーカーを拘束しないよう、nginx がレスポンスを受け取って代わりに送信します。ワーカーの占有時間が大きく減ります。
  • リクエストサイズ制限、タイムアウト、アクセスログといった防衛線の役割も兼ねます。
nginx.conf
server {
    listen 443 ssl;
    server_name example.com;

    location /static/ {
        alias /srv/myapp/static/;
    }
    location / {
        proxy_pass http://127.0.0.1:8000;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

Docker でデプロイするなら #

コンテナ環境では systemd の席をコンテナオーケストレーター(Docker Compose、Kubernetes など)が代わりに務め、イメージの中では gunicorn がフォアグラウンドで動きます。

Dockerfile
FROM python:3.13-slim

WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv && uv sync --frozen --no-dev

COPY . .

# コンテナでは標準出力へログ、フォアグラウンドで実行
CMD ["uv", "run", "gunicorn", "main:app", \
     "--worker-class", "uvicorn.workers.UvicornWorker", \
     "--workers", "2", "--bind", "0.0.0.0:8000", \
     "--access-logfile", "-", "--error-logfile", "-"]
  • 依存関係のインストール層とコードのコピー層を分けると、コードだけ変わったときにビルドキャッシュが生きてビルドが速くなります。lock ファイルによる再現性は Python パッケージング #5 で扱いました。
  • コンテナではワーカー数を少なめ(コンテナあたり 1〜2)にして、スケールはコンテナ数で調整するほうがオーケストレーターと相性が良いです。

最後のチェックリスト — ログ、ヘルスチェック、終了処理 #

  • ログは stdout へ: ファイルログの代わりに標準出力へ出し、収集は journald やコンテナランタイムに任せます。「ログがどこにあるか探し回る」状況そのものをなくす設計です。
  • ヘルスチェックのエンドポイント: /healthz のようなパスを作り、ロードバランサーとオーケストレーターが生きているプロセスにだけトラフィックを流せるようにします。DB まで確認する readiness とプロセスの生存だけ見る liveness を分けるとさらに良いです。
  • graceful shutdown: デプロイの再起動で処理中のリクエストを切らないためには、新しいリクエストの受付を止め、進行中のリクエストを終えてから終了する必要があります。gunicorn の --graceful-timeout がその役割で、コンテナなら SIGTERM を gunicorn が直接受け取れるよう CMD を exec 形式(JSON 配列)で書くのが前提条件です。
  • 環境変数で設定を分離: DB の接続先やシークレットキーはコードやイメージに入れず、環境から注入します。

まとめ #

  • 本番構成は nginx(プロキシ)、gunicorn(プロセス管理)、ワーカー(アプリ実行)の層構造です。開発サーバーは本番に使いません。
  • WSGI アプリは gunicorn の sync ワーカー、ASGI アプリは gunicorn + uvicorn ワーカーの組み合わせが標準です。ワーカー数は コア × 2 + 1 から始めて負荷テストで確定します。
  • プロセスの生存は systemd(またはコンテナオーケストレーター)に任せ、ログは stdout、設定は環境変数に出します。
  • nginx は TLS、静的ファイル、バッファリングで Python ワーカーの負担を減らす前段です。
  • ヘルスチェックと graceful shutdown まで備えて、初めて無停止デプロイが成立します。
X