- 技術 💻
Nginxリバースプロキシ設定|proxy_passの書き方と502/504対処
Nginxのリバースプロキシとは、ブラウザからのリクエストをNginxがいったん受け取り、裏側のアプリケーションサーバー(バックエンド)へ転送する仕組みです。設定の中心は proxy_pass の1行ですが、末尾にスラッシュを付けるかどうかでバックエンドに届くパスが変わる、ヘッダーを設定しないとアプリから見た接続元IPがNginxになる、など、動かしてみて初めて気づくポイントがいくつもあります。
この記事では、実際にNginx(1.24.0)と、受け取ったパスとヘッダーをそのまま返す小さなバックエンドを立て、curlで転送結果を1つずつ確かめました。proxy_passの書き方ごとのパスの違い、ヘッダーの有無による差、負荷分散、WebSocket、タイムアウトで出る504、アップロードサイズ超過の413まで、実際の出力とエラーログつきで解説します。
Nginxリバースプロキシ設定の早見表
| やりたいこと | 設定 |
|---|---|
| 転送する(パスはそのまま) | proxy_pass http://127.0.0.1:3000; |
| 転送する(location部分を置き換える) | proxy_pass http://127.0.0.1:3000/; |
| アプリに本来のホスト名を渡す | proxy_set_header Host $host; |
| アプリに接続元IPを渡す | proxy_set_header X-Real-IP $remote_addr; と X-Forwarded-For $proxy_add_x_forwarded_for; |
| アプリにhttp/httpsを伝える | proxy_set_header X-Forwarded-Proto $scheme; |
| 複数台に振り分ける | upstream app { server ...; server ...; } + proxy_pass http://app; |
| WebSocketを通す | proxy_http_version 1.1; + Upgrade / Connection ヘッダー |
| 応答待ちの時間を延ばす | proxy_read_timeout 120s;(初期値 60秒) |
| アップロード上限を上げる | client_max_body_size 20m;(初期値 1MB) |
| 設定の文法を確認する | sudo nginx -t |
| 設定を反映する(無停止) | sudo nginx -s reload または sudo systemctl reload nginx |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Nginxリバースプロキシ設定|proxy_passの書き方と502/504対処」
リバースプロキシの役割
ブラウザ ──https──▶ Nginx(:443) ──http──▶ アプリ(127.0.0.1:3000)
│ アプリ(127.0.0.1:3001)
└ 静的ファイルは自分で返す
| 役割 | 内容 |
|---|---|
| 入口を1つにする | 80/443番ポートで受け、パスやドメインごとに別のアプリへ振り分ける |
| HTTPS終端 | 証明書の処理をNginxに任せ、アプリはhttpだけ扱えばよくなる |
| 負荷分散 | 同じアプリを複数台に分け、リクエストを振り分ける |
| 静的ファイル配信・キャッシュ | 画像やCSSはNginxが直接返し、アプリの負担を減らす |
| 保護 | アップロードサイズの上限やタイムアウトを入口で決められる |
アプリは 127.0.0.1 だけで待ち受け、外からはNginx経由でだけ届くようにするのが基本の形です。
最小の設定
Ubuntu/Debian系なら /etc/nginx/sites-available/ に設定を書き、sites-enabled/ にシンボリックリンクを張ります。RHEL系は /etc/nginx/conf.d/*.conf に置きます。
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
sudo ln -s /etc/nginx/sites-available/app.conf /etc/nginx/sites-enabled/
sudo nginx -t # 文法チェック
sudo systemctl reload nginx
この4行の proxy_set_header は、ほぼすべての構成で入れておくと考えてかまいません。理由は後の「ヘッダー」の章で実際の出力を見ながら説明します。
proxy_passの末尾スラッシュで、届くパスが変わる
最もよくあるつまずきがここです。公式ドキュメントのルールは次の2つだけです。
- proxy_pass にURI(ホストの後ろの
/以降)がない → リクエストのパスをそのまま転送する - proxy_pass にURIがある(
/だけでもURI) → locationに一致した部分をそのURIに置き換えて転送する
受け取ったパスをそのまま返すバックエンドを 127.0.0.1:9001 に立て、次の設定で確かめました。
location /a/ { proxy_pass http://127.0.0.1:9001; } # スラッシュなし
location /b/ { proxy_pass http://127.0.0.1:9001/; } # スラッシュあり
location /c { proxy_pass http://127.0.0.1:9001/; } # locationにスラッシュなし
location /d/ { proxy_pass http://127.0.0.1:9001/api/; } # 別のパスに置き換え
$ curl -s localhost:8080/a/users
{"backend": 9001, "path": "/a/users", ...}
$ curl -s localhost:8080/b/users
{"backend": 9001, "path": "/users", ...}
$ curl -s localhost:8080/c/users
{"backend": 9001, "path": "/users", ...}
$ curl -s localhost:8080/cx/users
{"backend": 9001, "path": "/x/users", ...}
$ curl -s localhost:8080/d/users
{"backend": 9001, "path": "/api/users", ...}
| location | proxy_pass | リクエスト | バックエンドに届いたパス |
|---|---|---|---|
/a/ |
http://127.0.0.1:9001 |
/a/users |
/a/users(そのまま) |
/b/ |
http://127.0.0.1:9001/ |
/b/users |
/users(/b/ が / に置き換わる) |
/c |
http://127.0.0.1:9001/ |
/c/users |
/users |
/c |
http://127.0.0.1:9001/ |
/cx/users |
/x/users(意図しない一致) |
/d/ |
http://127.0.0.1:9001/api/ |
/d/users |
/api/users |
「/api/ 以下をアプリに渡したい。アプリ側のルートは /users になっている」なら /b/ の形(両方にスラッシュ)、「アプリ側も /api/users で待っている」なら /a/ の形(スラッシュなし)です。
/c のように location側だけスラッシュを省くと、/cx/users のような別のパスまで一致し、/c が / に置き換わって /x/users になりました。location と proxy_pass は、スラッシュの有無をそろえると安全です。
正規表現のlocationではURIを書けない
location ~ ^/re/ のような正規表現のlocationで proxy_pass http://127.0.0.1:9001/api/; と書くと、nginx -t の時点でエラーになります。
nginx: [emerg] "proxy_pass" cannot have URI part in location given by regular expression, or inside named location, or inside "if" statement, or inside "limit_except" block
正規表現で振り分けたいときは、proxy_pass はURIなしにして、パスの書き換えが必要なら rewrite を組み合わせます。
location ~ ^/re/(.*)$ {
rewrite ^/re/(.*)$ /api/$1 break;
proxy_pass http://127.0.0.1:9001;
}
proxy_set_header:アプリに正しい情報を渡す
同じリクエストを、ヘッダー設定なし(/b/)とあり(/h/)で比べました。クライアントは X-Forwarded-For: 203.0.113.9 を付けて送っています(手前にもう1段プロキシがある想定)。
# ヘッダー設定なし
$ curl -s localhost:8080/b/x
{"backend": 9001, "path": "/x", "Host": "127.0.0.1:9001", "X-Real-IP": null, "X-Forwarded-For": null, "X-Forwarded-Proto": null}
# 4つのproxy_set_headerを設定
$ curl -s -H 'X-Forwarded-For: 203.0.113.9' localhost:8080/h/x
{"backend": 9001, "path": "/x", "Host": "localhost", "X-Real-IP": "127.0.0.1", "X-Forwarded-For": "203.0.113.9, 127.0.0.1", "X-Forwarded-Proto": "http"}
| ヘッダー | 設定する値 | 設定しないと | 使いみち |
|---|---|---|---|
Host |
$host |
127.0.0.1:9001(proxy_passに書いた宛先)が届く |
アプリが生成するURL、リダイレクト先、バーチャルホストの判定 |
X-Real-IP |
$remote_addr |
届かない。アプリから見た接続元は常にNginx | アクセスログ、レート制限 |
X-Forwarded-For |
$proxy_add_x_forwarded_for |
届かない | 経由したプロキシを含む接続元の一覧。既存の値の末尾に追記される |
X-Forwarded-Proto |
$scheme |
届かない | アプリが「httpsで来た」ことを知る。リダイレクトのループ防止 |
Hostを設定しないと、アプリが作るリダイレクト先が http://127.0.0.1:9001/... になる、というのはよくある症状です。
X-Forwarded-For はクライアントが自由に書ける値でもあります。アプリ側で「信頼するプロキシを通った分だけ採用する」設定(各フレームワークのtrusted proxy設定や、Nginxの real_ip モジュール)と組み合わせて使います。
upstreamで負荷分散する
upstream app {
server 127.0.0.1:9001;
server 127.0.0.1:9002;
keepalive 16; # バックエンドへの接続を再利用する
}
server {
listen 80;
location / {
proxy_pass http://app;
proxy_http_version 1.1;
proxy_set_header Connection ""; # keepaliveを使うときに必要
}
}
9001と9002の2台で4回リクエストすると、交互に振り分けられました。
$ for i in 1 2 3 4; do curl -s localhost:8080/lb/ | cut -c1-20; done
{"backend": 9001, "p
{"backend": 9002, "p
{"backend": 9001, "p
{"backend": 9002, "p
さらに、停止しているサーバー(9003)を加えても、6回とも200が返りました。エラーログには接続失敗が記録され、Nginxが次のサーバーに自動で再送したことがわかります。
200 200 200 200 200 200
[error] connect() failed (111: Connection refused) while connecting to upstream, ... upstream: "http://127.0.0.1:9003/"
| 振り分け方 | 書き方 | 向いている場面 |
|---|---|---|
| ラウンドロビン | 何も書かない(初期値) | 各サーバーの性能が同じ |
| 重み付け | server 10.0.0.1 weight=3; |
性能の違うサーバーが混ざる |
| 接続数が少ない順 | least_conn; |
処理時間がリクエストごとに大きく違う |
| 接続元IPで固定 | ip_hash; |
セッションをサーバー内に持つアプリ |
| 待機系 | server 10.0.0.9 backup; |
通常時は使わず、ほかが全部落ちたときだけ使う |
| 失敗時の切り離し | server 10.0.0.1 max_fails=3 fail_timeout=30s; |
30秒間に3回失敗したら30秒外す(初期値は1回・10秒) |
WebSocketを通す
WebSocketは、最初のHTTPリクエストで Upgrade: websocket を送り、101 Switching Protocols で接続を切り替えます。Upgrade と Connection は1区間ごとのヘッダー(hop-by-hop)で、Nginxはそのままでは転送しません。
http {
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
location /ws/ {
proxy_pass http://127.0.0.1:9100;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s; # 無通信で切れないように
}
}
}
Pythonの websockets で作ったエコーサーバーを置き、設定あり(/ws/)となし(/ws-ng/、proxy_passだけ)で接続しました。
/ws/ -> echo:hello
/ws-ng/ -> InvalidStatus server rejected WebSocket connection: HTTP 505
"GET /ws/ HTTP/1.1" 101 ...
"GET /ws-ng/ HTTP/1.1" 505 ...
検証したNginx 1.24では、バックエンドへのリクエストが初期値でHTTP/1.0になるため、サーバーが505を返しました。公式ドキュメントによると 1.29.7から proxy_http_version の初期値は1.1 に変わっていますが、Upgrade と Connection の2行は引き続き必要です。バージョンにかかわらず上の設定をそのまま書いておくのが確実です。
map は http ブロックに1回だけ書きます。WebSocketでない普通のリクエストには Connection: close が送られるので、同じlocationで両方を扱えます。
タイムアウトとアップロードサイズ
| ディレクティブ | 初期値 | 意味 | 超えたとき |
|---|---|---|---|
proxy_connect_timeout |
60s | バックエンドとの接続を確立するまで | 504 |
proxy_send_timeout |
60s | リクエストを送る間の、書き込みと書き込みの間隔 | 504 |
proxy_read_timeout |
60s | レスポンスを読む間の、読み込みと読み込みの間隔 | 504 |
client_max_body_size |
1m | リクエスト本文の上限。0 で無制限 |
413 |
proxy_read_timeout はレスポンス全体の時間ではなく「前回受け取ってから次を受け取るまで」の間隔です。少しずつデータを返し続けるストリーミングなら、60秒を超えても切れません。
5秒かかるパスに proxy_read_timeout 2s; を設定すると、ちょうど2秒で504になりました。
$ curl -s -o /dev/null -w '%{http_code} %{time_total}\n' localhost:8080/to/slow
504 2.002518
[error] upstream timed out (110: Connection timed out) while reading response header from upstream, ... upstream: "http://127.0.0.1:9001/slow"
client_max_body_size 1k; の場所に5,000バイトをPOSTすると413です。このときバックエンドにはリクエストが届かず、Nginxの時点で断っています。
$ head -c 5000 /dev/zero | curl -s -o /dev/null -w '%{http_code}\n' --data-binary @- localhost:8080/up/
413
[error] client intended to send too large body: 5000 bytes, ...
アプリ側の上限を上げたのに413が出るときは、Nginxの client_max_body_size を確認します。http・server・location のどこにでも書け、内側の設定が優先されます。
HTTPS終端
証明書をNginxが持ち、アプリにはhttpで転送する構成です。
server {
listen 80;
server_name app.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
http2 on; # 1.25.1以降の書き方
server_name app.example.com;
ssl_certificate /etc/letsencrypt/live/app.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/app.example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
自己署名証明書で動かすと、httpは301でhttpsに移り、アプリには X-Forwarded-Proto: https が届きました(HTTP/2で応答。検証ではX-Real-IPの行を省いた設定で実行したため null になっています)。
$ curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:8080/x
301 https://localhost:8443/x
$ curl -sk https://localhost:8443/x
{"backend": 9001, "path": "/x", "Host": "localhost", "X-Real-IP": null, "X-Forwarded-For": "127.0.0.1", "X-Forwarded-Proto": "https"}
なお、http2 on; は1.25.1で追加された書き方で、1.24では次のエラーになりました。1.25.0以前では listen 443 ssl http2; と書きます。
nginx: [emerg] unknown directive "http2"
OSのパッケージのNginxは、公式の最新版(2026年9月時点で stable 1.30系、mainline 1.31系)より前のバージョンのことがあります。nginx -v でバージョンを確認してから、ドキュメントの書き方を選びます。
502・504・413の切り分け
まずエラーログを見るのが最短です(/var/log/nginx/error.log)。ログの文言で原因がほぼ決まります。下の表のうち、Connection refused の502、upstream timed out の504、413の3つは、この記事の検証で実際に出力されたものです。
| ステータス | error.log の文言の例 | 主な原因 | 対処 |
|---|---|---|---|
| 502 Bad Gateway | connect() failed (111: Connection refused) while connecting to upstream |
アプリが起動していない、ポート違い | アプリの起動状態と待ち受けポートを確認 |
| 502 | connect() to unix:/run/app.sock failed (13: Permission denied) |
ソケットの権限、SELinux | ソケットの権限、setsebool -P httpd_can_network_connect 1 |
| 502 | upstream prematurely closed connection |
アプリが処理中に落ちた、応答が壊れている | アプリのログを確認 |
| 502 | upstream sent too big header |
Cookieなどでレスポンスヘッダーが大きい | proxy_buffer_size 16k; などで拡大 |
| 504 Gateway Timeout | upstream timed out (110: Connection timed out) while reading response header |
アプリの処理が proxy_read_timeout を超えた |
処理を速くする。必要な場所だけタイムアウトを延ばす |
| 413 | client intended to send too large body |
本文が client_max_body_size を超えた |
上限を上げる |
# ログを追いかけながら再現する
sudo tail -f /var/log/nginx/error.log
# Nginxを通さず、アプリに直接リクエストする(ここで失敗すればアプリ側の問題)
curl -v http://127.0.0.1:3000/
# アプリが待ち受けているか
ss -lntp | grep 3000
「Nginxを経由すると失敗し、直接なら成功する」ならNginxの設定、「直接でも失敗する」ならアプリ側、と分けて考えると早く原因にたどり着けます。
nginx -t で設定を確かめる
設定を変えたら、反映する前に必ず nginx -t を実行します。
$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
セミコロンを1つ消すと、ファイル名と行番号つきで教えてくれます。
nginx: [emerg] directive "listen" is not terminated by ";" in /etc/nginx/sites-enabled/app.conf:12
nginx: configuration file /etc/nginx/nginx.conf test failed
| コマンド | 用途 |
|---|---|
nginx -t |
文法と設定の確認 |
nginx -T |
確認に加え、読み込まれた全設定を連結して表示(includeの確認に便利) |
nginx -s reload |
無停止で設定を反映(文法エラーなら変更前の設定のまま動き続ける) |
nginx -v / nginx -V |
バージョン / ビルドオプションとモジュール |
nginx -t && systemctl reload nginx と続けて書くと、確認に通ったときだけ反映できます。
設定のチェックリスト
| 確認すること | 見るところ |
|---|---|
| proxy_passの末尾スラッシュは意図どおりか | locationとproxy_passのスラッシュがそろっているか |
4つの proxy_set_header を入れたか |
Host / X-Real-IP / X-Forwarded-For / X-Forwarded-Proto |
アプリは 127.0.0.1 で待ち受けているか |
外から直接アクセスできないこと |
WebSocketのパスに Upgrade / Connection があるか |
map と proxy_http_version 1.1 |
| 長い処理のパスだけタイムアウトを延ばしたか | 全体で延ばすと、詰まったときに接続がたまる |
| アップロードの上限はアプリと合っているか | client_max_body_size |
反映前に nginx -t を通したか |
エラーログも併せて確認 |
よくある質問
Q. proxy_passの末尾スラッシュは付けるべきですか?
A. アプリ側が受け取りたいパスで決めます。location /api/ へのリクエストを /api/... のまま渡すならスラッシュなし、/api/ を取り除いて /... で渡すならスラッシュあり(proxy_pass http://127.0.0.1:3000/;)です。どちらの場合も、location側のスラッシュとそろえます。
Q. アプリのログに記録される接続元IPがすべて127.0.0.1になります。
A. アプリから見た接続相手はNginxだからです。X-Real-IP と X-Forwarded-For を設定し、アプリ側で「Nginxからのヘッダーを信頼する」設定を有効にすると、本来の接続元IPを使えます。
Q. reloadとrestartはどちらを使えばいいですか? A. 設定の変更なら reload です。処理中の接続を切らずに新しい設定へ切り替わり、設定に誤りがあれば変更前の設定のまま動き続けます。restartは一度止めるため、その間はリクエストを受けられません。
Q. 504が出たら、タイムアウトを長くすれば解決しますか? A. 一時的には表示されなくなりますが、アプリの処理が遅い原因は残ります。まずアプリ側で時間のかかる処理を調べ、レポート出力のように本当に時間が必要なパスだけ、locationを分けてタイムアウトを延ばすのがおすすめです。
まとめ
- リバースプロキシは、Nginxが受けたリクエストをバックエンドに転送する仕組み。設定の中心は
proxy_pass - proxy_passにURI(
/を含む)があると、locationに一致した部分が置き換わる。なければパスはそのまま - Host・X-Real-IP・X-Forwarded-For・X-Forwarded-Proto の4行はほぼ必須。ないとアプリには宛先
127.0.0.1:9001とNginxのIPしか届かない - 複数台なら upstream。停止中のサーバーがあっても次のサーバーに再送される
- WebSocketは
Upgrade/Connectionヘッダーと HTTP/1.1 の設定が必要 - 502は接続できない、504は時間切れ、413はサイズ超過。まず error.log の文言を見る
- 反映前に
nginx -t、反映はreload
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Nginxリバースプロキシ設定|proxy_passの書き方と502/504対処」
株式会社RJCでは、アプリとインフラの境目にある設定も、実際に動かして確かめながら仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。