メインコンテンツへスキップ
友田 陽大
Flask 本番運用
Python
Flask
Gunicorn
Docker
WSGI
本番運用
AWS
バックエンド

Flaskの本番デプロイ実践:Gunicorn・WSGIサーバー選定・ProxyFix・Docker・グレースフルシャットダウン

Flask 3.1系を本番品質でデプロイする実装ガイド。開発サーバーを捨てる理由、WSGIアプリとサーバーの分離、Gunicorn/Waitress/uWSGI/mod_wsgiの選定、ワーカー数とgeventワーカー、ProxyFixの正しい設定とALBの背後での運用、マルチステージDockerと非rootコンテナ、SIGTERMによるグレースフルシャットダウンとECSのゼロダウンタイムデプロイまでを公式ドキュメントに忠実な実コードで解説します。

公開日
読了時間
29分
著者
友田 陽大
シェア
目次

導入:本番障害の多くは「サーバーの選び方」で決まる

Flask アプリのコードがどれほど綺麗でも、それを動かすサーバーの選定とデプロイ構成を間違えれば、本番は静かに壊れますflask run のまま本番に出してしまう、ワーカー数が 1 のまま負荷で詰まる、ロードバランサの背後で request.remote_addr が全部プロキシの IP になる、デプロイのたびに処理中のリクエストが切れて 502 が出る——これらは「アプリのバグ」ではなく「デプロイ設計の欠如」が生む障害です。

本記事は、Flask 本番運用ガイド(ピラー) の §8「デプロイ」を、本番品質まで深掘りするスポークです。扱うのは Flask 3.1 系(現行安定版)の公式ドキュメントに忠実なデプロイ設計だけです。具体的には、WSGI サーバーの選定、Gunicorn のワーカー設計、ProxyFix の正しい設定、マルチステージ Docker、そしてゼロダウンタイムを実現するグレースフルシャットダウンまでを、実コードで通します。

筆者は、経済産業大臣賞を受賞した B2B SaaS のバックエンドを Python / Flask / SQLAlchemy / PostgreSQL で設計・実装し、API Gateway → ALB → ECS(Fargate) 上で 221 エンドポイントを本番運用してきました。ここで示すのは、その「プロキシチェーンの背後で Flask を安全に動かし続ける」という実戦で必要だった設計です。

💡 この記事で扱うバージョンFlask 3.1 系を前提とします。Flask 3.1 は Werkzeug を WSGI/HTTP 層に持ち、ProxyFixTRUSTED_HOSTS といったデプロイに直結する機能はここに含まれます。WSGI サーバー(Gunicorn 等)は Flask の依存ではなく、別途インストールするものです。


1. 大前提:開発サーバーは本番で使ってはならない

すべての出発点は、Flask 公式の明確な警告です。

"Do not use the development server when deploying to production. It is intended for use only during local development. It is not designed to be particularly secure, stable, or efficient."

訳せば、「開発サーバーを本番にデプロイしてはならない。ローカル開発専用であり、特に安全でも、安定でも、効率的でもない設計だ」。公式は「本番(Production)とは『開発でない(not development)』という意味だ」とも言い切っています。社内ツールであっても、外部に公開する瞬間からそれは本番です。

flask runapp.run() も、この「使ってはならない開発サーバー」です。

# ❌ 本番でこれを起動してはいけない
if __name__ == "__main__":
    app.run()  # これは Werkzeug の開発サーバー。安全でも安定でも効率的でもない

⚠️ アンチパターンif __name__ == "__main__": app.run() を Docker の CMD やプロセスマネージャのエントリポイントにする。これは「開発専用サーバーを本番トラフィックに晒す」ことそのものです。app.run()ローカルで素のスクリプトを叩いたときだけ動くよう if __name__ == "__main__": に隔離し、本番経路(Docker・systemd・ECS)からは決して呼ばない。本番起動は次節以降の WSGI サーバーが担います。

1.1 WSGI「アプリ」と WSGI「サーバー」を分けて考える

なぜ Flask 単体では本番を動かせないのか。それは Flask が WSGI アプリケーションであり、サーバーではないからです。公式の言葉では「Flask は WSGI アプリケーションであり、WSGI サーバーがそれを動かす」。

両者の役割は明確に分かれます。

役割実体
WSGI サーバーTCP で HTTP を受け、HTTP リクエストを WSGI の environ 辞書に変換してアプリへ渡し、返ってきたレスポンスを HTTP に戻すGunicorn / Waitress / uWSGI / mod_wsgi
WSGI アプリenviron を受け取りレスポンスを返す純粋な関数。ルーティング・ビュー・テンプレートあなたの Flask app

