この記事でわかること

  • 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の起動と停止を試してみてください。