Nginxのリバースプロキシ(Reverse Proxy / リバプロ)は、Web運用の“要”です。アプリケーションサーバ(Node.js / Python / Go など)をインターネットに直接さらさず、Nginxを前段に置くことで、HTTPS終端・負荷分散・パス/ドメイン振り分け・キャッシュ・認証・レート制限といった運用機能をまとめて担えます。
この記事では、まず「リバプロとは何か」を腹落ちさせた上で、Dockerで即再現できるハンズオンを用意し、最後に本番で差が出る設計ポイント(タイムアウト・ヘッダ・WebSocket・バッファ・ログ・セキュリティ)まで丁寧に解説します。
1. リバースプロキシとは?(フォワードプロキシとの違い)
プロキシには大きく2種類あります。
- フォワードプロキシ(Forward Proxy):クライアント側に立つ
例:社内ネットワークから外部サイトへ出るときに代理でアクセスする(閲覧制御・キャッシュ) - リバースプロキシ(Reverse Proxy):サーバ側に立つ
例:インターネットからのアクセスをNginxが受け、バックエンドのアプリへ転送する
つまりリバプロは「ユーザーから見える“入口”」であり、アプリケーションはNginxの裏側(内側)に隠れます。これにより、アプリを直接公開しなくて済む=運用が楽で安全になり、スケールもしやすくなります。
2. なぜNginxをリバプロにするのか(メリット)
代表的なメリットを、実務目線で整理します。
2.1 HTTPS終端(TLS termination)
アプリ側にHTTPSを実装せずとも、Nginxで証明書を持って終端できます。
アプリはHTTPのままでもOK(もちろん内部もTLSにするケースもあります)。
2.2 ルーティング(ドメイン/パスで振り分け)
api.example.com→ APIサーバapp.example.com→ フロントエンド/api→ API、/→ SPA
などをNginx設定で一元管理できます。
2.3 負荷分散(ロードバランシング)
複数のアプリインスタンスへ分散できます。スケールの第一歩です。
2.4 本番に必要な付加価値
- Basic認証、IP制限
- レート制限(DoS対策)
- キャッシュ
- gzip/brotli
- アクセスログ整備、可観測性
3. ハンズオン:Dockerで「Nginxリバプロ→アプリ」構成を作る
このハンズオンでは、最短で「リバプロが何をしているか」を体感できるように、以下を作ります。
- ブラウザ → Nginx(リバプロ) → Node.jsアプリ(バックエンド)
/はアプリへ転送/apiは別のアプリへ転送(“振り分け”を体験)- ついでにログ・ヘッダの基本も確認
前提:Docker / Docker Composeが使える状態
以降、作業ディレクトリでコピペOKです。
3.1 ディレクトリ構成
mkdir nginx-reverse-proxy-handson
cd nginx-reverse-proxy-handson
mkdir -p nginx app1 app2
3.2 バックエンド(Node.js)を2つ用意
app1(ルート用)
app1/server.js
const http = require("http");
const server = http.createServer((req, res) => {
// どのヘッダが届いてるか確認するために返す
const body = {
service: "app1",
path: req.url,
method: req.method,
headers: req.headers,
};
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(body, null, 2));
});
server.listen(3000, () => {
console.log("app1 listening on :3000");
});
app1/package.json
{
"name": "app1",
"version": "1.0.0",
"main": "server.js"
}
app2(/api用)
app2/server.js
const http = require("http");
const server = http.createServer((req, res) => {
const body = {
service: "app2",
path: req.url,
method: req.method,
headers: req.headers,
};
res.setHeader("Content-Type", "application/json; charset=utf-8");
res.end(JSON.stringify(body, null, 2));
});
server.listen(3000, () => {
console.log("app2 listening on :3000");
});
app2/package.json
{
"name": "app2",
"version": "1.0.0",
"main": "server.js"
}
3.3 app用のDockerfile(2つとも同じでOK)
app1/Dockerfile と app2/Dockerfile を作ります。
FROM node:20-alpine
WORKDIR /app
COPY package.json ./
COPY server.js ./
EXPOSE 3000
CMD ["node", "server.js"]
3.4 Nginx設定(ここが本題)
nginx/nginx.conf
events {}
http {
# DockerネットワークのDNSを使うために、resolver指定が必要になるケースがあるが
# 多くの場合は不要。今回の構成では upstream を使う。
upstream app_root {
server app1:3000;
}
upstream app_api {
server app2:3000;
}
server {
listen 80;
# アクセスログ(検証用に見やすいフォーマット)
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
# /api は app2 に転送(パスルーティングの例)
location /api/ {
proxy_pass http://app_api/;
# リバプロで絶対入れるべきヘッダ群
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;
}
# / は app1 に転送
location / {
proxy_pass http://app_root;
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_pass の “末尾スラッシュ” の超重要ポイント
location /api/とproxy_pass http://app_api/;の組み合わせは/api/xxx→http://app_api/xxxになります(/api/が剥がれる)proxy_pass http://app_api;にすると挙動が変わり、パスがそのまま付くなど混乱しがちです
ここは本番でも事故りやすいので、locationとproxy_passのスラッシュ関係は必ず理解してください。
3.5 docker-compose.yml を作る
docker-compose.yml
services:
nginx:
image: nginx:1.27-alpine
ports:
- "8080:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app1
- app2
app1:
build: ./app1
app2:
build: ./app2
3.6 起動して動作確認
docker compose up --build
別ターミナルで確認:
ルート(app1に転送される)
curl -s http://localhost:8080/ | head
JSONで service: "app1" が返ればOK。
/api(app2に転送される)
curl -s http://localhost:8080/api/hello | head
JSONで service: "app2" が返ればOK。
返ってきたJSONの headers を見ると、x-forwarded-for や x-forwarded-proto が付いているはずです。
これが「Nginxが入口となり、アプリに“本来のリクエスト情報”を渡している」状態です。
3.7 Nginxログを確認(運用の入口)
docker compose ps
docker compose logs nginx
Nginxコンテナ内のファイルログも確認できます。
docker compose exec nginx sh -lc "tail -n 20 /var/log/nginx/access.log"
docker compose exec nginx sh -lc "tail -n 50 /var/log/nginx/error.log"
3.8 停止・削除
docker compose down
4. 本番で必須の“設計ポイント”(ここからが差が出る)
ハンズオンで「転送できた」だけだと、まだ本番には足りません。現場でよくハマる点をまとめます。
4.1 タイムアウト(504/499の沼)
アプリが重い処理をすると、Nginx側のタイムアウトで落ちることがあります。
よく調整する代表例:
proxy_connect_timeout 5s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
send_timeout 60s;
- 504 Gateway Timeout:バックエンド応答待ちでタイムアウト
- 499(Nginx特有):クライアントが待ち切れず切断した(Nginxログに出る)
APIが遅い時は「アプリが遅いのか」「Nginxで切られているのか」をログで切り分けます。
4.2 バッファとアップロード(413/422/400周り)
ファイルアップロードや大きなJSONを扱う場合、これが必要になることがあります。
client_max_body_size 20m;
proxy_buffering on;
proxy_buffers 8 16k;
proxy_busy_buffers_size 32k;
特に 413 Request Entity Too Large は定番です。まず client_max_body_size を疑います。
4.3 WebSocket対応(リアルタイム通信)
WebSocketを使う場合は追加設定が必要です。
location /ws/ {
proxy_pass http://app_root;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_read_timeout 3600s;
}
この設定がないと、接続が確立しない/すぐ切れるなどの症状が出がちです。
4.4 静的ファイルとアプリの分離(速度と安定)
- 静的ファイル(画像、CSS、JS)はNginxから配る
- APIやSSRはアプリに任せる
これが基本です。Nginxは静的配信が得意で、アプリの負担を減らせます。キャッシュ戦略も立てやすいです。
4.5 セキュリティの基本(最低限)
不要なヘッダの抑制
server_tokens off;
セキュリティヘッダ(まずは最低限)
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
add_header Referrer-Policy strict-origin-when-cross-origin always;
(CSPは設計が絡むので、別記事として丁寧にやるのがおすすめです)
5. よくあるトラブルシュート(原因の当たりをつける)
502 Bad Gateway
- バックエンドが落ちている
- 接続先ホスト/ポートが間違っている
- upstreamの名前解決ができていない(Dockerならネットワーク/サービス名)
→ docker compose logs app1 / app2 と nginx error.log を見る。
404が返る(特に /api のパス)
locationとproxy_passのスラッシュが原因でパスが変わっている
→ location /api/ + proxy_pass http://app_api/; の組み合わせで再確認。
クライアントIPが取れない
X-Forwarded-For等が付与されていない- さらに前段(CDN/LB)がいる場合は、その前段のヘッダも考慮
コメントを残す