つまり本番デプロイとは、**「あなたの WSGI アプリ(app)を、本番グレードの WSGI サーバーに読み込ませる」**ことに尽きます。app.run() の開発サーバーを、Gunicorn に置き換えるだけ——構造はこれだけシンプルです。アプリケーションファクトリ(create_app)で app を組み立てている場合の読み込ませ方は §3.3 で扱います(ファクトリ自体の設計は 大規模構成ガイド を参照)。


2. WSGI サーバーの選定:Gunicorn / Waitress / uWSGI / mod_wsgi

Flask 公式が「自前ホスティング」の選択肢として挙げる WSGI サーバーは、Gunicorn・Waitress・mod_wsgi・uWSGI・gevent です。実務で第一候補になるのは Gunicorn と Waitress の 2 つで、残りは特定要件向けです。

サーバープラットフォーム特徴選ぶ場面
GunicornLinux / WSL(Windows 不可プリフォーク型。ワーカー種別が豊富(sync / gevent 等)。設定が素直Linux 本番のデフォルト。 コンテナ・ECS・EC2
Waitressクロスプラットフォーム(Windows 可純 Python 実装。依存ゼロ、スレッドベースWindows サーバー、純 Python で完結させたいとき
uWSGILinux高機能・高性能だが設定が複雑(学習コスト大)多言語・高度なチューニングが要る大規模構成
mod_wsgiApache 同梱Apache httpd に WSGI を組み込む既存の Apache 資産に相乗りするとき

選定の指針はシンプルです。

  • Linux でコンテナ/VM に載せるなら Gunicorn。 これが本記事の主役です。設定が素直で、ワーカー種別の選択肢が広く、ECS/Fargate のようなコンテナ基盤と相性が良い。
  • Windows で動かす必要があるなら Waitress。 公式も Waitress を「Windows でも動く純 Python の選択肢」と位置づけています。Gunicorn は Windows をサポートしません(WSL 上では動きます)。
  • uWSGI は強力だが設定が重い。 「とりあえず uWSGI」は学習コストに見合わないことが多く、Gunicorn で要件を満たせるなら Gunicorn を選ぶべきです。
  • mod_wsgi は Apache 前提。新規でわざわざ選ぶ理由は薄く、既存 Apache に組み込む特殊事情がある場合の選択肢です。

💡 筆者の選定:木材流通 SaaS では Linux コンテナ(ECS/Fargate)が前提だったため、迷わず Gunicorn を採用しました。Waitress や uWSGI を検討する余地があったのは「Windows 制約」や「極端なチューニング要件」がある場合だけで、コンテナ × Linux という今どきの一般的な構成では、Gunicorn が事実上の標準です。


3. Gunicorn を本番品質で設定する

3.1 基本の起動:ロード構文を理解する

Gunicorn のアプリ指定は {module_import}:{app_variable} という形式です。公式が示す等価関係がそのまま読み方になります。

# 'from hello import app' と等価
gunicorn -w 4 'hello:app'

# 'from hello import create_app; create_app()' と等価(ファクトリ)
gunicorn -w 4 'hello:create_app()'

hello がモジュール(hello.py)、コロンの後ろが「そのモジュールから取り出す WSGI アプリ」です。後者のように 括弧を付ければファクトリ関数の呼び出し結果を WSGI アプリとして使えます。アプリケーションファクトリ(create_app)を採用しているなら、後者の 'myapp:create_app()' 形式を使います。

3.2 ワーカー数(-w):CPU × 2 を出発点に

Gunicorn の既定ワーカー数は 1 です。公式の言葉では「デフォルトはワーカー 1 つだけで、これはおそらくあなたが望むものではない("The default is only 1 worker, which is probably not what you want.")」。本番で 1 ワーカーは、リクエストを 1 つ処理している間に他のリクエストが待たされる致命的なボトルネックです。

出発点は公式が示す CPU × 2("a starting value could be CPU * 2")。あくまで「出発点」であって、ここから負荷試験で調整します。

# 4 コアのホストなら 8 ワーカーから始める
gunicorn -w 8 'myapp:create_app()'

ただしワーカー数にはメモリのトレードオフがあります。Gunicorn のデフォルト(sync ワーカー)はプリフォーク型で、各ワーカーがアプリを丸ごとメモリに抱える別プロセスです。ワーカー 8 つなら、アプリのメモリフットプリント × 8 がおおよその必要メモリになります。コンテナのメモリ上限(ECS のタスク定義など)に対して、CPU × 2 がメモリに収まるかを必ず確認してください。

⚠️ 「ワーカーを増やせば速くなる」は誤り。ワーカー数は CPU コア数とメモリで頭打ちになります。コア数を大きく超えるワーカーは、コンテキストスイッチとメモリ消費を増やすだけで、スループットは上がりません。スケールが必要なら、ワーカー数ではなくコンテナ/タスクの数を増やす(水平スケール)——これが ECS のオートスケーリングの考え方です(ECS Fargate 本番ガイド)。

3.3 sync ワーカー vs gevent ワーカー:ここを混同しない

Gunicorn のワーカー種別の選択は、誤解が最も多いポイントです。公式の指針は明確です。

"The default sync worker is appropriate for most use cases. If you need numerous, long running, concurrent connections, Gunicorn provides an asynchronous worker using gevent."

まず既定の sync ワーカーで十分です。sync ワーカーは「1 ワーカーが 1 リクエストを最後まで処理する」モデルで、CPU バウンドな処理や、レスポンスが速い一般的な API に最適です。木材流通 SaaS の 221 エンドポイントも、ほとんどが sync ワーカーで問題なく捌けました。

gevent ワーカーが効くのは、**「大量の・長時間の・同時接続」**が要件のときだけです。具体的には、外部 API のレスポンス待ちが長い、ロングポーリングや SSE のような接続を多数維持する、といった IO 待ちが支配的なワークロードです。

# gevent ワーカー(IO 待ちが支配的なときだけ)
gunicorn -k gevent 'myapp:create_app()'

gevent を使う場合、依存に greenlet>=1.0 が必要です。

ここで絶対に混同してはならない点があります。公式が明示的に釘を刺しています。

"This is NOT the same as Python's async/await, or the ASGI server spec."

つまり gevent ワーカーは Python の async/await でも ASGI でもありませんgevent はグリーンレットによる協調的マルチタスクで IO 待ちを多重化する仕組みであり、async def ビュー(§7 で扱う)とも、FastAPI のような ASGI フレームワークとも別物です。「非同期にしたいから gevent」という安易な選択は、コードの前提を壊します。gevent を入れるなら、ブロッキング呼び出しが monkey-patch で協調スケジューリングされる前提を理解した上で導入してください。

💡 スレッド・eventlet について:Gunicorn には --threads(ワーカーあたりのスレッド数)や eventlet ワーカーもありますが、これらは Flask の公式ドキュメントには載っていません。Gunicorn 自身のドキュメント(docs.gunicorn.org)の領分です。本記事で --threadseventlet に触れる場合は「Flask の指針」ではなく「Gunicorn の機能」として区別してください。Flask 公式が言及するのは sync と gevent だけです。

3.4 バインドと「root で動かさない」

Gunicorn を外部からアクセス可能にするには -b--bind)でバインド先を指定します。

gunicorn -w 4 -b 0.0.0.0 'myapp:create_app()'

ここに 2 つの重大な警告があります。いずれも公式が verbatim で注意しているものです。

"Gunicorn should not be run as root..."

Gunicorn を root で動かしてはいけません。 万一プロセスが乗っ取られたとき、root 権限なら被害が全権限に及びます。最小権限の原則として、専用の非特権ユーザーで起動します(Docker での具体策は §5)。

"Don't [bind to 0.0.0.0] when using a reverse proxy setup, otherwise it will be possible to bypass the proxy."

リバースプロキシの背後では 0.0.0.0 にバインドしてはいけません。 0.0.0.0 は全ネットワークインターフェースで待ち受けるため、プロキシを経由せず Gunicorn に直接到達できてしまい、プロキシで掛けている認証・WAF・ヘッダ整形をすべてバイパスされます。プロキシ背後では、プロキシからのみ到達できるアドレス(Unix ソケットや 127.0.0.1、あるいはコンテナ内部ネットワークの専用ポート)にバインドします。

# プロキシと同一ホストなら localhost に閉じる
gunicorn -w 4 -b 127.0.0.1:8000 'myapp:create_app()'

# Unix ソケット(nginx と同居する典型)
gunicorn -w 4 -b unix:/run/myapp.sock 'myapp:create_app()'

⚠️ コンテナでの 0.0.0.0 は文脈次第。ECS/Fargate のように「ALB → コンテナの特定ポート」だけが疎通経路で、コンテナのネットワークがセキュリティグループで閉じられている場合、コンテナ内で 0.0.0.0:8000 にバインドすること自体は一般的です。危険なのは「0.0.0.0 でバインドしつつ、そのポートが外部から直接届く」状態です。ネットワーク境界(SG・VPC)でプロキシ以外からの直接到達を遮断できているかが判断基準になります。

3.5 アクセスログとタイムアウト

Gunicorn のアクセスログは既定でオフです。コンテナ運用では標準出力にログを集約するのが定石なので、--access-logfile=-- は stdout)で有効化します。

