FastAPIをDockerで本番運用する方法:Uvicorn、Nginx、HTTPS、監視の実践ガイド

green snake
Photo by Pixabay on Pexels.com

FastAPIがローカルで動いても、そのまま本番公開できるわけではありません。本番では、アプリを起動するプロセス、外部通信を受けるリバースプロキシ、HTTPS、死活監視、終了処理、ログ収集を一つの構成として設計します。

この記事では、小規模な単一サーバーでDocker Composeを使う構成を例にします。Kubernetesなどがコンテナの複製や切り替えを担う環境では、同じイメージを使いながらプロセス数の考え方を変えます。公開前の全体点検には、関連記事のFastAPIプロトタイプを本番運用へ進める実務チェックリストも利用できます。

最初に決める実行構成

ASGIサーバーは、FastAPIアプリとHTTP通信をつなぐ実行サーバーです。ここではUvicornを使います。Nginxはその前段で通信を受け、HTTPSの終端、転送、必要に応じた静的ファイル配信を担当します。

Gunicornは常に必要な部品ではありません。Uvicorn自体にも複数ワーカーを起動する機能があります。Gunicornのプロセス管理を使う場合は、非推奨になったuvicorn.workersではなく、別パッケージのuvicorn-workerを選びます。現行の扱いはUvicornのデプロイ公式資料で確認できます。

実行環境ごとの役割分担
実行環境 アプリのプロセス 複製と切り替え
単一サーバーのDocker Compose Uvicornの複数ワーカー、またはGunicornとuvicorn-workerを検討する 同一ホスト内で運用手順を組み、停止時間を許容できるか確認する
Kubernetesなどのクラスタ 原則として1コンテナに1プロセスを置く クラスタ側のレプリカ、ヘルスプローブ、ローリング更新に任せる
マネージドコンテナ基盤 基盤の推奨とCPU、メモリ制限に合わせる 基盤のロードバランサー、HTTPS、更新方式を利用する

クラスタ環境でコンテナ内にも多数のワーカーを置くと、プロセス単位とコンテナ単位の両方で容量を管理することになります。まず1コンテナ1プロセスにすると、メモリ上限とレプリカ数の関係を把握しやすくなります。一方、単一サーバーでは複数ワーカーが選択肢になります。どちらも固定の正解ではなく、負荷試験と利用できるメモリから決めます。

設定とライフサイクルをアプリに組み込む

環境ごとに変わる値は、イメージへ直接書き込まず、実行時の環境変数から読み込みます。次の例はPydantic Settingsの現行形式を使い、ローカル開発では.envを読めるようにしたものです。本番の秘密情報は、利用する基盤のSecret管理へ移します。

# app/core/settings.py
from functools import lru_cache

from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_file=".env",
        env_file_encoding="utf-8",
        extra="ignore",
    )

    app_name: str = "My FastAPI"
    app_env: str = "development"
    log_level: str = "INFO"
    database_url: str


@lru_cache
def get_settings() -> Settings:
    return Settings()

ライフサイクルは、アプリの起動前と終了時に共有資源を準備し、解放する仕組みです。データベース接続プールやHTTPクライアントを扱う場合は、古いstartupとshutdownのイベント記法ではなく、FastAPIが推奨するlifespanへまとめます。実装の分け方はFastAPIのlifespanで共有リソースを管理する方法でも詳しく解説しています。

# app/main.py
from contextlib import asynccontextmanager

from fastapi import FastAPI


@asynccontextmanager
async def lifespan(app: FastAPI):
    # ここで接続プールなどを準備する
    yield
    # ここで接続プールなどを解放する


app = FastAPI(title="Production FastAPI", lifespan=lifespan)


@app.get("/health/liveness", include_in_schema=False)
async def liveness():
    return {"status": "alive"}


@app.get("/health/readiness", include_in_schema=False)
async def readiness():
    # 必須の依存先を短いタイムアウトで確認し、
    # 受付できない場合はHTTP 503を返す実装に置き換える
    return {"status": "ready"}

このreadinessの例は、エンドポイントの形を示すだけです。依存先を確認していないため、貼り付けただけでは「リクエストを処理する準備ができた」ことを保証しません。実際のアプリでは、受付に不可欠なデータベースなどを短いタイムアウトで確認し、処理できないときにHTTP 503を返します。

Dockerfileは小さく、終了シグナルを届かせる

依存ファイルを先にコピーすると、アプリコードだけを変更したビルドで依存インストール層のキャッシュを再利用できます。実行ユーザーはrootから分離し、起動コマンドはJSON配列のexec形式にします。exec形式ならUvicornがPID 1になり、Dockerからの終了シグナルを直接受け取れます。

FROM python:3.11-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV PIP_NO_CACHE_DIR=1

RUN useradd --create-home --uid 10001 appuser
WORKDIR /app

COPY requirements.txt ./
RUN python -m pip install --upgrade pip && python -m pip install -r requirements.txt

COPY --chown=appuser:appuser app ./app
USER appuser

