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

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

- 公開日: 2026-06-22
- 著者: 友田 陽大
- タグ: Python, Flask, Gunicorn, Docker, WSGI, 本番運用, AWS, バックエンド
- URL: https://tomodahinata.com/blog/flask-deployment-gunicorn-docker-production-wsgi-guide

## 要点

- 開発サーバー（flask run / app.run()）は本番禁止。FlaskはWSGI『アプリ』であり、Gunicornのような『サーバー』がHTTP↔WSGIを変換して動かす。この分離がデプロイ設計の出発点
- WSGIサーバーはLinux本番ならGunicornが定番。Windows/クロスプラットフォームならWaitress。ワーカー数の出発点はCPU×2、既定の1では足りない
- リバースプロキシ/ALBの背後ではProxyFixを噛ませ、x_for等を『信頼するプロキシ段数』に正しく設定する。間違えるとX-Forwarded-Forを偽装できる重大なセキュリティ問題になる
- 本番コンテナはマルチステージ・slimベース・非rootユーザー・Gunicornエントリポイント・/healthへのHEALTHCHECKで固める。秘密は環境変数（FLASK_前綴り）で注入する
- SIGTERMとgraceful-timeoutでインフライトのリクエストを捌き切ってから落とす。readiness/livenessを分離し、ECSのローリングデプロイでゼロダウンタイムを実現する

---

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

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

本記事は、[Flask 本番運用ガイド（ピラー）](/blog/flask-production-guide) の §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 層に持ち、`ProxyFix` や `TRUSTED_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 run` も `app.run()` も、この「使ってはならない開発サーバー」です。

```python
# ❌ 本番でこれを起動してはいけない
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 で扱います（ファクトリ自体の設計は [大規模構成ガイド](/blog/flask-application-factory-blueprints-large-app-structure-guide) を参照）。

---

## **2. WSGI サーバーの選定：Gunicorn / Waitress / uWSGI / mod_wsgi**

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

| サーバー | プラットフォーム | 特徴 | 選ぶ場面 |
|---|---|---|---|
| **Gunicorn** | Linux / WSL（**Windows 不可**） | プリフォーク型。ワーカー種別が豊富（sync / gevent 等）。設定が素直 | **Linux 本番のデフォルト。** コンテナ・ECS・EC2 |
| **Waitress** | クロスプラットフォーム（**Windows 可**） | 純 Python 実装。依存ゼロ、スレッドベース | Windows サーバー、純 Python で完結させたいとき |
| **uWSGI** | Linux | 高機能・高性能だが設定が複雑（学習コスト大） | 多言語・高度なチューニングが要る大規模構成 |
| **mod_wsgi** | Apache 同梱 | 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}` という形式です。公式が示す等価関係がそのまま読み方になります。

```bash
# '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"）。あくまで「出発点」であって、ここから負荷試験で調整します。

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

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

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

### 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 待ちが支配的なワークロードです。

```bash
# 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）の領分です。本記事で `--threads` や `eventlet` に触れる場合は「Flask の指針」ではなく「Gunicorn の機能」として区別してください。Flask 公式が言及するのは sync と gevent だけです。

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

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

```bash
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`、あるいはコンテナ内部ネットワークの専用ポート）にバインドします。

```bash
# プロキシと同一ホストなら 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）で有効化します。

```bash
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 に散らすより、レビューしやすく再現性が高い構成です。

```python
# 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"
```

```bash
# 設定ファイルは自動で読まれる（カレントの gunicorn.conf.py）
gunicorn 'myapp:create_app()'

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

ログの構造化やリクエスト ID の付与といった可観測性の設計は、[エラー処理・可観測性ガイド](/blog/flask-error-handling-logging-observability-guide) に分けています。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` ミドルウェアです。公式が示す形がこれです。

```python
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_for` | `X-Forwarded-For` | クライアントの送信元 IP（`request.remote_addr` に反映） |
| `x_proto` | `X-Forwarded-Proto` | 元のスキーム（`http`/`https`） |
| `x_host` | `X-Forwarded-Host` | 元の `Host` ヘッダ |
| `x_prefix` | `X-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=1`。**API 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 ソケットで待ち受け、`ProxyFix` は `x_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` が外から操作され得るので、許可ホストを明示します。

