FastAPIアプリをGunicornとUvicornで公開する手順|Apacheの後ろで動かすときの注意点
FastAPIのアプリをGunicornとUvicornで公開する手順を解説します。役割の違い、uvicorn-workerでの起動と止め方、リバースプロキシの後ろでhttpsやリダイレクト、サブパスがずれる理由、500・502・404の切り分けを紹介します。
目次
- この記事でわかること
- FastAPIを公開するときの全体像
- FastAPI・Uvicorn・Gunicornの役割の違い
- 検証した版と前提
- 最小のFastAPIアプリを用意する
- Uvicorn単体で動作を確かめる
- GunicornとUvicornのワーカーで起動する
- 古い書き方のuvicorn.workersに注意
- fastapi runやuvicorn --workersも使える
- ワーカーの数と止め方
- リバースプロキシの後ろで動かすときの注意
- httpsの判定とリダイレクト
- サブパスで公開するとき(root_path)
- つまずきやすい点: 500・502・404・リダイレクトの切り分け
- セキュリティと運用の注意
- Claude Codeに任せる範囲の判断
- まとめ
この記事でわかること
- FastAPI・Uvicorn・Gunicornが、それぞれ何を受け持つか
- Gunicornとuvicorn-workerで、FastAPIのアプリを127.0.0.1で動かす手順と止め方
- リバースプロキシの後ろで、https・リダイレクト・サブパスがずれる理由
- 500・502・404・リダイレクトの切り分け方
Apacheの設定そのもの(ProxyPassの書き方・反映・戻し方)はApacheのリバースプロキシで複数のアプリを公開する方法で扱いました。この記事は、その転送先で動くアプリのサーバー側を扱います。
FastAPIを公開するときの全体像
外からの要求はApacheが受け、アプリのサーバーへ転送します。アプリのサーバーは127.0.0.1だけで待ち受け、外から直接は届かないようにします。
ブラウザ
│ https
Apache(リバースプロキシ)
│ http://127.0.0.1:8081
Gunicorn(親プロセス: ワーカーの起動と監視)
├─ ワーカー1(Uvicorn)→ FastAPIのアプリ
└─ ワーカー2(Uvicorn)→ FastAPIのアプリ
Gunicornの公式ドキュメントも、プロキシサーバーの後ろで動かすことを強く勧めています。このサイトのアプリも同じ形で、Gunicornに2つのワーカーを付け、127.0.0.1だけで待ち受けて、手前のApacheから転送しています。
FastAPI・Uvicorn・Gunicornの役割の違い
| 名前 | 受け持つこと |
|---|---|
| FastAPI | アプリを書くためのフレームワーク(ASGIのアプリ) |
| Uvicorn | HTTPの要求を受けて、アプリを呼び出すサーバー |
| Gunicorn | ワーカーのプロセスを起動し、監視し、止める |
| uvicorn-worker | GunicornのワーカーとしてUvicornを動かすパッケージ |
ASGIはWSGIの後継として作られた非同期の仕様です。WSGIは1回の同期の呼び出しで要求を受けて応答を返す形のため、WebSocketのような長く続く接続には向きません。
Gunicornの既定のワーカー(sync)はWSGIのアプリ用です。FastAPIはASGIのアプリなので、-kでワーカーの種類を指定しないと正しく動きません。検証で-kを付けずに起動すると、起動はするのに、ページを開くと500になりました。ログには次の誤りが出ていました。
TypeError: FastAPI.__call__() missing 1 required positional argument: 'send'
検証した版と前提
この記事のコマンドは、2026年10月に、架空の最小のアプリで次の2つの組み合わせを確かめたものです。
| 組み合わせ | 版 |
|---|---|
| 新しく作る場合(この記事の手順) | fastapi 0.143.0・starlette 1.7.0・uvicorn 0.54.0・gunicorn 26.2.0・uvicorn-worker 0.4.0 |
| 以前の版 | fastapi 0.115.0・starlette 0.38.6・uvicorn 0.30.6・gunicorn 23.0.0 |
AlmaLinux 8とPython 3.11のサーバーで、検証専用のディレクトリと仮想環境を作り、127.0.0.1だけで待ち受けて確かめました。2026年10月時点のPyPIでは、最新のfastapi・uvicorn・gunicornはいずれもPython 3.10以上が必要です。版が変わると表示や既定の動きが変わることがあるので、手元の版はpip freezeで確かめてください。
最小のFastAPIアプリを用意する
作業用のディレクトリ(例:myapp)にmain.pyを置き、同じ場所に静的ファイル用のstaticディレクトリを作ってhello.txtを入れます。
【用途】最小のアプリ /【実行場所】サーバーの作業用のディレクトリ /【状態変更】あり(ファイルを作る)
from pathlib import Path
from fastapi import FastAPI
from fastapi.staticfiles import StaticFiles
BASE = Path(__file__).resolve().parent
app = FastAPI()
app.mount("/static", StaticFiles(directory=BASE / "static"), name="static")
@app.get("/")
def index():
return {"message": "hello"}
@app.get("/items/")
def items():
return {"items": ["apple", "banana"]}
仮想環境を作り、必要なものを入れます。
【用途】仮想環境と依存の準備 /【実行場所】サーバーの作業用のディレクトリ /【状態変更】あり(仮想環境を作り、パッケージを入れる)
python3.11 -m venv .venv
source .venv/bin/activate
pip install "fastapi[standard]" gunicorn uvicorn-worker
pip install "fastapi[standard]"は、アプリを動かすサーバーのUvicornやfastapiコマンドなどの標準の依存を一緒に入れます。pip install fastapiだけでは、これらは入りません。検証でも、fastapi 0.115.0だけを入れた環境では、fastapiコマンドが「fastapi[standard]を入れてください」と表示して止まりました。
仮想環境は有効にしなくても、.venv/bin/gunicornのようにパスを指定すれば使えます。
Uvicorn単体で動作を確かめる
まず、使うポートが空いているかを確かめます。この記事では例として8081を使いますが、いつも空いているとは限りません。
【用途】空いているポートの確認 /【実行場所】サーバー /【状態変更】なし(読み取り)
ss -ltnp
一覧に:8081が無ければ使えます。このサイトでも、使おうとしたポートを別のアプリが使っていて、ポートを変えたことがあります。
【用途】Uvicorn単体で起動する /【実行場所】サーバー(1つ目のターミナル) /【状態変更】あり(127.0.0.1でアプリを動かす)
uvicorn main:app --host 127.0.0.1 --port 8081
【用途】応答を確かめる /【実行場所】サーバー(2つ目のターミナル) /【状態変更】なし(読み取り)
curl -i http://127.0.0.1:8081/
curl -i http://127.0.0.1:8081/static/hello.txt
-iを付けると、応答のヘッダーも表示されます。どちらも200 OKが返れば準備はできています。確かめたら、1つ目のターミナルで動いているUvicornを止めます。
Uvicornの--hostの既定は127.0.0.1です。0.0.0.0を指定すると、同じネットワークの機器からも届くようになります。リバースプロキシの後ろで動かすときは使いません。
GunicornとUvicornのワーカーで起動する
本番では、Gunicornにワーカーの管理を任せ、ワーカーとしてUvicornを動かします。ワーカーの種類には、uvicorn-workerパッケージのuvicorn_worker.UvicornWorkerを指定します。
【用途】Gunicornで起動する /【実行場所】サーバー /【状態変更】あり(127.0.0.1でアプリを動かす)
gunicorn -k uvicorn_worker.UvicornWorker -w 2 -b 127.0.0.1:8081 main:app
-k:ワーカーの種類。付け忘れると既定のsyncになり、FastAPIは500になります-w 2:ワーカーの数-b 127.0.0.1:8081:待ち受けるアドレスとポート。外からはApacheを通します
検証では、親プロセス1つとワーカー2つの計3つのプロセスで動き、/へ200が返りました。
古い書き方のuvicorn.workersに注意
-k uvicorn.workers.UvicornWorkerという書き方もあります。Uvicornの公式では、uvicorn.workersは0.30.0で非推奨になり、uvicorn-workerパッケージを使うよう案内されています。
検証では、この古い書き方でも動き、既定の設定では非推奨の警告も表示されませんでした(PYTHONWARNINGS=defaultにすると表示されます)。動いてしまうので気づきにくく、新しく作るときはuvicorn-workerを使います。このサイトのアプリは、まだ古い書き方のまま動いています。
fastapi runやuvicorn --workersも使える
FastAPIの公式ドキュメントは、fastapi runまたはuvicornの--workersで複数のワーカーを動かす方法を説明しています。検証ではuvicorn --workers 2と、fastapi[standard]を入れた環境のfastapi run --workers 2も200を返しました。この記事では、ワーカーの監視と穏やかな停止をGunicornに任せる構成を主にしています。
ワーカーの数と止め方
Gunicornの公式ドキュメントでは、ワーカーの数の目安は2〜4 × CPUのコア数です。小さなサーバーでは2つ程度から始め、メモリの使い方を見て調整します。
止めるときは、Gunicornの親プロセスへTERMを送ります。親プロセスは、ワーカーの処理が終わるのを待ってから止まります。親プロセスの番号は、起動したときのログのListening at:の行の括弧の中に出ます(Booting workerの行はワーカーの番号です)。
【用途】Gunicornを止める /【実行場所】サーバー /【状態変更】あり(アプリを止める)
kill -TERM 親プロセスの番号
ss -ltnp
検証では、親プロセスとワーカーのまとまりへTERMを送ると、すべてのプロセスが止まり、ログの最後にShutting down: Masterと出て、ポートの待ち受けもなくなりました。上の親プロセスだけへTERMを送るコマンドは、Gunicornの公式の説明に基づくもので、この形のままでは検証していません。止めた後にss -ltnpで:8081が消えたことを確かめます。
リバースプロキシの後ろで動かすときの注意
httpsの判定とリダイレクト
Apacheでhttpsを受けて、アプリへはhttpで転送する構成では、アプリから見ると要求はhttpです。元がhttpsだったことはX-Forwarded-Protoのヘッダーで伝えます。Apacheのmod_proxyが自動で付けるヘッダーにX-Forwarded-Protoは含まれないため、必要ならRequestHeaderで付けます。
検証では、Uvicornは既定で127.0.0.1から届いたX-Forwarded-ProtoとX-Forwarded-Forを使い、信頼する送り元から127.0.0.1を外すとhttpのままでした。Gunicornと組み合わせたときは、Gunicornの--forwarded-allow-ipsと環境変数FORWARDED_ALLOW_IPSのどちらでも結果が変わりました。
末尾のスラッシュへのリダイレクト(/itemsから/items/へ)では、LocationのhttpかhttpsかはX-Forwarded-Protoで、ホストはHostのヘッダーで決まり、X-Forwarded-Hostは使われませんでした。そのため、Apache側で元のHostを渡すProxyPreserveHost Onが必要です。ProxyPreserveHostを有効にすると、ProxyPassに書いたホスト名ではなく、元の要求のHostが転送先へ渡されます。
ProxyPreserveHost On
RequestHeader set X-Forwarded-Proto "https"
この2行は、ProxyPassで転送する設定と同じ場所に書きます。このサイトのApacheの設定にも、ProxyPreserveHost Onと、X-Forwarded-Protoをhttpsにする設定があります。ただし、Apacheを経由したアプリの動きは、この記事の検証では確かめていません。ProxyPassの書き方と、設定の変更・反映・戻し方はApacheのリバースプロキシの記事の手順で行ってください。
信頼する送り元は127.0.0.1だけにします。Gunicornの公式ドキュメントは、--forwarded-allow-ipsを'*'にするのは、信頼できない送り元がGunicornに直接届く場合は危険だとしています。
サブパスで公開するとき(root_path)
https://ドメイン/myapp/のようなサブパスで公開し、Apacheで先頭の/myappを取り除いて転送すると、アプリには/static/hello.txtのように先頭のない形で届きます。このときアプリに先頭を教えるのがroot_pathです。
検証した2つの版の組み合わせでは、指定のしかたで結果が違いました。
先頭を取り除いた/static/hello.txtへの要求と、/itemsへのリダイレクト先を比べた結果です。
| 設定 | 結果 |
|---|---|
| 指定なし | 静的ファイル200・リダイレクト先 /items/ |
FastAPI(root_path="/myapp") |
静的ファイル404・リダイレクト先 /items/(/myappが付かない) |
uvicorn --root-path /myapp |
静的ファイル200・リダイレクト先 /myapp/items/ |
FastAPIの公式ドキュメントでは2つは同じ意味だと説明されていますが、検証した版ではこのように違いました。全ての版や構成で同じとは限らないため、使う版で確かめてください。このサイトでも、FastAPIのroot_pathにサブパスを渡すと静的ファイルが404になったため、root_pathは空のままにして、リンクに公開URLを明示する作りにしました。本文のリンクからサブパスが抜けて404になったこともあります。
Gunicornとuvicorn-workerの構成でサブパスを渡す方法は、この記事では確かめていません。この記事のGunicornの手順は、サブドメインのように、アプリがルート(/)で受ける構成を前提にしています。
つまずきやすい点: 500・502・404・リダイレクトの切り分け
- 500:
-kの付け忘れや、アプリの例外。ログの誤りを見る - 502:アプリが動いていない、ポートが違う。
ss -ltnpと、サーバー内からのcurl -iで確かめる - 404:サブパスとroot_pathの組み合わせ。先頭を取り除いた形で要求してみる
- リダイレクトがhttpや127.0.0.1になる:X-Forwarded-ProtoとHostが届いていない。Apacheの設定を見る
502はApacheが転送先につながらないときに出ます。まずサーバーの中からcurl -i http://127.0.0.1:8081/で、アプリが直接応答するかを確かめます。Apache側の切り分けはApacheのリバースプロキシの記事のSTEP 7を参考にしてください。
起動のときに止まる場合は、ログの最後の行を見ます。検証では次のように表示されました(ポートの番号は記事の例の8081に置き換えています)。
ERROR: [Errno 98] error while attempting to bind on address ('127.0.0.1', 8081): address already in use
ERROR: Error loading ASGI app. Could not import module "no_such_module".
1行目は使用中のポートで起動したとき、2行目は存在しないモジュールを指定したときです。
サーバーの再起動の後にアプリが起動していないときや、設定のファイルが読まれないときの確かめ方は、サーバー障害の切り分けをClaude Codeと進めるにまとめています。
セキュリティと運用の注意
- アプリのサーバーは127.0.0.1で待ち受け、外からはApacheを通す(
0.0.0.0にしない) - プロキシのヘッダーを信頼する送り元は127.0.0.1だけにする(
'*'にしない) - 新しく作るときのワーカーはuvicorn-workerにする(非推奨のuvicorn.workersを使わない)
- 版を固定し、上げるときは手元で起動と応答を確かめてから本番に入れる
サーバーを再起動したときに自動で起動させる方法(systemdやcronの@reboot)は、この記事では扱いません。このサイトのアプリは、起動用のスクリプトをcronの@rebootから実行しています。
アプリが使うPostgreSQLのユーザーとDBを、必要最小限の権限で作る手順は、AlmaLinuxでPostgreSQLを構築しアプリ用のユーザーとDBを作る手順で紹介しています。
Claude Codeに任せる範囲の判断
Claude Codeに手伝ってもらうときは、まず読み取りだけを頼み、結果を見てから自分で進めます。読み取りの確認の考え方はサーバーの状態確認をClaude Codeに読み取り専用で任せる方法にまとめています。
【用途】起動の前の確認を頼む /【実行場所】Claude Code /【状態変更】なし(読み取りだけ・実行の前に確認を求める)
FastAPIのアプリをGunicornで動かす準備として、サーバーの状態を読み取りだけで確認してください。
- ss -ltnp で、127.0.0.1 の 8081 番台に空いているポートがあるか
- python3.11 があるか、作業用のディレクトリに仮想環境があるか
設定ファイルの変更・パッケージのインストール・プロセスの起動や停止はしないでください。
結果と、次に実行するコマンドの案を報告して、私の確認を待ってください。
サーバー全体の作業の流れはClaude CodeでAlmaLinuxサーバーを構築・運用する方法、Pythonの基本はPythonを学ぶ方法を参考にしてください。
まとめ
- FastAPIはアプリ、Uvicornはサーバー、Gunicornはワーカーの管理を受け持つ
- 新しく作るときは
gunicorn -k uvicorn_worker.UvicornWorkerで起動し、127.0.0.1で待ち受ける。使う前にポートの空きを確かめる -kを付け忘れると500、古いuvicorn.workersは警告が見えないまま動くので注意する- リバースプロキシの後ろでは、X-Forwarded-ProtoとHost(ProxyPreserveHost)が正しく届くようにし、信頼する送り元を127.0.0.1に限る
- サブパスのroot_pathは版や指定のしかたで結果が違ったため、使う版で確かめる
まずはUvicorn単体で応答を確かめ、それからGunicornの起動と停止を試してみてください。
よくある質問
Uvicornだけで本番に出してもよいですか?
FastAPIの公式ドキュメントは、fastapi runやuvicornの--workersで複数のワーカーを動かす方法を説明しています。検証ではuvicorn --workers 2でも応答を確かめました。この記事では、ワーカーの監視と穏やかな停止をGunicornに任せる構成を主にしています。
Gunicornのワーカーの数はいくつにすればよいですか?
Gunicornの公式ドキュメントでは、目安は2〜4 × CPUのコア数とされています。小さなサーバーでは2つ程度から始め、メモリの使い方を見ながら調整してください。
uvicorn.workers.UvicornWorkerは使えなくなったのですか?
Uvicornの公式では、uvicorn.workersは0.30.0で非推奨になり、uvicorn-workerパッケージを使うよう案内されています。検証した版ではまだ動き、既定の設定では非推奨の警告も表示されませんでしたが、新しく作るときはuvicorn-workerを使ってください。
プロキシの後ろでリダイレクト先がhttpになるのはなぜですか?
アプリへはhttpで転送されるため、元がhttpsだったことはX-Forwarded-Protoのヘッダーで伝える必要があります。検証では、127.0.0.1から届いたこのヘッダーが既定で使われ、リダイレクト先がhttpsになりました。ホストはHostのヘッダーで決まりました。
systemdで常駐させるにはどうすればよいですか?
常駐の方法(systemdやcronの@reboot)はこの記事の範囲外です。RHEL 9の公式の手順では、自分で作るユニットは/etc/systemd/system/に置き、作成や変更の後にsystemctl daemon-reloadを実行します。このサイトのアプリは、起動用のスクリプトをcronの@rebootから実行しています。