EXPOSE 8000

HEALTHCHECK --interval=15s --timeout=3s --start-period=10s --retries=3 CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health/liveness', timeout=2)" || exit 1

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers"]

ここではDockerのヘルスチェックをlivenessへ向けています。データベース障害のたびにアプリコンテナまで不健康と判定すると、再起動しても直らない障害で再起動を繰り返す可能性があるためです。readinessは、ロードバランサーやオーケストレーターがトラフィックを流すか判断する用途に分けます。

requirements.txtには、少なくともFastAPI、Uvicorn、pydantic-settingsを含めます。再現性を高めるには、検証した依存バージョンをロックし、更新時にテストしてからイメージを作り直します。DockerfileとComposeの基本的な役割を確認したい場合は、PythonアプリのDocker構成入門が補足になります。

Docker ComposeでアプリとNginxを分離する

Composeのトップレベルにあったversionは現在は互換性のために残る情報で、指定すると廃止扱いの警告が出ます。次の例では省略し、アプリの8000番ポートをホストへ公開せず、Nginxだけを外部公開します。

services:
  app:
    build: .
    image: my-fastapi:${APP_IMAGE_TAG}
    environment:
      APP_ENV: production
      LOG_LEVEL: INFO
      FORWARDED_ALLOW_IPS: ${NGINX_PROXY_IPS}
      DATABASE_URL: ${DATABASE_URL}
    expose:
      - "8000"
    restart: unless-stopped
    stop_grace_period: 30s

  nginx:
    image: nginx:alpine
    depends_on:
      app:
        condition: service_healthy
    volumes:
      - ./deploy/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./certs:/etc/nginx/certs:ro
    ports:
      - "80:80"
      - "443:443"
    restart: unless-stopped

APP_IMAGE_TAG、DATABASE_URL、NGINX_PROXY_IPSは実行環境で設定します。NGINX_PROXY_IPSにはNginxの実アドレスまたはネットワークだけを指定します。Nginxも検証したタグまたはダイジェストに固定します。depends_onへservice_healthyを指定すると、ComposeはappがhealthyになるまでNginxの起動を待ちます。ただし、ヘルスチェックが後から失敗した際の復旧方針まで自動で決まるわけではありません。監視通知、再起動条件、切り戻し手順は別に用意します。

NginxでHTTPSとWebSocketを扱う

公式Nginxイメージのconf.dはhttpコンテキストから読み込まれます。WebSocket用のmapはserverブロック内には置けないため、次のようにファイルの最上位へ置きます。HTTP/2は現在のhttp2 on;記法を使います。

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

upstream fastapi_upstream {
    server app:8000;
    keepalive 16;
}