gunicorn -w 4 --access-logfile=- 'myapp:create_app()'

タイムアウトは 2 種類を区別します。

  • --timeout(既定 30 秒):ワーカーがこの秒数応答しないと、マスタがワーカーを kill して再起動します。長時間処理がある場合は延ばしますが、安易に長くすると詰まったワーカーが居座るので、長時間処理はワーカー内で同期実行せず非同期ジョブ(キュー)に逃がすのが筋です。
  • --graceful-timeout(既定 30 秒):再起動・シャットダウン時に、処理中のリクエストを捌き切るのを待つ猶予。グレースフルシャットダウン(§6)の中核パラメータです。

3.6 gunicorn.conf.py:設定をコードで管理する

コマンドラインのフラグが増えてきたら、設定ファイル gunicorn.conf.py に集約します。CLI に散らすより、レビューしやすく再現性が高い構成です。

# gunicorn.conf.py — 本番設定をコードで一元管理
import multiprocessing
import os

# バインド:コンテナ内部ポート。外部到達はネットワーク境界(ALB/SG)で制御
bind = os.getenv("GUNICORN_BIND", "0.0.0.0:8000")

# ワーカー数:CPU×2 を出発点に、環境変数で上書き可能に
workers = int(os.getenv("GUNICORN_WORKERS", multiprocessing.cpu_count() * 2))

