nginxリバースプロキシ設定を1行ずつ解説
Ubuntu 24.04のnginxでリバースプロキシを構築します。proxy_pass、アプリに必要な4つのヘッダー、WebSocket、末尾スラッシュ、アップロード設定を1行ずつ確認できます。
nginx のリバースプロキシ設定で行われること
nginx のリバースプロキシは、80 番ポートと 443 番ポートに到着したリクエストを、それぞれローカルポートで待ち受けているアプリケーションへ渡します。その後、アプリケーションの応答をブラウザーへ返します。設定は 1 つの server ブロックで構成され、ブロック自体も短いものです。難しい部分のほとんどは、実際のクライアントとクライアントが使用したプロトコルをアプリケーションへ伝える 5、6 行にあります。
以下では、Ubuntu 24.04 上でディストリビューションの nginx パッケージを使用し、何もない状態から設定を作成します。前提は、アプリケーションがすでに 127.0.0.1:3000 で応答していることです。まだプロキシを決めていない場合は、まず nginx、Caddy、Traefik の比較 を参照してください。ここからは、nginx の設定例を 1 行ずつ説明します。
これらの設定は、ご自身のサーバーで実行してください。リロードする前に、変更のたびに sudo nginx -t でテストし、出力内容を確認してください。
Where nginx keeps its config on Ubuntu
sudo apt update
sudo apt install -y nginx
ls -l /etc/nginx/sites-enabled/The main file is /etc/nginx/nginx.conf. It sets global options inside an http { } block and then pulls in two directories: /etc/nginx/conf.d/*.conf and /etc/nginx/sites-enabled/*. On Ubuntu and Debian you write one file per site in /etc/nginx/sites-available/ and switch it on with a symlink into /etc/nginx/sites-enabled/. Deleting the symlink disables the site and keeps the file.
Two directives used later only work in the http context, never inside a server block: map and upstream. Put them in their own file under /etc/nginx/conf.d/, because that directory is included at the http level.
The package ships an enabled site called default. It is marked default_server, which means it answers any request whose Host header matches no server_name anywhere in your config. While it stays enabled, a request that misses your names lands on it instead of on your app. Remove the symlink once your own site works.
sudo rm /etc/nginx/sites-enabled/default
sudo nginx -t
sudo systemctl reload nginxアプリケーション1つをプロキシする最小の server block
server {
listen 80;
listen [::]:80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:3000;
}
}これを /etc/nginx/sites-available/app.example.com として保存し、有効化して読み込みます。
sudo ln -s /etc/nginx/sites-available/app.example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
curl -sI -H 'Host: app.example.com' http://127.0.0.1/listen 80; は IPv4 にバインドし、listen [::]:80; は IPv6 にバインドします。2 行目を省略すると、DNS(domain name system)の検索結果でサーバーに対する AAAA レコードが返る訪問者は接続を拒否されます。一方、IPv4 を使う全員にはサイトが正常に表示されます。受け取るバグ報告は「自分の環境では動作する」です。
server_name はブラウザーが送信する Host ヘッダーと照合されます。複数の名前をスペースで区切って指定できます。一致する block がない場合、nginx は default_server である block を使用します。そのため、パッケージに含まれるサイトを削除する必要がありました。
location / はリクエストパスに対するプレフィックス一致で、/ はすべてのパスに一致します。proxy_pass は nginx が接続を開くアドレスです。アプリケーションは 127.0.0.1 にバインドしたままにし、外部からは nginx 経由だけでアクセスできるようにします。アプリケーションをコンテナで実行する場合は、3000:3000 ではなく 127.0.0.1:3000:3000 として公開してください。Docker は独自のルールを作成し、ufw を経由せずにポートを直接公開するため、ファイアウォールの設定に関係なく、公開したポートへインターネットから直接アクセスできてしまいます。
curl 行はサーバー自体から正しい Host ヘッダーを送信します。そのため、DNS がどこも指していない段階でも block をテストできます。
他に何も記述しない場合に nginx が upstream へ送信する内容
proxy_pass だけでは、アプリケーションに関する4つの情報が隠れます。
nginx はデフォルトでバックエンドと HTTP/1.0 で通信し、Connection: close を送信します。そのため、リクエストごとに新しい upstream 接続が作成され、プロトコルのアップグレードもできません。
Host ヘッダーは proxy_pass の値に書き換えられます。この値は 127.0.0.1:3000 です。Host から絶対リンクを生成するアプリケーションは、サーバー外部から開けないリンクを生成するようになります。
アプリケーションへ到達する接続元は nginx です。そのため、アプリケーションから見えるクライアントアドレスは 127.0.0.1 になります。アプリケーション内のすべてのログ行とレート制限は、訪問者ではなくプロキシを記録することになります。
アプリケーションは、ブラウザーが HTTPS を使用したことを判別できません。受信した接続は、loopback アドレス上のプレーンな HTTP だからです。
4行でこれらをすべて修正できます。
設定する4つのヘッダーと、それぞれがバックエンドに見せる情報
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;
}Host には、訪問者が入力した名前が入ります。$host はリクエストに含まれる名前からポートを除き、英字を小文字にした値です。これを設定すると、アプリケーションは正しい絶対 URL を生成できます。ログイン後のリダイレクトや、パスワードリセットメール内のリンクなどです。設定しない場合、これらの URL は 127.0.0.1:3000 を指すため、ログイン時にブラウザーが接続を拒否するアドレスへ移動します。8080 で提供する場合など、アプリケーションでポートも必要になるときは、クライアントが送信したヘッダーをそのまま保持する $http_host を使用します。
X-Real-IP には、1 つの値である $remote_addr が入ります。これは nginx が接続を受け付けた接続元アドレスです。アプリケーションは、自身のアクセスログやレート制限でこの値を参照します。
X-Forwarded-For には、一覧が入ります。$proxy_add_x_forwarded_for は、クライアントがそのヘッダーに設定した値へ $remote_addr を追加するため、値はコンマ区切りになり、nginx が追加した項目が最後になります。この点によって、ヘッダーを信頼できるかどうかが決まります。クライアントは任意の X-Forwarded-For を送信できるため、先頭の項目を読むアプリケーションには、どのアドレスでも通知できてしまいます。nginx がエッジサーバーの場合は、代わりに $remote_addr を記述して、クライアント側の値を破棄します。CDN や別のプロキシが前段にある場合は、realip module の set_real_ip_from と real_ip_header を使用します。これにより、$remote_addr 自体が実際のクライアントアドレスになります。
X-Forwarded-Proto には、http または https が入ります。フレームワークはこれを参照して、Cookie に Secure を付けるか、HTTPS へのリダイレクトを強制するかを判断します。TLS サイトでこれを省略すると、HTTPS を強制する設定のアプリケーションは http を認識します。HTTPS URL へリダイレクトを返し、次のリクエストを nginx 経由で受けても http のままとなるため、再びリダイレクトします。ブラウザーは処理を中止し、ERR_TOO_MANY_REDIRECTS を表示します。
これら4行をすべての location に繰り返し記述すると、設定が分岐していきます。1つのファイルにまとめて include してください。
# /etc/nginx/snippets/proxy-headers.conf
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;location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
}ここでの継承には注意点があります。location が独自のディレクティブを1つも定義していない場合に限り、server block の proxy_set_header ディレクティブを継承します。location 内に proxy_set_header を1つ追加すると、その location では server レベルで定義したすべてのヘッダーが破棄されます。そのため、すべてを同じレベルに置くか、プロキシする各 location でスニペットを include してください。
WebSocket アプリケーションが接続後に切断されるのはなぜですか?
デフォルト設定では upgrade が許可されず、デフォルトの読み取りタイムアウトによって、アイドル状態のトンネルが 60 秒後に閉じられるためです。WebSocket は、Upgrade: websocket と Connection: Upgrade を含む HTTP リクエストとして開始されます。これらは hop-by-hop ヘッダーです。つまり、プロキシはそのまま転送せずに処理する必要があります。また、HTTP/1.0 には upgrade の仕組みがありません。両方とも手動で追加する必要があります。
map は http コンテキストに、専用ファイルとして配置します。
# /etc/nginx/conf.d/websocket.conf
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}次に location を設定します。
location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
}この map は、1 つの location で 2 種類のトラフィックを処理するために使用します。通常のリクエストでは $http_upgrade が空なので、$connection_upgrade は close になります。upgrade リクエストでは $http_upgrade に websocket が入るため、upstream に送信されるヘッダーは Connection: upgrade になります。proxy_set_header Connection "upgrade"; を固定すると、通常のページリクエストにもそのヘッダーが毎回送信され、一部のバックエンドはそのようなリクエストに 400 を返します。
proxy_read_timeout が、「読み込まれるが、その後更新されない」という報告の原因です。デフォルト値は 60 秒で、接続の存続時間ではなく、バックエンドから 2 回読み取る間隔を測定します。60 秒間通信がない WebSocket は nginx によって閉じられ、ブラウザーのコンソールにはソケットがコード 1006 で閉じたと表示されます。1 分未満の間隔で独自の heartbeat を送信するアプリケーションでは、この問題は発生しません。送信しないアプリケーションでは、ちょうど 1 分後に切断されます。最初に問題が現れるのは、ライブエディターやダッシュボードです。HTTPS の背後で稼働する自己ホスト型 n8n インスタンスがよくある例です。
proxy_pass の末尾のスラッシュで URL が変わるのはなぜですか?
ルールは1文で説明できます。proxy_pass が URI(uniform resource identifier)で終わっている場合は、単独の / だけの場合でも、nginx はリクエストパスのうち location プレフィックスに一致した部分を削除し、その場所にその URI を配置します。proxy_pass がホストとポートで終わっている場合、リクエストパスは変更されずにそのまま渡されます。
location /app/ {
proxy_pass http://127.0.0.1:3000/;
}/app/status へのリクエストは、バックエンドでは /status として到達します。
location /app/ {
proxy_pass http://127.0.0.1:3000;
}/app/status へのリクエストは、バックエンドでは /app/status として到達します。
どちらを使うかはアプリによって異なります。base-path またはサブフォルダーの設定があるアプリでは、/app を設定に指定して、2つ目の形式を使います。プレフィックスを認識しないアプリでは、1つ目の形式が必要です。1つ目の形式には、すぐに分かる問題があります。アプリが返す HTML には /static/main.css のような絶対パスが残るため、ブラウザーはサイトのルートに対してそれらを要求します。一致する location がないため、ページはスタイルなしで表示されます。ブラウザーのネットワークタブでは、これらのアセットへのリクエストが 404 で返ることを確認できます。解決策は、アプリ独自の base-path 設定を使うか、同じバックエンドを指す2つ目の location /static/ を追加することです。
正規表現の location では、proxy_pass に URI を指定できません。sudo nginx -t は設定を拒否し、その理由として "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 を示します。
各アプリに固有の名前(app.example.com)を割り当て、location / からプロキシすれば、この種の問題はすべて解消します。DNS レコードを追加できない場合に限り、サブパスを使う価値があります。
1つの名前の背後に複数のバックエンドを配置するにはどうすればよいですか?
upstream ブロックを使用します。これは http コンテキストに属するため、同じファイル内の server ブロックより前に記述するか、/etc/nginx/conf.d/ に記述します。
upstream app_backend {
least_conn;
server 127.0.0.1:3000 max_fails=3 fail_timeout=30s;
server 127.0.0.1:3001 max_fails=3 fail_timeout=30s;
keepalive 32;
}次に、proxy_pass http://app_backend; でその名前を指定します。
デフォルトの方式はラウンドロビンです。least_conn は、アクティブな接続数が最も少ないバックエンドへ各リクエストを送信します。処理時間が均一でないリクエストに適しています。ip_hash は、1 つのクライアントアドレスを 1 つのバックエンドに固定します。アプリがセッションを自身のメモリに保持する場合は、ip_hash が必要です。このようなバックエンドが 2 台ある状態でラウンドロビンを使用すると、リクエストがセッションを保持していないインスタンスに到達するたびに、ユーザーがランダムにログアウトされます。セッションを共有ストレージへ移すほうが適切です。
max_fails=3 fail_timeout=30s は、30 秒以内に 3 回失敗したサーバーを 30 秒間、負荷分散の対象から外すことを意味します。ブロック内のすべてのサーバーがこの状態になると、クライアントには 502 が返され、エラーログには no live upstreams while connecting to upstream と記録されます。
keepalive 32 は、ワーカープロセスごとにバックエンドへのアイドル接続を最大 32 本保持します。これにより、ほとんどのリクエストで TCP ハンドシェイクを省略できます。これは proxy_http_version 1.1 を使用し、上流へ送信する Connection: close がない場合にのみ機能します。同じ location で WebSocket 用の map も使用する場合は、空の場合を close から空文字列に変更してください。通常のリクエストに Connection ヘッダーが付かなくなり、プール済みの接続が再利用されます。
map $http_upgrade $connection_upgrade {
default upgrade;
'' '';
}upstream ブロック内の名前は、nginx の起動時に名前解決されます。バックエンドが再起動時に新しいアドレスを受け取るコンテナの場合、nginx を reload するまで古いアドレスを使い続けます。Docker ネットワーク内では、組み込み resolver を使用して名前解決をリクエスト時に実行できます。
resolver 127.0.0.11 valid=10s;
set $backend http://app:3000;
proxy_pass $backend;コンテナの追加と削除が頻繁になり、nginx の設定を追従させるための編集が負担になる場合は、コンテナのラベルを読み取るプロキシのほうが適しています。複数の Docker Compose アプリの前段に Traefik を配置する方法では、コンテナ自体からルートを構築します。
アップロードが 413 Request Entity Too Large で失敗するのはなぜですか?
client_max_body_size のデフォルト値は 1 megabyte です。これより大きいリクエストボディは、アプリケーションに届く前に nginx によって拒否され、エラーログには client intended to send too large body が記録されます。server block、またはアップロードを処理する location で値を引き上げてください。
client_max_body_size 512m;0 に設定すると、このチェックは完全に無効になります。アプリケーション側にも独自の上限があります。そのため、この変更後も 413 が発生する場合はバックエンドから返されています。次にアプリケーション側のアップロード設定を確認してください。
nginx はデフォルトで、upstream への接続を開く前にリクエストボディ全体を読み込みます。大きなデータは、まずディスク上の一時ファイルに書き込みます。これにより、バックエンドにはアップロードデータがローカルの速度で届くため、低速なクライアントからアプリケーションを保護できます。非常に大きなアップロードでは、ストリーミングに切り替えることもできます。
proxy_request_buffering off;この場合、バックエンドは到着したデータをそのまま受信するため、それを処理できなければなりません。また、ボディはすでに読み終わっているため、nginx は別の upstream に対してリクエストを再試行できなくなります。
デフォルトで 60 seconds の client_body_timeout は、アップロード全体ではなく、ボディの連続する 2 回の読み取りの間に適用されます。低速でも安定して進むアップロードは完了できます。転送が停止すると、接続は切断されます。
レスポンスバッファリングと、ライブ出力を壊す設定
proxy_bufferingはデフォルトで有効で、通常はこの設定が適しています。nginx はアプリケーションが書き込める速度でレスポンスを読み取り、保持してから、遅いクライアントにはそのクライアントの速度に合わせて送信します。アプリケーションのワーカーは、遅いダウンロードが完了するまで処理中のままにならず、早く処理を終えられます。
この設定はストリーミングレスポンスを壊します。Server-sent events やライブのログ出力では、バッファがいっぱいになるまで読み手に何も表示されません。バッファリングは、その location だけで無効にします。
proxy_buffering off;アプリケーションを制御できる場合は、ストリーミングレスポンスだけに X-Accel-Buffering: no ヘッダーを付ける方法が適しています。nginx はレスポンスごとにこのヘッダーを読み取り、そのレスポンスだけバッファリングを無効にします。通常のページでは、バッファリングの利点を維持できます。
エラーログに upstream sent too big header while reading response header from upstream と表示される場合、レスポンスヘッダーが 1 つのバッファに収まりませんでした。proxy_buffer_size のデフォルト値は 1 つのメモリページで、プラットフォームに応じて 4 または 8 kilobytes です。長い cookie や大きな認証ヘッダーがこのサイズを超えることがあります。両方の値を増やしてください。
proxy_buffer_size 16k;
proxy_buffers 8 16k;この構成で TLS を配置する場所はどこですか?
上記のすべての手前にある nginx で終端します。TLS(トランスポート層セキュリティ)はプロキシで終端し、nginx からアプリへの接続は、ネットワーク上の他のホストから読み取られない loopback address 上のプレーンな HTTP のままにします。アプリは、訪問者が HTTPS を使用したことを、4 つのヘッダーのうち 4 番目である X-Forwarded-Proto から認識します。
証明書のパスを手動で記述しないでください。DNS レコードをサーバーに向け、ファイアウォールを開放し、Certbot にこの同じ server block を編集させます。Certbot は ssl_certificate のパスを指定する listen 443 ssl 行と、port 80 からのリダイレクトを追加します。Certbot を使用して nginx 用の Let's Encrypt 証明書を発行するでは、発行と更新タイマーについて説明しています。
sudo ufw allow 'Nginx Full'
sudo ufw statusNginx Full は nginx パッケージがインストールする application profile で、port 80 と port 443 を同時に開放します。すべての訪問者を HTTPS にリダイレクトした後も、HTTP-01 renewal challenge のために port 80 は開放したままにする必要があります。
設定をテストしてから reload します
sudo nginx -t
sudo systemctl reload nginxnginx -t はすべての include ファイルを解析し、テストが成功したことを報告するか、処理が停止したファイルと行を表示します。reload する前に、この出力を確認してください。設定に問題がある状態で reload しても適用されません。nginx は以前の設定でサービスを継続するため、サイトは稼働したまま変更だけが反映されません。systemctl restart は動作が異なり、さらに危険です。restart では最初に実行中のサーバーを停止するため、設定エラーがあると nginx 自体が起動していない状態になります。基本は reload を使用し、restart はそれが必要なまれな変更に限定してください。
sudo tail -f /var/log/nginx/error.log
sudo ss -lntp | grep -E ':(80|443|3000)'ss の行には、各ポートを保持しているプロセスが表示されます。これにより、アプリが実際に proxy_pass の指す場所で待ち受けていることを確認できます。
実際に遭遇する障害
エラーログに connect() failed (111: Connection refused) while connecting to upstream が記録された 502 Bad Gateway。 proxy_pass に指定されたアドレスで待ち受けているプロセスがありません。アプリが停止しているか、別のポートにバインドされているか、ホストから到達できないコンテナ内部のアドレスにバインドされています。
no live upstreams while connecting to upstream を伴う 502。 upstream ブロック内のすべてのサーバーが、現在 max_fails により失敗状態と判定されています。バックエンドを修復してください。fail_timeout の期限が切れると、nginx はバックエンドへの接続を再試行します。
upstream timed out (110: Connection timed out) while reading response header from upstream を伴う 504 Gateway Time-out。 バックエンドは接続を受け付けた後、proxy_read_timeout 秒間何も送信していません。実際に時間のかかるレポートであればタイムアウトを延長して問題ありませんが、停止しているアプリに対して延長するのは適切ではありません。
すべてのパスでアプリから 404 が返る。 末尾のスラッシュに関するルールによってパスが書き換えられています。アプリのログに記録されたパスと、要求したパスを比較してください。
別のサイトが応答する。 server_name が Host ヘッダーに一致しないため、要求が default_server ブロックにフォールスルーしています。
ページの読み込み後、約 1 分でインターフェースが停止する。 WebSocket に関する問題です。Upgrade の処理がないか、proxy_read_timeout がまだ 60 秒のままです。
FAQ
nginx で proxy_pass を追加した後に 502 Bad Gateway が返るのはなぜですか?
nginx が proxy_pass にあるアドレスへの接続を開けませんでした。/var/log/nginx/error.log のエラーログに原因が記録されています。connect() failed (111: Connection refused) while connecting to upstream は、そのアドレスで待ち受けているプロセスがないことを示します。no live upstreams は、upstream ブロック内のすべてのサーバーが失敗とマークされたことを示します。sudo ss -lntp | grep 3000 を実行すると、そのポートを保持しているプロセスと、バインドされているアドレスを確認できます。コンテナ内部のアドレスにバインドされたアプリや、指定したものとは異なるポートで待ち受けるアプリでは、毎回このエラーになります。
nginx の背後でアプリが約 1 分後に切断されるのはなぜですか?
その接続は WebSocket であり、proxy_read_timeout はデフォルトの 60 秒のままです。この値は、バックエンドからの 2 回の読み取りの間隔を測定します。通信のないソケットは nginx によって閉じられ、ブラウザーのコンソールには close code 1006 が表示されます。proxy_http_version 1.1 を設定し、Upgrade と Connection を map とともに $http_upgrade へ渡し、proxy_read_timeout を 3600s などの値まで引き上げます。Upgrade ヘッダーがないとアップグレード自体が行われないため、アプリはポーリングにフォールバックするか、ライブ更新を表示しません。
proxy_pass の末尾のスラッシュは重要ですか?
重要です。バックエンドが受け取るパスが変わります。location /app/ と proxy_pass http://127.0.0.1:3000/ の場合、/app/status へのリクエストは /status としてバックエンドに到達します。これは、ホストとポートの後に URI があると、一致した location のプレフィックスが置き換えられるためです。最後のスラッシュを削除すると、同じリクエストは /app/status として到達します。プレフィックスを削除すると、アプリ独自のアセットリンクが壊れることがあります。リンクが絶対パスのままだとサイトのルートで 404 になるため、base-path 設定を持つアプリでは、パスをそのまま渡す形式のほうが適しています。
アプリケーションのログで、すべての訪問者の IP アドレスが 127.0.0.1 になるのはなぜですか?
アプリが受け取る接続は、実際に loopback アドレス上の nginx から来ているためです。訪問者のアドレスは、設定したヘッダーによってのみアプリに伝わります。単一の値には proxy_set_header X-Real-IP $remote_addr;、追加されたチェーンには proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; を使用します。アプリ側でも、これらのヘッダーを信頼するよう設定する必要があります。ただし、クライアントは独自の X-Forwarded-For を送信できます。そのため nginx がエッジサーバーの場合は、追記せず $remote_addr で上書きします。
nginx とアプリの間の接続にも TLS は必要ですか?
アプリが同じサーバー上で動作し、127.0.0.1 にバインドされている場合は必要ありません。この通信はマシンの外部に出ないためです。nginx で TLS を終端し、loopback 上では proxy_pass を平文の HTTP のまま使用して、アプリに訪問者が HTTPS を使用したことを知らせるため X-Forwarded-Proto $scheme を送信します。バックエンドが別のホスト上にあり、管理していないネットワークを経由する場合、その経路には独自の保護が必要です。バックエンドまで HTTPS を使用するか、2 台のマシン間にプライベートトンネルを構成します。