server {
    listen 80;
    server_name example.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    http2 on;
    server_name example.com;

    ssl_certificate     /etc/nginx/certs/fullchain.pem;
    ssl_certificate_key /etc/nginx/certs/privkey.pem;

    location / {
        proxy_pass http://fastapi_upstream;
        proxy_http_version 1.1;

        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Request-ID      $request_id;

        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        proxy_connect_timeout 5s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

example.comと証明書のパスは実環境へ合わせます。証明書の取得と更新も運用手順に含めます。クラウドのロードバランサーやCDNでHTTPSを終端する場合は、Nginxへ証明書を置かず、その基盤から内部HTTPで転送する構成も選べます。

プロキシから受け取るX-Forwarded-ForとX-Forwarded-ProtoをUvicornが信頼する範囲は、実際のネットワークに合わせて--forwarded-allow-ipsで限定します。アプリのポートを直接公開したまま、すべての送信元を信頼する設定にはしません。

ワーカー数と終了処理を負荷試験で決める

CPUコア数 × 2 + 1はGunicornで知られる出発点ですが、FastAPIの非同期処理、データベース接続数、1プロセス当たりのメモリを反映した答えではありません。まず少数のワーカーで負荷試験を行い、CPU使用率、メモリ、応答時間、エラー率、依存先の上限を見て増減します。

単一サーバーでGunicornを採用する場合の起動例は次のとおりです。uvicorn-workerとgunicornを依存へ追加して使います。

gunicorn app.main:app --bind 0.0.0.0:8000 --workers 2 --worker-class uvicorn_worker.UvicornWorker --graceful-timeout 25 --timeout 60

ワーカー数2と各タイムアウトは例であり、推奨値ではありません。アプリ側が処理中のリクエストを待てる時間より、Dockerや実行基盤が強制終了するまでの猶予を長くします。データベース接続やキューの解放はlifespanの終了処理に置きます。

単一のappコンテナをdocker compose upで作り直すだけでは、無停止更新は保証できません。停止時間を避けるには、少なくとも新旧のインスタンスを同時に動かし、新しいreadinessが成功してからトラフィックを切り替える仕組みが必要です。

ヘルスチェックの役割を分ける

監視エンドポイントの判断基準
種類 確認すること 失敗時の扱い
liveness プロセスが応答できるか 継続失敗ならプロセスやコンテナの再起動を検討する
readiness 新しいリクエストを処理できるか トラフィックの転送対象から一時的に外す
外形監視 利用者と同じ経路でHTTPSや主要機能が動くか 通知し、影響範囲と復旧手順を確認する

readinessで確認する依存先を増やしすぎると、任意機能の一時障害までAPI全体の受付停止につながります。受付に不可欠な依存先だけを対象にし、短いタイムアウトを設定します。外形監視はコンテナ内部ではなく、DNS、HTTPS、Nginxを通る利用者側の経路から実行します。

ログ、メトリクス、トレースをつなぐ

可観測性は、障害の原因を外部から調べられるようにする性質です。ログだけを増やすのではなく、次の三つを役割で分けます。

  • ログ:個別の出来事、例外、リクエストIDを時刻付きで残す
  • メトリクス:リクエスト数、応答時間、エラー率、CPU、メモリを集計する
  • トレース:一つのリクエストがAPI、データベース、外部サービスを通った時間配分を追う

コンテナではログを標準出力と標準エラーへ出し、収集先は実行基盤に任せます。JSON形式を使う場合は、少なくとも時刻、重要度、イベント名、サービス名、リクエストID、例外情報の項目名をチーム内で揃えます。認証情報、Cookie、アクセストークン、個人情報は記録しません。

OpenTelemetryを導入する場合は、アプリ名やOTLPの送信先を環境変数へ置きます。最初から全通信を計装するのではなく、障害調査で必要なAPIと依存先から始め、送信量と保存期間を確認します。

本番公開前のセキュリティ確認

  • アプリを非rootユーザーで実行し、書き込み可能な場所を必要な範囲に限定する
  • Nginx以外のポートを外部へ公開しない
  • .env、秘密鍵、認証情報をイメージやリポジトリへ含めない
  • CORSの許可元を実際のフロントエンドに限定する
  • ベースイメージとPython依存を定期的に更新し、更新後にテストする
  • Nginxのclient_max_body_sizeやタイムアウトを、アップロード要件に合わせる
  • 証明書の更新失敗を監視し、期限前に通知する

大容量アップロードをAPIプロセスで受け続けると、ワーカーと通信時間を占有します。要件によっては、オブジェクトストレージへ直接アップロードする署名付きURLを使い、APIは認可とアップロード先の発行だけを担当させます。

ビルドから公開確認までの手順

  1. イメージタグと依存バージョンを固定する
  2. docker compose configで展開後の設定を確認する
  3. docker compose buildでイメージを作る
  4. テストと脆弱性検査を通す
  5. docker compose up -dで起動する
  6. docker compose psでhealth状態を確認する
  7. docker compose exec nginx nginx -tでNginx設定を検証する
  8. HTTPS経由でliveness、readiness、主要APIを確認する
  9. ログ、メトリクス、アラートが届くことを確認する
  10. 直前のイメージタグへ戻す切り戻し手順を試す
docker compose config
docker compose build
docker compose up -d
docker compose ps
docker compose exec nginx nginx -t
curl --fail https://example.com/health/liveness
curl --fail https://example.com/health/readiness
docker compose logs --tail=100 app nginx

症状から確認箇所を絞る

本番で起こりやすい症状と確認箇所
症状 最初に確認する箇所 対応の方向
WebSocketが切れる NginxのUpgrade、Connection、タイムアウト 公式設定と照合し、アプリのping設計も確認する
HTTPSなのにリダイレクト先がHTTPになる X-Forwarded-ProtoとUvicornの信頼範囲 信頼できるプロキシだけを許可する
更新時に接続が切れる 新旧インスタンスの重複稼働、readiness、終了猶予 起動完了後に切り替え、処理中の要求を待って停止する
応答時間が不安定になる CPU、メモリ、DB接続、イベントループを塞ぐ同期処理 計測結果を基にワーカー数と依存先の容量を調整する
unhealthyになるが再起動しない Dockerのhealth状態と再起動を同一視していないか 監視と実行基盤に復旧ルールを設定する

公式資料で確認しておきたい項目

FastAPIの本番運用で残すべき設計判断

本番運用では、起動コマンドを動かすことより、役割の境界を記録することが大切です。単一サーバーかクラスタか、誰がHTTPSを終端するか、何をreadinessで確認するか、何秒待って終了するか、どの指標で増強を判断するかを運用手順へ残します。

まず小さな構成で起動し、ヘルスチェック、終了処理、ログ、切り戻しを検証します。その後、実測した負荷に合わせてワーカー、レプリカ、監視項目を増やせば、構成の複雑さと運用上の必要性を対応させられます。

この記事に関連する株式会社greedenの取り組み

FastAPIの本番運用では、実装に加え、インフラ、監視、保守まで一続きで設計します。株式会社greedenは、FastAPIやDockerに対応したWebシステム開発を、要件定義からクラウド基盤、リリース後の改善まで支援します。

投稿者 greeden

コメントを残す

メールアドレスが公開されることはありません。 ※ が付いている欄は必須項目です

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)