# ワーカー種別:既定は sync。IO 待ちが支配的なら "gevent" を環境変数で指定
worker_class = os.getenv("GUNICORN_WORKER_CLASS", "sync")

# ログはすべて stdout/stderr へ(コンテナのログドライバが集約)
accesslog = "-"
errorlog = "-"
loglevel = os.getenv("GUNICORN_LOG_LEVEL", "info")

# タイムアウト:詰まったワーカーを kill。長時間処理はキューに逃がす前提
timeout = int(os.getenv("GUNICORN_TIMEOUT", "30"))

# グレースフルシャットダウン猶予(§6)。ALB のドレイン時間と整合させる
graceful_timeout = int(os.getenv("GUNICORN_GRACEFUL_TIMEOUT", "30"))

# プロセス名(ps で見分けやすく)
proc_name = "myapp"
# 設定ファイルは自動で読まれる(カレントの gunicorn.conf.py)
gunicorn 'myapp:create_app()'

# 明示する場合
gunicorn -c gunicorn.conf.py 'myapp:create_app()'

ログの構造化やリクエスト ID の付与といった可観測性の設計は、エラー処理・可観測性ガイド に分けています。Gunicorn のアクセスログと、Flask アプリ側の構造化ログを「同じ相関 ID で串刺しにする」のが本番の肝です。


4. リバースプロキシ/ロードバランサの背後で動かす

4.1 なぜプロキシを前に置くのか

WSGI サーバーには HTTP サーバーが内蔵されています。しかし公式はこう述べます。

"WSGI servers have HTTP servers built-in. However, a dedicated HTTP server may be safer, more efficient, or more capable. Putting an HTTP server in front of the WSGI server is called a 'reverse proxy.'"

専用の HTTP サーバー(リバースプロキシ)を前に置くと、より安全・効率的・高機能になる——TLS 終端、静的ファイル配信、レート制限、バッファリング、ヘルスチェックなどをプロキシ層に任せ、Gunicorn はアプリ処理に専念できます。前段の選択肢として公式は nginx・Apache httpd を挙げ、PaaS(Cloud Run・Elastic Beanstalk・App Engine・Azure など)も同様にプロキシ構成だと述べています。そして重要な一文があります。

"You'll probably need to Tell Flask it is Behind a Proxy when using most hosting platforms."

「ほとんどのホスティングではプロキシの背後にいることを Flask に教える必要がある」——これが次の ProxyFix です。

筆者の構成は API Gateway → ALB → ECS(Fargate) という多段プロキシでした。クライアントとアプリの間に複数の中継があるため、ProxyFix の設定は特に慎重さが要りました。

4.2 ProxyFix:プロキシ段数を正しく教える

プロキシの背後では、クライアントの本来の情報(送信元 IP・プロトコル・ホスト)は X-Forwarded-For / X-Forwarded-Proto / X-Forwarded-Host といったヘッダに格納されてアプリへ届きます。Flask(Werkzeug)にこれを正しく解釈させるのが ProxyFix ミドルウェアです。公式が示す形がこれです。

from werkzeug.middleware.proxy_fix import ProxyFix

app.wsgi_app = ProxyFix(
    app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1
)

各引数 x_* は、**「その X-Forwarded-* ヘッダをセットしているプロキシの段数(COUNT)」**です。

