Nginxリバースプロキシ入門:仕組み・設計の勘所・本番の落とし穴まで

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/Dockerfileapp2/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/xxxhttp://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-forx-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 / app2nginx error.log を見る。

404が返る(特に /api のパス)

  • locationproxy_pass のスラッシュが原因でパスが変わっている

location /api/ + proxy_pass http://app_api/; の組み合わせで再確認。

クライアントIPが取れない

  • X-Forwarded-For 等が付与されていない
  • さらに前段(CDN/LB)がいる場合は、その前段のヘッダも考慮

投稿日

カテゴリー:

投稿者:

タグ:

コメント

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です