Dockerize FastAPI: 「ローカルでは動く」から本番環境対応まで(Alembic & マルチステージビルド対応)

Docker tutorial - IT technology blog
Docker tutorial - IT technology blog

午前2時の悪夢:「自分のPCでは動いているんですけど?」

ある夜、サーバー監視をしていた時のことを鮮明に覚えています。デプロイ直後に500エラーが連発しました。FastAPIコンテナのログには、たった一行こう記されていました:sqlalchemy.exc.ProgrammingError: (psycopg2.errors.UndefinedTable) relation "users" does not exist

ローカル環境ではすべてが完璧に動作していました。問題は、デプロイ時にAlembicのマイグレーションを実行し忘れたことでした。さらに悪いことに、1.2GBもある巨大で古いDockerfileのせいで、修正用の新しいイメージをプルするだけで膨大な時間がかかってしまいました。

もしあなたが、コンテナの動作が重い、イメージサイズが数GBもある、あるいはDocker内でAlembicをどう実行すべきか迷っているなら、この記事はあなたのためのものです。これは、私が本番環境で何度も苦い経験をして得た教訓をまとめたものです。

なぜ「とりあえず作った」Dockerfileが問題を引き起こすのか?

多くの人は、以下のようなシンプルなDockerfileから書き始めるでしょう:

FROM python:3.11
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

一見問題なさそうですが、実際には3つの大きな問題を抱えた「時限爆弾」です:

  • 巨大なサイズ: イメージにはコンパイラやpipのキャッシュ、不要なファイルが含まれています。
  • 低いセキュリティ: root権限でコンテナを実行するのは、ハッカーにアプリを乗っ取られた場合に非常に危険です。
  • DBの同期ズレ: データベースのスキーマが自動的に更新されません。

マルチステージビルド戦略:1GBから150MBへのダイエット

サイズの問題を解決するには、マルチステージビルド(Multi-stage build)が最良の選択です。考え方はシンプルです。依存関係のビルドにはツールがフルセット入ったイメージを使い、実行時には必要なものだけを「超軽量(slim)」なイメージにコピーします。

Docker API의デバッグ時のちょっとしたコツ:JSONレスポンスを素早く確認したい場合は、toolcraft.app/ja/tools/developer/json-formatterに貼り付けてみてください。拡張機能を入れなくてもデータを非常に速く整形できます。

以下は、実際のプロジェクトで私がよく使用している最適化されたDockerfileの構成です:

# ステージ 1: Builder - コンパイラのインストールとwheelのビルド
FROM python:3.11-slim as builder

WORKDIR /app

ENV PYTHONDONTWRITEBYTECODE 1
ENV PYTHONUNBUFFERED 1

RUN apt-get update && apt-get install -y --no-install-recommends gcc python3-dev libpq-dev

COPY requirements.txt .
RUN pip wheel --no-cache-dir --no-deps --wheel-dir /app/wheels -r requirements.txt

# ステージ 2: Final - 本番環境用の軽量イメージ
FROM python:3.11-slim

WORKDIR /app

# セキュリティのため専用ユーザーを作成(rootを使用しない)
RUN addgroup --system app && adduser --system --group app

# 必要なランタイムライブラリのみをインストール
RUN apt-get update && apt-get install -y libpq-dev && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/wheels /wheels
COPY --from=builder /app/requirements.txt .
RUN pip install --no-cache /wheels/*

COPY . .

# appユーザーに権限を付与
RUN chown -R app:app /app
USER app

CMD ["/app/entrypoint.sh"]

Alembicによるマイグレーション処理:完全自動化

デプロイが終わってから手動で alembic upgrade head を入力するのを待ってはいけません。これを entrypoint.sh ファイルに組み込みましょう。このファイルにより、FastAPIが起動する前にデータベースが常に準備され、正しいバージョンであることが保証されます。

#!/bin/sh

echo "データベース接続を確認中..."
# DBの準備が整ったことを確実にするために wait-for-it.sh スクリプトの使用を推奨します

echo "マイグレーションを実行中..."
alembic upgrade head

echo "サーバーを起動中..."
exec "$@"

実行権限の付与を忘れないでください:chmod +x entrypoint.sh。この手順を忘れると、コンテナは即座に “Permission denied” エラーを吐きます。

Docker Compose:開発環境と本番環境の分離

ローカルで開発する際は、Ctrl+Sを押すたびにコンテナが変更を自動反映するように --reload が必要です。しかし、本番環境では --reload はパフォーマンスを低下させ、不安定にするため厳禁です。

解決策は、柔軟な docker-compose.yml を使用することです:

services:
  db:
    image: postgres:15-alpine
    volumes:
      - postgres_data:/var/lib/postgresql/data/
    environment:
      - POSTGRES_USER=myuser
      - POSTGRES_PASSWORD=mypass
      - POSTGRES_DB=fastapi_db

  web:
    build: .
    command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload
    volumes:
      - .:/app
    ports:
      - "8000:8000"
    env_file:
      - .env
    depends_on:
      - db

volumes:
  postgres_data:

注意: .env ファイル内の DATABASE_URLlocalhost から db に変更してください。これはComposeファイル内のサービス名であり、コンテナ同士が内部ネットワークで見つけ合えるようにするためのものです。

本番環境向けにGunicornへアップグレード

リリース(Go-live)時には、マルチワーカー(multi-worker)メカニズムを活用するために uvicornGunicorn に置き換えましょう。Gunicornはプロセスの管理に優れています。もし1つのワーカーがフリーズしても、アプリを稼働させ続けるために別のワーカーを自動的に再起動します。

gunicorn -w 4 -k uvicorn.workers.UvicornWorker app.main:app --bind 0.0.0.0:8000

一般的なワーカー数の計算式は:(2 x CPUコア数) + 1 です。例えばサーバーが2コアなら、5ワーカーに設定するのが適切です。

SQLAlchemyモデル修正時の標準フロー

スキーマの不一致を防ぐために、以下の5つのステップに従ってください:

  1. Pythonコード内のモデルを更新する。
  2. docker-compose exec web alembic revision --autogenerate -m "変更内容の説明" を実行する。
  3. alembic/versions フォルダ内のファイルを再確認し、間違いがないかチェックする。
  4. コードと新しいマイグレーションファイルの両方をGitにコミットする。
  5. デプロイ時、サーバー上での残りの作業は entrypoint.sh が担当します。

FastAPIのDocker化は、単に数行のコマンドをコピー&ペーストすることではありません。それはレイヤーを最適化し、ユーザー権限を保護し、データを同期させるプロセスです。最初から正しく行うことで、深夜の呼び出しに怯えることなく、枕を高くして眠れるようになります。

Share: