
nginxのリバースプロキシとは、ブラウザからのリクエストをnginxがいったん受け取り、裏側で動くアプリケーション(Grafana、社内Webアプリ、APIサーバーなど)へ転送して、その応答をクライアントに返す構成です。利用者から見えるのはnginxの80番・443番だけで、アプリ本体は 127.0.0.1 で待ち受けたまま外に出ません。
ここではUbuntu 24.04に入るnginx 1.24.0を使い、1台のサーバーに届くリクエストをホスト名とパスで4つのバックエンドへ振り分けます。HostとX-Forwarded-*ヘッダー、WebSocket、HTTPS化、アップロード上限とタイムアウトまで設定し、最後に「バックエンドに実際に何が届いたか」をテスト用のエコーサーバーで確かめます。
この記事でわかること
proxy_passの末尾の/でバックエンドに届くパスがどう変わるか- nginxが既定で送るHostヘッダーと、
proxy_set_headerが上位から継承されなくなる条件 - WebSocketを通す
mapの書き方と、60秒で切れる理由 - certbotでのHTTPS化と、nginx 1.24と1.25以降で異なる
http2の書き方 - 502・413・504をerror.logの文面から切り分ける手順
nginxのリバースプロキシとは:フォワードプロキシとの違いと前段に置く理由
名前が似ているフォワードプロキシは、社内PCがインターネットへ出るときに通る「利用者側の出口」です。ブラウザやOSにプロキシの設定(またはPACファイル)を入れるので、クライアントはプロキシの存在を知っています。これに対してリバースプロキシは、サーバー側の入口に立ちます。DNSが指している先がnginxになっているだけなので、利用者は普通にURLを開くだけで、後ろにアプリが何台あるかは見えません。
| 観点 | フォワードプロキシ | リバースプロキシ |
|---|---|---|
| 置き場所 | 利用者側ネットワークの出口 | サーバー側の入口 |
| 設定を意識する人 | クライアント(ブラウザ・OS・PAC) | サーバー管理者だけ |
| 主な目的 | 閲覧制御、アクセスログ、キャッシュ | TLS終端、振り分け、アプリの隠蔽 |
| 代表的なソフト | Squid、UTMのWebプロキシ | nginx、Caddy、HAProxy、Traefik |
自宅や小さなオフィスでリバースプロキシが効く理由は具体的です。グローバルIPと443番は1つしかないので、Grafana(3000番)とUptime Kuma(3001番)と自作アプリ(8080番)を外から使うには、ホスト名で分けて1つの443番に集約するしかありません。証明書もnginxの1か所で管理でき、アプリは 127.0.0.1 だけで待ち受けさせられるので、アプリ自体の脆弱なデバッグ画面や管理APIがネットワークに直接出ることもなくなります。アップロード上限やIP制限も、アプリごとに調べる代わりにnginxの設定で統一できます。
振り分け方は、サブパス(example.com/grafana/)よりサブドメイン(grafana.example.com)をおすすめします。多くのアプリはルートパスで動く前提で絶対パスのリンクを生成するため、サブパスに置くとCSSや画面遷移が壊れます。Grafanaなら root_url と serve_from_sub_path を設定すれば動きますが、アプリごとにこの種の設定を探す手間がかかります。サブパスが素直に使えるのは、同じアプリのAPIを /api/ に分けるような、パスの設計を自分で決められる場合です。
構成と前提:Ubuntu 24.04のnginxで4つのバックエンドを振り分ける
この記事で作る振り分けは次の表のとおりです。4つのホスト名のAレコード(LAN内だけで使うなら社内DNSの登録)は、すべてnginxを動かすサーバーを指しておきます。
| 受け付けるURL | 転送先 | 中身 | 設定上の注意 |
|---|---|---|---|
app.example.com/ | 127.0.0.1:8080 | 社内Webアプリ本体 | アップロード上限を50MBに広げる |
app.example.com/api/ | 127.0.0.1:8081 | 同じアプリのAPIサーバー | 先頭の /api を取り除いて渡す |
kuma.example.com | 127.0.0.1:3001 | 監視ツール(例:Uptime Kuma) | WebSocketを通し、タイムアウトを延ばす |
grafana.example.com | 127.0.0.1:3000 | Grafana | 標準のヘッダー設定だけで動く |
sudo apt update
sudo apt install nginx
nginx -v
# nginx version: nginx/1.24.0 (Ubuntu) ← 執筆時点のUbuntu 24.04の版Ubuntu/Debianのnginxは、/etc/nginx/nginx.conf の http ブロックから conf.d/*.conf と sites-enabled/* を読み込みます。サイトごとの設定は sites-available/ に置き、sites-enabled/ へシンボリックリンクを張ったものだけが有効になる仕組みです。共通部品は snippets/ に置きます。パッケージには /etc/nginx/proxy_params という4行のファイルも同梱されていますが、Hostに $http_host を使い、WebSocket用の行がないため、この記事では自前のスニペットを作ります。$http_host はクライアントが送ったHostヘッダーをポート番号込みでそのまま使い、$host はポートを含まず、Hostヘッダーが空でもserver_nameで補われる、という違いがあります。
インストール直後に有効になっている default サイトは外しておきます。残したままだと、後で書く default_server と衝突します。
sudo unlink /etc/nginx/sites-enabled/default最小の設定:proxy_passとlocationの書き方
まずhttpコンテキストに置く部品を2つ作ります。1つ目はWebSocket用の変数を作る map、2つ目は「どのバックエンドが何秒で応答したか」を残すアクセスログの書式です。どちらも conf.d/ に置けば http ブロックの中で読み込まれます。
map $http_upgrade $connection_upgrade {
default upgrade;
"" close;
}log_format upstreamlog '$remote_addr "$host" "$request" $status '
'upstream=$upstream_addr ustatus=$upstream_status '
'rt=$request_time urt=$upstream_response_time';
access_log /var/log/nginx/proxy_access.log upstreamlog;次に、各locationで読み込むヘッダー設定をスニペットにまとめます。なぜスニペットにするのかは、次の章で説明する継承の仕様が理由です。
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;サイト本体の設定です。先頭の default_server は、IPアドレス直打ちや登録していないホスト名で来たリクエストに何も返さず接続を切ります(444はnginx独自の「応答せずに閉じる」コード)。これがないと、どのserver_nameにも一致しないリクエストは最初に書いたserverブロック、つまり app.example.com のアプリに流れ、IPアドレスを総当たりするスキャナーにアプリを見せることになります。
# 未知のホスト名・IP直打ちは応答せずに切断する
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 80;
listen [::]:80;
server_name app.example.com;
client_max_body_size 50m;
location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:8080;
}
# /api/users はバックエンドに /users として届く
location /api/ {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:8081/;
}
}
server {
listen 80;
listen [::]:80;
server_name kuma.example.com;
location / {
include snippets/proxy-headers.conf;
proxy_read_timeout 1h;
proxy_pass http://127.0.0.1:3001;
}
}
server {
listen 80;
listen [::]:80;
server_name grafana.example.com;
location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:3000;
}
}sudo ln -s /etc/nginx/sites-available/home-proxy.conf /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx設定でいちばん間違えやすいのが、proxy_pass の末尾の / です。proxy_pass にホストの後ろのURI部分(/ 1文字でも)を書くと、locationに一致した部分がそのURIに置き換わります。書かなければ、リクエストのパスがそのまま渡ります。エコーサーバーで確かめた結果が次の表です。
| locationとproxy_pass | リクエスト | バックエンドに届くパス |
|---|---|---|
location /api/ と proxy_pass http://127.0.0.1:8081/; | /api/users | /users |
location /api/ と proxy_pass http://127.0.0.1:8081; | /api/users | /api/users |
location /api/(proxy_passの書き方によらない) | /api | nginxが /api/ へ301でリダイレクト |
APIサーバーが /users のようにプレフィックスなしでルートを定義しているなら前者、/api/users で定義しているなら後者です。どちらにするかは、バックエンドのログに出るパスを見て決めるのが確実です。
ヘッダーの設定:Host・X-Forwarded-For・X-Forwarded-Proto
proxy_set_header を1行も書かないと、nginxはバックエンドに Host: 127.0.0.1:8080 と Connection: close を送ります。検証用のエコーサーバーが受け取った内容は次のとおりで、元のホスト名 app.example.com はどこにも残っていません。アプリがこのHostをもとにリダイレクト先やOAuthのコールバックURLを組み立てると、ログイン直後に http://127.0.0.1:8080/ へ飛ばされるという症状になります。
GET /
Host: 127.0.0.1:8080
Connection: close
User-Agent: curl/8.5.0
Accept: */*スニペットの各行の役割は次のとおりです。Host $host は、アプリが自分の公開URLを組み立てるために使います。X-Real-IP と X-Forwarded-For は接続元IPで、アプリ側のアクセスログやログイン試行の制限に使われます。X-Forwarded-Proto は元の通信がhttpかhttpsかを伝え、これがないとHTTPS化したあとに「アプリはhttpで来たと思ってhttpsへリダイレクトし、nginxはまたhttpで渡す」というリダイレクトループが起きます。
継承の仕様には注意が必要です。proxy_set_header は、そのレベル(locationなど)に1行も書かれていない場合に限って上位(server、http)の設定を引き継ぎます。serverブロックにHostとX-Forwarded-Protoを書き、locationに X-Real-IP だけを足した設定で試すと、バックエンドに届いたのは次の内容でした。server側の2行は黙って無視されています。
GET /
X-Real-IP: 127.0.0.1
Host: 127.0.0.1:8080
Connection: close共通のヘッダーをスニペットにまとめ、各locationで include しているのはこのためです。セキュリティヘッダーを付ける add_header も同じ継承ルールなので、locationに1つ足した途端にserverで付けたヘッダーが消える、という事故も同じ仕組みで起きます。
$proxy_add_x_forwarded_for は、クライアントが送ってきたX-Forwarded-Forの後ろに接続元IPを追記します。試しに X-Forwarded-For: 203.0.113.9 を付けてリクエストすると、バックエンドには 203.0.113.9, 127.0.0.1 が届きました。nginxがインターネットに直接面している場合は proxy_set_header X-Forwarded-For $remote_addr; で上書きし、先頭の値を信用するアプリに偽のIPを渡さないようにしてください。
nginxの前にさらにCloudflareなどのプロキシがいる場合は逆で、set_real_ip_from で前段のIP範囲を指定し、real_ip_header で本当のクライアントIPが入るヘッダーを指定します。Cloudflare Tunnelで公開する場合の設定でこの組み合わせを使っています。
WebSocketを通す:Upgradeヘッダーとタイムアウト
WebSocketは、HTTP/1.1のGETリクエストに Upgrade: websocket と Connection: Upgrade を付けて始まり、サーバーが101 Switching Protocolsを返すと双方向の通信路に切り替わります。この2つのヘッダーはホップバイホップ(中継ごとに消費される)ヘッダーなので、nginxは明示しない限りバックエンドへ渡しません。スニペットの proxy_http_version 1.1 と Upgrade・Connection の2行、そして conf.d/ に置いた map がそのための設定です。
map は、クライアントがUpgradeヘッダーを送ってきたときだけ $connection_upgrade を upgrade にし、通常のリクエストでは close にします。Upgradeヘッダーを転送しない設定のserverに同じハンドシェイクを送ると、nginxは普通のGETとして扱って200を返しました。アプリ側はWebSocketが張れないと判断し、ロングポーリングに落ちるか、画面に「切断されました」と出し続けます。
もう1つの落とし穴が proxy_read_timeout です。既定の60秒間バックエンドから何も届かないと、nginxはその接続を閉じます。数十秒ごとにpingを送るアプリなら問題になりませんが、送らないアプリは放置すると1分ごとに再接続を繰り返します。kuma.example.com のlocationだけ proxy_read_timeout 1h; にしているのはこのためで、全体の既定値を延ばすと、応答しなくなった通常のバックエンドにもクライアントを1時間待たせることになります。
LLMのチャット画面のように、応答を少しずつ返すServer-Sent Eventsやストリーミング応答では、nginxのバッファリングが逆に邪魔になります。該当するlocationに proxy_buffering off; を入れるか、バックエンドが応答ヘッダーに X-Accel-Buffering: no を付ければ、トークンが1つずつ画面に流れるようになります。
HTTPSにする:certbotと443番の設定
公開ドメインがあり、インターネットから80番が届くなら、Let's Encryptの証明書をcertbotで取るのが手堅い方法です。certbotはHTTP-01チャレンジ(認証局がこのサーバーの80番に確認用のファイルを取りに来る方式)で所有を確認し、nginxの設定を書き換えて443番を有効にします。
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d app.example.com -d grafana.example.com -d kuma.example.com
systemctl list-timers certbot.timer
sudo certbot renew --dry-run執筆時点のUbuntu 24.04のcertbotは2.9.0で、更新はsystemdの certbot.timer が1日2回実行します。--dry-run が通れば、証明書の期限を手作業で追いかける必要はありません。certbotが書き換えた行には # managed by Certbot のコメントが付くので、どこが自動生成かは後から見分けられます。書き換え後の443番のserverブロックは、おおむね次の形になります。
server {
listen 443 ssl http2; # nginx 1.24の書き方
listen [::]:443 ssl http2;
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;
include /etc/letsencrypt/options-ssl-nginx.conf;
client_max_body_size 50m;
location / {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:8080;
}
location /api/ {
include snippets/proxy-headers.conf;
proxy_pass http://127.0.0.1:8081/;
}
}
# 知らないSNIにはTLSハンドシェイクの段階で断る
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
ssl_reject_handshake on;
}443番の default_server に ssl_reject_handshake on; を置くと、IP直打ちや未登録のホスト名で来たTLS接続を、証明書を見せる前に拒否します。これがないと、最初のserverブロックの証明書が返り、そこに並んだホスト名(SAN)から内部のサービス名が読み取れてしまいます。
HTTP/2の書き方はnginxの版で変わりました。1.24では listen 443 ssl http2; と書き、1.25.1以降は listen 443 ssl; と http2 on; の2行に分けます。1.24で http2 on; と書くと unknown directive "http2" で起動せず、公式Dockerイメージの nginx:stable(執筆時点で1.30.5)で古い書き方をすると the "listen ... http2" directive is deprecated という警告が出ます。ネット上の設定例をコピーするときは、まず nginx -v で自分の版を見てください。
HSTS(Strict-Transport-Security ヘッダー)は、すべてのサブドメインをHTTPSにできると確認してから入れてください。ブラウザは指定した期間(例:max-age=31536000 なら1年)httpでの接続を拒否し続けるため、HTTPのまま残したい機器の管理画面が同じドメイン配下にあると、そこへ入れなくなります。
外から80番が届かないLAN専用のサーバーでは、HTTP-01は使えません。DNSのTXTレコードで所有を証明するDNS-01チャレンジを使うか、DNS-01の設定が数行で済むCaddyで自動HTTPSのリバースプロキシを作る方法を検討してください。
アップロード上限・タイムアウト・バッファリング
リバースプロキシを挟んだ直後に出る不具合の多くは、nginxの既定値が原因です。変更する候補を表にまとめます。
| ディレクティブ | 既定値 | 超えたときの症状 | 変える場面 |
|---|---|---|---|
client_max_body_size | 1m | 413 Request Entity Too Large | 写真・動画・バックアップファイルのアップロード |
proxy_read_timeout | 60s | 504 Gateway Time-out、WebSocketの切断 | 重いレポート生成、WebSocket |
proxy_buffering | on | ストリーミング応答がまとめて届く | SSE、LLMのトークン逐次表示 |
proxy_request_buffering | on | アップロードがnginxに一度ためられる | 数GBのファイルをそのまま流したい場合 |
2MBのファイルを上限1MBのままPOSTすると、クライアントには413が返り、error.logには client intended to send too large body: 2000000 bytes と記録されます。上限は必要なserverやlocationにだけ設定し、0(無制限)をhttpコンテキストに書くのは避けてください。アップロードを受け付けないはずのアプリにまで巨大なリクエストが届くようになります。
Dockerのコンテナへ振り分けるとき
nginxをホストに入れ、アプリをコンテナで動かす構成では、アプリのポートを "127.0.0.1:3000:3000" のようにループバックだけに公開します。Dockerが公開したポートは、Dockerが自分で追加するiptablesのルールで処理されるため、ufwで閉じたつもりでも外から届きます。nginxからは proxy_pass http://127.0.0.1:3000; で届くので、外に出す必要はありません。
nginx自体もDocker Composeで動かす場合は、proxy_pass http://grafana:3000; のようにサービス名で書けます。ただしnginxは起動時にこの名前を解決するので、grafanaのコンテナが止まっていると host not found in upstream "grafana" でnginxごと起動できません。1つのアプリの停止がすべてのサイトを巻き込むのを避けるには、Docker内蔵DNSを resolver に指定し、転送先を変数に入れます。
server {
listen 80;
server_name grafana.example.com;
resolver 127.0.0.11 valid=10s ipv6=off;
set $grafana_upstream http://grafana:3000;
location / {
include snippets/proxy-headers.conf;
proxy_pass $grafana_upstream;
}
}この形にすると、grafanaがいない間もnginxは起動し、該当ホストへのアクセスだけが502になります(error.logには grafana could not be resolved と出ます)。コンテナが起動すれば10秒以内に名前が引け、そのまま転送が再開しました。変数に入れるのはスキームとホストとポートまでにし、パスを含めないでください。パスを含めるとリクエストのURIが丸ごと置き換わります。Composeでのネットワークの分け方はDocker Composeで自宅サーバーのスタックを動かすガイドにまとめています。
つまずきポイント
502 Bad Gatewayになる
error.logに connect() failed (111: Connection refused) while connecting to upstream とあれば、転送先のポートで誰も待ち受けていません。sudo ss -ltnp | grep 8081 でアプリの待ち受けアドレスを確認します。よくあるのは、アプリが localhost 指定でIPv6の ::1 だけに待ち受け、nginxは 127.0.0.1 に接続しているケースです。RHEL系(Rocky Linux、AlmaLinux)で (13: Permission denied) while connecting to upstream が出る場合はSELinuxが止めているので、sudo setsebool -P httpd_can_network_connect 1 で許可します。
ログイン後に127.0.0.1へ飛ばされる、リダイレクトが止まらない
Hostまたは X-Forwarded-Proto がバックエンドに届いていません。locationに proxy_set_header を1行だけ足して、スニペットの内容が継承されなくなっていないかを最初に疑ってください。ヘッダーは届いているのに直らない場合は、アプリ側がプロキシのヘッダーを信用する設定になっていません(Expressの trust proxy、Djangoの SECURE_PROXY_SSL_HEADER など、アプリごとに名前が違います)。
413や504が出る
413はアップロード上限、504はバックエンドの応答待ちの上限です。413はerror.logの client intended to send too large body で確定できます。504はバックエンドのログで処理時間を確かめ、本当に60秒以上かかる処理だけ proxy_read_timeout を延ばします。
nginx -tで警告やエラーが出る
conflicting server name "app.example.com" on 0.0.0.0:80, ignored は、同じserver_nameが2つのファイルにあるという警告で、後から読まれた方は無視されます。sites-enabled/ に編集前のコピーが残っていることが多いので、grep -rn server_name /etc/nginx/sites-enabled/ で重複を探します。unknown directive "http2" は、1.25以降の書き方を1.24で使ったときのエラーです。
動作確認
DNSを登録する前でも、curlの -H 'Host: …' で任意のホスト名としてリクエストを送れば、振り分けを確かめられます。バックエンドの代わりに、受け取ったリクエスト行とヘッダーをそのまま返す小さなPythonスクリプトを4つのポートで起動します。WebSocketのハンドシェイクが届いた場合は101を返すので、Upgradeヘッダーの転送も確認できます。
#!/usr/bin/env python3
# 受け取ったリクエスト行とヘッダーを返すテスト用バックエンド
# WebSocketのUpgrade要求には101を返してすぐ閉じる(ハンドシェイク確認用)
import base64, hashlib, sys
from http.server import BaseHTTPRequestHandler, HTTPServer
WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
class Echo(BaseHTTPRequestHandler):
protocol_version = "HTTP/1.1"
def do_GET(self):
if self.headers.get("Upgrade", "").lower() == "websocket":
key = self.headers["Sec-WebSocket-Key"] + WS_GUID
accept = base64.b64encode(hashlib.sha1(key.encode()).digest()).decode()
self.send_response(101)
self.send_header("Upgrade", "websocket")
self.send_header("Connection", "Upgrade")
self.send_header("Sec-WebSocket-Accept", accept)
self.end_headers()
self.close_connection = True
return
body = f"port={self.server.server_port}\n{self.command} {self.path}\n{self.headers}".encode()
self.send_response(200)
self.send_header("Content-Type", "text/plain; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
port = int(sys.argv[1]) if len(sys.argv) > 1 else 8080
HTTPServer(("127.0.0.1", port), Echo).serve_forever()cd ~/proxy-test
for p in 8080 8081 3000 3001; do python3 echo_headers.py $p & done$ sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
$ curl -s -H 'Host: app.example.com' http://127.0.0.1/api/users | grep -E '^(port|GET|Host|X-)'
port=8081
GET /users
Host: app.example.com
X-Real-IP: 127.0.0.1
X-Forwarded-For: 127.0.0.1
X-Forwarded-Proto: http
X-Forwarded-Host: app.example.com
$ curl -si -H 'Host: kuma.example.com' -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' http://127.0.0.1/ | head -1
HTTP/1.1 101 Switching Protocols
$ sudo tail -n 1 /var/log/nginx/proxy_access.log
127.0.0.1 "kuma.example.com" "GET / HTTP/1.1" 101 upstream=127.0.0.1:3001 ustatus=101 rt=0.001 urt=0.0012つ目のcurlで、/api/users が port=8081 に /users として届き、Hostが app.example.com のまま渡っていれば振り分けとヘッダーは正しく動いています。3つ目で101が返ればWebSocketの設定も有効です。アクセスログの upstream= を見れば、本番でもどのバックエンドが応答したかと、nginx全体の時間(rt)とバックエンドの時間(urt)の差がわかります。rt だけが大きい場合は、クライアント側の回線が遅いと判断できます。確認が終わったら kill %1 %2 %3 %4 でエコーサーバーを止めてください。
よくある質問
Nginxをリバースプロキシとして使うメリットは?
TLS終端、ホスト名とパスでの振り分け、アップロード上限、IP制限を1つの設定ファイルで管理でき、アプリを127.0.0.1に閉じ込めたまま公開できる点です。動作が軽く、certbotのプラグインもあります。一方で証明書の取得や更新の自動化はCaddyほど手軽ではないので、新規の小規模構成ならCaddyも比較候補に入れてください。
リバースプロキシはなぜ必要なのか?
1つのグローバルIPと443番で複数のサービスを公開するには、ホスト名を見て振り分ける役が必要だからです。加えて、証明書の管理を1か所に集め、アプリのポートを外に出さずに済みます。LAN内で自分だけが使うサービスなら、プロキシを挟まずにポート番号で直接アクセスしても実害はありません。
プロキシとリバースプロキシの違いは何ですか?
置き場所と、誰のための中継かが違います。フォワードプロキシは利用者側の出口にあり、社内PCの通信をまとめて制御・記録します。もう一方はサーバー側の入口にあり、外からのリクエストを裏のアプリへ配ります。利用者がプロキシの設定を意識するのは前者だけです。
リバースプロキシの設定方法は?
nginxなら、serverブロックに server_name でホスト名を、locationに proxy_pass http://127.0.0.1:ポート; と proxy_set_header Host $host; を書くのが最小構成です。sudo nginx -t で構文を確かめてから sudo systemctl reload nginx で反映します。
まとめ
振り分けの設定そのものは数行ですが、動かない原因の大半は、proxy_pass の末尾の /、locationで途切れるヘッダーの継承、既定値のままのアップロード上限とタイムアウトの3つに集中します。共通ヘッダーをスニペットにしてすべてのlocationで読み込み、未知のホスト名は default_server で切っておけば、サービスを足すときはserverブロックを1つ書くだけで済みます。困ったときは、エコーサーバーで「バックエンドに何が届いているか」を見るのが最短の切り分けです。