引数対応ヘッダ意味
x_forX-Forwarded-Forクライアントの送信元 IP(request.remote_addr に反映)
x_protoX-Forwarded-Proto元のスキーム(http/https
x_hostX-Forwarded-Host元の Host ヘッダ
x_prefixX-Forwarded-Prefixプロキシが剝がしたパスプレフィックス

そして公式の警告が、この設定の本質を突いています。

"This middleware should only be used if the application is actually behind a proxy, and should be configured with the number of proxies that are chained in front of it. Since incoming headers can be faked, you must set how many proxies are setting each header so the middleware knows what to trust. ... It can be a security issue if you get this configuration wrong."

要点は 3 つです。

  1. 実際にプロキシの背後にいるときだけ使う。 プロキシがないのに ProxyFix を入れると、クライアントが送った偽の X-Forwarded-* を信じてしまいます。
  2. 前段のプロキシ段数を正確に設定する。 ヘッダは偽装可能なので、「何段のプロキシが各ヘッダをセットしているか」を x_for 等の数値で固定し、その分だけ末尾から信頼します。
  3. 設定を間違えるとセキュリティ問題になる。 段数を多く見積もると、クライアントが注入した偽 IP を本物として扱ってしまい、IP ベースのレート制限・監査ログ・アクセス制御がすべて欺かれます。

⚠️ 段数の数え方を絶対に間違えないx_for=1 は「X-Forwarded-For をセットするプロキシが 1 段」という意味です。ALB だけが前段なら x_for=1API Gateway → ALB の 2 段がいずれも X-Forwarded-For を積むなら、構成を実測した上で適切な段数(多くの場合 x_for=2)を設定します。「とりあえず全部 1」や「とりあえず大きい数」は禁物で、自分のプロキシチェーンが各ヘッダを何段で積んでいるかを実際のリクエストヘッダで確認してから数値を決めてください。筆者は本番相当の環境で X-Forwarded-For の実値を観測し、段数を確定させました。

4.3 ALB・nginx へのマッピング

ProxyFix の設定は、前段の構成にそのまま対応します。

  • AWS ALB の背後(ECS/Fargate):ALB は TLS を終端し、X-Forwarded-For / X-Forwarded-Proto を付与します。ALB の前にさらに API Gateway や CloudFront がいて、それらも X-Forwarded-For を積むなら、その合計段数を x_for に設定します。TLS は ALB で終端されるため、Gunicorn・Flask には平文(HTTP)で届きます。だからこそ x_proto で「元は HTTPS だった」ことを ProxyFix 経由で Flask に教える必要があります。
  • nginx の背後(同一ホスト or VM):nginx で proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 等を設定し、Gunicorn は 127.0.0.1 か Unix ソケットで待ち受け、ProxyFixx_for=1(nginx 1 段)にします。

4.4 TLS 終端時の Cookie・Host 検証

プロキシで TLS を終端する構成では、Flask 側のセキュリティ設定を 2 つ忘れずに固めます。

  • SESSION_COOKIE_SECURE = True:本番は HTTPS 前提なので、セッション Cookie に Secure 属性を付け、平文では送らせません。TLS は ALB/nginx で終端されますが、x_proto で元が HTTPS だと Flask に伝わっていれば、Secure Cookie は正しく機能します。
  • TRUSTED_HOSTS(Flask 3.1 で追加):ルーティング時に Host ヘッダを検証し、Host ヘッダ攻撃(偽の Host でリンク生成やキャッシュ汚染を狙う)を防ぎます。プロキシの背後では Host が外から操作され得るので、許可ホストを明示します。
app.config.update(
    SESSION_COOKIE_SECURE=True,    # HTTPS 限定 Cookie
    SESSION_COOKIE_HTTPONLY=True,
    SESSION_COOKIE_SAMESITE="Lax",
    TRUSTED_HOSTS=["api.example.com"],  # Host ヘッダ検証(3.1+)
)

Cookie 属性・CSRF・TRUSTED_HOSTS の詳細な設計は セキュリティ実装ガイド で深掘りしています。

💡 SERVER_NAME の挙動変化(Flask 3.1):Flask 3.1 では SERVER_NAME を設定しても、host_matching=Truesubdomain_matching=False のときにリクエストをそのドメインに制限しなくなりました。本番で「特定ホストだけ受け付けたい」目的には SERVER_NAME ではなく TRUSTED_HOSTS を使うのが正解です。


5. Docker:本番コンテナを正しく組む

ECS/Fargate のようなコンテナ基盤に載せるなら、イメージの作り方が本番品質を左右します。要件は「小さい・非 root・開発サーバーを含まない・ヘルスチェックを持つ」です。マルチステージビルドでこれらを満たします。

# syntax=docker/dockerfile:1

# ---- builder:依存をビルド・インストールするステージ ----
FROM python:3.12-slim AS builder

# ビルド時のみ必要なツールはこのステージに閉じ込める
ENV PIP_NO_CACHE_DIR=1 \
    PIP_DISABLE_PIP_VERSION_CHECK=1

WORKDIR /app

# 仮想環境に依存をインストール(次ステージへ丸ごとコピーする)
RUN python -m venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH"

COPY requirements.txt .
RUN pip install -r requirements.txt gunicorn

# ---- runtime:実行に必要なものだけの最終ステージ ----
FROM python:3.12-slim AS runtime

# 非 root ユーザーを用意(§3.4 の「root で動かさない」を満たす)
RUN useradd --create-home --uid 10001 appuser

# builder からインストール済みの仮想環境だけを持ち込む
COPY --from=builder /opt/venv /opt/venv
ENV PATH="/opt/venv/bin:$PATH" \
    PYTHONUNBUFFERED=1 \
    PYTHONDONTWRITEBYTECODE=1

WORKDIR /app
COPY --chown=appuser:appuser src/ ./src/
COPY --chown=appuser:appuser gunicorn.conf.py ./

# ここで root を捨てる。以降のプロセスは非特権ユーザー
USER appuser

EXPOSE 8000

# /health を叩いて生存確認(§6 の readiness/liveness と連動)
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
    CMD python -c "import urllib.request,sys; sys.exit(0 if urllib.request.urlopen('http://127.0.0.1:8000/health').status==200 else 1)"

# 開発サーバーは絶対に使わない。Gunicorn でファクトリを起動
CMD ["gunicorn", "-c", "gunicorn.conf.py", "myapp:create_app()"]

ポイントを整理します。

  • マルチステージbuilder でコンパイラやビルドツールを使い、runtime には仮想環境(/opt/venv)だけを持ち込みます。最終イメージにビルドツールが残らず、サイズと攻撃面が縮みます。
  • slim ベースpython:3.12-slim で土台を小さく。alpine は musl libc 由来で psycopg などのビルドに苦労することがあるため、Python では slim が無難です。
  • 非 root USERuseradd で作った appuserUSER で切り替え、§3.4 の「root で動かさない」をコンテナレベルで満たします。
  • 開発サーバーを含まないCMD は Gunicorn。flask runapp.run() も登場しません。
  • HEALTHCHECK/health エンドポイント(ピラー §3 で定義した @app.route("/health"))を叩いて 200 を確認します。ECS では別途タスク定義側のヘルスチェックも設定しますが、Docker レベルでも持たせておくと local/compose でも有効です。

5.1 .dockerignore

ビルドコンテキストを汚さず、秘密や不要物を絶対にイメージへ入れないために .dockerignore を必ず置きます。

.git
.gitignore
__pycache__/
*.pyc
.venv/
venv/
.env
.env.*
instance/
tests/
.pytest_cache/
*.md
Dockerfile
.dockerignore

⚠️ .envinstance/ を必ず除外する.env(秘密)や instance/(ローカルの設定・SQLite)がイメージに焼き込まれると、レジストリに秘密が漏れます。秘密はイメージに入れず、実行時に環境変数で注入します。

5.2 設定は FLASK_ 前綴りの環境変数で注入する

秘密や環境差分はイメージに焼かず、実行時に環境変数で渡すのが 12-factor の原則です。Flask 3.0 の from_prefixed_env() を使えば、FLASK_ で始まる環境変数を自動で app.config に取り込めます(値は json.loads で型付け)。設定管理の全体像はピラー Flask 本番運用ガイド §4 を参照してください。

# ECS のタスク定義 / Secrets Manager から注入する想定
docker run --rm -p 8000:8000 \
  -e FLASK_SECRET_KEY="$(python -c 'import secrets; print(secrets.token_hex())')" \
  -e FLASK_SQLALCHEMY_DATABASE_URI="postgresql+psycopg://..." \
  -e GUNICORN_WORKERS=8 \
  myapp:latest

このコンテナを Fargate タスクとして動かし、ALB のターゲットに登録する具体的な手順は ECS Fargate 本番ガイド に集約しています。

💡 DB コネクションプールとワーカー数の掛け算に注意:Gunicorn は各ワーカーが独立プロセスなので、SQLAlchemy のコネクションプールもワーカーごとに別々に持ちますpool_size=5 × ワーカー 8 個 × タスク 4 個 = 最大 160 接続が、PostgreSQL の max_connections を食い潰す——これが本番で頻発する事故です。複数ワーカー × 複数タスク構成では、PgBouncer のような接続プーラを挟むのが定石です。詳細は PostgreSQL コネクションプーリングガイド にまとめています。


6. グレースフルシャットダウン:ゼロダウンタイムの核心

デプロイのたびに 502/504 が出るなら、原因はほぼ「処理中のリクエストを切ってコンテナを落としている」ことです。ゼロダウンタイムは、**「新しいコンテナが受け付け可能になってから、古いコンテナが処理中のリクエストを捌き切って静かに退場する」**ことで実現します。

6.1 SIGTERM と graceful-timeout

コンテナオーケストレータ(ECS・Kubernetes)は、コンテナを止めるときまず SIGTERM を送ります。Gunicorn はこれを受けると、

  1. 新規リクエストの受付を止める、
  2. 処理中(インフライト)のリクエストを --graceful-timeout の猶予内で捌き切る
  3. 全ワーカーが片付いたらプロセスを終了する、

という順でグレースフルに落ちます。猶予を過ぎても終わらないワーカーは強制終了されます。

# gunicorn.conf.py(再掲・抜粋)
# ALB のデレジスタ遅延(deregistration delay)と整合させる
graceful_timeout = int(os.getenv("GUNICORN_GRACEFUL_TIMEOUT", "30"))

ここで重要なのは、Gunicorn の graceful_timeout と、ロードバランサのドレイン時間(ALB の deregistration delay)を整合させることです。ALB がターゲットへの新規ルーティングを止めてから、インフライトを捌き切るまでの猶予が、Gunicorn 側の猶予と食い違うと、捌き切る前にプロセスが消える/ALB がまだ振り続けるといったズレが生じます。

💡 SIGTERM で落とせるよう、PID 1 を Gunicorn にする。Docker で CMD ["gunicorn", ...](exec 形式)にすると Gunicorn が PID 1 になり、SIGTERM を直接受け取れます。シェル形式(CMD gunicorn ...)にするとシェルが PID 1 になり、シグナルが Gunicorn に伝わらずグレースフルシャットダウンが効かなくなる——これはコンテナでありがちな落とし穴です。本記事の Dockerfile が exec 形式(JSON 配列)なのはこのためです。

6.2 アプリ側で SIGTERM の後始末をする場合

通常は Gunicorn のグレースフル処理に任せれば十分ですが、ワーカー終了時に明示的な後始末(接続のクローズ等)が要る場合は、gunicorn.conf.py のフック(worker_exit など)か、アプリ側で teardown_appcontext(ピラー §5 参照)でリソースを閉じます。DB 接続のように「リクエストの寿命に縛るべきリソース」は、そもそも gteardown_appcontext で管理しておけば、シャットダウン時に個別対応する必要はありません。

6.3 readiness と liveness を分ける

ヘルスチェックは目的によって 2 種類に分けます。混同すると、起動途中のコンテナにトラフィックが流れたり、一時的に遅いだけのコンテナが殺されたりします。

種類問い失敗時の挙動チェック内容
readiness「今リクエストを受けられるか?」ロードバランサがトラフィックを送らないDB 接続・必須依存の疎通まで含めて確認
liveness「プロセスは生きているか?」コンテナを再起動するプロセスが応答するかの軽量チェック
# liveness:軽量。プロセスが応答すれば 200
@app.route("/health")
def liveness():
    return {"status": "ok"}


# readiness:依存の疎通まで確認。起動直後や DB 断のときは 503
@app.route("/ready")
def readiness():
    try:
        db.session.execute(text("SELECT 1"))
    except Exception:
        return {"status": "not ready"}, 503
    return {"status": "ready"}

⚠️ liveness で重い依存チェックをしない。liveness に DB 疎通を入れると、DB が一時的に遅いだけでコンテナが「死んだ」と判定され再起動ループに陥ります。DB 断は readiness(トラフィックを止める)で扱い、liveness はプロセスの生死だけを見る——この分離が安定運用の肝です。ヘルスチェックの設計は エラー処理・可観測性ガイド でも扱っています。

6.4 ローリングデプロイ(ECS)に結びつける

ここまでを ECS のローリングデプロイに当てはめると、ゼロダウンタイムの流れはこうなります。

  1. ECS が新タスク(新イメージのコンテナ)を起動する。
  2. ALB が readiness(/ready で「受付可能」と確認できたら、新タスクをターゲットに登録しトラフィックを流し始める。
  3. 旧タスクへ SIGTERM。ALB はデレジスタを開始し、新規リクエストを旧タスクへ送るのを止める。
  4. 旧タスクの Gunicorn が graceful_timeout の猶予でインフライトを捌き切り、静かに終了する。

この 4 ステップが噛み合えば、デプロイ中にユーザーがエラーを見ることはありません。Fargate でのタスク定義・サービス・デプロイ設定の具体は ECS Fargate 本番ガイド を参照してください。


7. async def ビュー:使ってよいとき・使ってはいけないとき

Flask は 2.0 から async def ビューに対応します(pip install flask[async] が必要)。しかし、これをデプロイ性能の改善策と誤解すると痛い目を見ます。公式の注意が決定的です。

async ビューでも、各リクエストは依然として 1 ワーカーを占有します。 async にしても同時に捌けるリクエスト数は増えませんasync が効くのは「1 つのビューの中で複数の IO(外部 API 呼び出しなど)を並行実行する」場合だけで、スループット(同時リクエスト数)の向上ではないのです。

import asyncio
import httpx


@app.route("/aggregate")
async def aggregate():
    # 1 ビュー内で 3 つの外部 API を並行呼び出し → ここでは async が効く
    async with httpx.AsyncClient() as client:
        a, b, c = await asyncio.gather(
            client.get("https://api.example.com/a"),
            client.get("https://api.example.com/b"),
            client.get("https://api.example.com/c"),
        )
    return {"a": a.json(), "b": b.json(), "c": c.json()}

さらに制約があります。asyncio.create_task() でバックグラウンドタスクを起こしても、ビューが返った時点でキャンセルされます。Flask の async ビューは「リクエスト処理中の IO 並行」のためのものであって、ファイア・アンド・フォーゲットなバックグラウンド処理の置き場ではありません。

判断はシンプルです。

  • 使ってよい:1 ビュー内で複数の独立した外部 IO を並行化して、そのビューのレイテンシを縮めたいとき。
  • 使ってはいけない/別解を取るべき:アプリ全体を非同期前提にしたいとき、大量の同時接続を捌きたいとき、バックグラウンドジョブを走らせたいとき。

アプリ全体を非同期ファーストにしたいなら、公式は ASGI 前提の Quart(Flask 互換 API を持つ)を勧めています。あるいは既存の Flask アプリを ASGI サーバー下で動かすなら、asgirefWsgiToAsgi アダプタを使う手があります。大量同時接続なら §3.3 の Gunicorn gevent ワーカーという選択肢もありますが、これは async/await とは別物(§3.3)なので混同しないこと。Flask・FastAPI・Django の使い分けそのものは 技術選定ガイド にまとめています。


8. 本番チェックリスト

ここまでの設計を、デプロイ前に必ず確認する形にまとめます。

カテゴリチェック項目根拠(本文)
サーバーflask run / app.run() を本番経路から排除した§1
サーバーWSGI サーバー(Linux なら Gunicorn)を選定した§2
Gunicornワーカー数を CPU × 2 から設定し、メモリ上限に収まる§3.2
Gunicornワーカー種別を要件で選んだ(既定 sync、IO 集中なら gevent)§3.3
Gunicorngevent を async/await・ASGI と混同していない§3.3
Gunicornroot で動かしていない(非 root ユーザー)§3.4 / §5
Gunicornプロキシ背後で 0.0.0.0 直公開になっていない§3.4
Gunicornアクセスログを stdout に出している(--access-logfile=-§3.5
プロキシProxyFix を入れ、x_for 等をプロキシ段数で正しく設定した§4.2
プロキシProxyFix の段数を実リクエストヘッダで検証した§4.2
プロキシSESSION_COOKIE_SECURE=TrueTRUSTED_HOSTS を設定した§4.4
Dockerマルチステージ・slim ベース・非 root USER§5
Docker開発サーバーを含まず CMD は Gunicorn(exec 形式)§5 / §6.1
Docker.dockerignore.envinstance/ を除外した§5.1
Docker秘密はイメージに焼かず環境変数で注入(FLASK_ 前綴り)§5.2
DBワーカー数 × タスク数 × プールサイズが max_connections§5.2
停止SIGTERM でグレースフルに落ち、PID 1 が Gunicorn§6.1
停止graceful_timeout と ALB のドレイン時間を整合させた§6.1
停止readiness と liveness を分離した§6.3
asyncasync def ビューの「1 ワーカー占有」制約を理解している§7

まとめ:デプロイは「アプリの外側」の設計である

Flask の本番デプロイで起きる障害のほとんどは、アプリのコードではなく**「アプリを動かす環境の設計」**から生まれます。本記事の規律を一言ずつにまとめます。

  1. 開発サーバーを捨てるflask run / app.run() は本番禁止。Flask は WSGI アプリであり、Gunicorn のような WSGI サーバーが動かす。
  2. Gunicorn を本番品質で設定する。ワーカーは CPU × 2 から、既定 sync で十分、gevent は IO 集中時だけ。root で動かさず、プロキシ背後で直公開しない。
  3. ProxyFix でプロキシ段数を正しく教えるx_for 等は「信頼するプロキシの段数」。間違えれば X-Forwarded-For を偽装され、IP ベースの制御が崩れる。
  4. コンテナを正しく組む。マルチステージ・slim・非 root・開発サーバー排除・/health ヘルスチェック。秘密は焼かず環境変数で注入する。
  5. グレースフルに落とすSIGTERMgraceful_timeout でインフライトを捌き切り、readiness/liveness を分け、ECS のローリングデプロイでゼロダウンタイムを実現する。
  6. async ビューを過信しない。1 ワーカー占有の制約を理解し、非同期ファーストが要件なら Quart/ASGI を検討する。

筆者が API Gateway → ALB → ECS(Fargate) 上で 221 エンドポイントを安定運用できたのは、この「アプリの外側」を一つずつ設計したからです。コードの品質と同じだけ、デプロイの品質に投資してください。全体像は Flask 本番運用ガイド(ピラー) に、各論はそこからリンクした各スポーク記事にあります。

友田

友田 陽大

経済産業大臣賞 受賞プロダクト開発者。TypeScript + Python + AWS で、SaaS・業界DX・ 実用レベルの生成AI(RAG)を、要件定義からインフラ・運用まで一人で完遂します。

この記事で解説した技術の適用事例

経済産業大臣賞受賞 | 木材流通業界のDXを実現したB2BサブスクリプションSaaS

ケーススタディを見る