```python
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` の詳細な設計は [セキュリティ実装ガイド](/blog/flask-security-sessions-csrf-secure-cookies-guide) で深掘りしています。

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

---

## **5. Docker：本番コンテナを正しく組む**

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

```dockerfile
# 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 `USER`**：`useradd` で作った `appuser` に `USER` で切り替え、§3.4 の「root で動かさない」をコンテナレベルで満たします。
- **開発サーバーを含まない**：`CMD` は Gunicorn。`flask run` も `app.run()` も登場しません。
- **`HEALTHCHECK`**：`/health` エンドポイント（ピラー §3 で定義した `@app.route("/health")`）を叩いて 200 を確認します。ECS では別途タスク定義側のヘルスチェックも設定しますが、Docker レベルでも持たせておくと local/compose でも有効です。

### 5.1 `.dockerignore`

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

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

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

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

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

```bash
# 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 本番ガイド](/blog/aws-ecs-fargate-production-guide) に集約しています。

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

---

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

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

### 6.1 SIGTERM と graceful-timeout

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

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

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

```python
# 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 接続のように「リクエストの寿命に縛るべきリソース」は、そもそも `g` と `teardown_appcontext` で管理しておけば、シャットダウン時に個別対応する必要はありません。

### 6.3 readiness と liveness を分ける

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

| 種類 | 問い | 失敗時の挙動 | チェック内容 |
|---|---|---|---|
| **readiness** | 「今リクエストを受けられるか？」 | ロードバランサがトラフィックを**送らない** | DB 接続・必須依存の疎通まで含めて確認 |
| **liveness** | 「プロセスは生きているか？」 | コンテナを**再起動する** | プロセスが応答するかの軽量チェック |

```python
# 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 はプロセスの生死だけを見る——この分離が安定運用の肝です。ヘルスチェックの設計は [エラー処理・可観測性ガイド](/blog/flask-error-handling-logging-observability-guide) でも扱っています。

### 6.4 ローリングデプロイ（ECS）に結びつける

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

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

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

---

## **7. `async def` ビュー：使ってよいとき・使ってはいけないとき**

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

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

```python
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 サーバー下で動かすなら、`asgiref` の `WsgiToAsgi` アダプタを使う手があります。大量同時接続なら §3.3 の Gunicorn `gevent` ワーカーという選択肢もありますが、これは `async`/`await` とは別物（§3.3）なので混同しないこと。Flask・FastAPI・Django の使い分けそのものは [技術選定ガイド](/blog/flask-vs-fastapi-vs-django-comparison-guide) にまとめています。

---

## **8. 本番チェックリスト**

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

| カテゴリ | チェック項目 | 根拠（本文） |
|---|---|---|
| サーバー | `flask run` / `app.run()` を本番経路から排除した | §1 |
| サーバー | WSGI サーバー（Linux なら Gunicorn）を選定した | §2 |
| Gunicorn | ワーカー数を `CPU × 2` から設定し、メモリ上限に収まる | §3.2 |
| Gunicorn | ワーカー種別を要件で選んだ（既定 sync、IO 集中なら gevent） | §3.3 |
| Gunicorn | gevent を `async`/`await`・ASGI と混同していない | §3.3 |
| Gunicorn | root で動かしていない（非 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=True`・`TRUSTED_HOSTS` を設定した | §4.4 |
| Docker | マルチステージ・slim ベース・非 root `USER` | §5 |
| Docker | 開発サーバーを含まず `CMD` は Gunicorn（exec 形式） | §5 / §6.1 |
| Docker | `.dockerignore` で `.env`・`instance/` を除外した | §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 |
| async | `async 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. **グレースフルに落とす**。`SIGTERM` と `graceful_timeout` でインフライトを捌き切り、readiness/liveness を分け、ECS のローリングデプロイでゼロダウンタイムを実現する。
6. **`async` ビューを過信しない**。1 ワーカー占有の制約を理解し、非同期ファーストが要件なら Quart/ASGI を検討する。

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