Nginx Production Configuration: SSL, Caching, and Security

Nginx production configuration guide: SSL termination, HTTP/2, caching headers, security headers, rate limiting, reverse proxy setup, and Cloudflare integration patterns.

E
ECOSIRE Research and Development Team
|2026年3月19日5 分で読める1.1k 語数|

Performance & Scalabilityシリーズの一部

完全ガイドを読む

Nginx 実稼働構成: SSL、キャッシュ、セキュリティ

Nginx は、事実上すべての高トラフィック Web 展開を処理する運用 Web サーバーです。適切に構成すると、単一のリクエストが Node.js アプリケーションに到達する前に、SSL 終了、効率的な静的ファイルの提供、WebSocket プロキシ、レート制限、セキュリティ ヘッダーが提供されます。設定が不適切な場合、ボトルネック、セキュリティ上の責任、または謎の Cloudflare リダイレクト ループの原因になります。

このガイドでは、マルチアプリ Node.js デプロイメントのための実稼働グレードの Nginx 構成について説明します。Next.js フロントエンド、NestJS API、Docusaurus ドキュメントはすべて、Cloudflare の背後にある同じサーバー上で実行されます。

重要なポイント

  • www と非 www を Cloudflare の背後で個別のサーバー ブロックに分割しないでください - リダイレクト ループが発生します
  • /etc/nginx/conf.d/ で単一の Nginx 構成ファイルを使用します — sites-enabled/ でもシンボリックリンクしないでください
  • X-XSS-Protection は非推奨です - 代わりに CSP を使用してください。 X-Frame-Options: DENY はまだ有効です
  • パス接頭辞を使用する場合、proxy_pass には末尾のスラッシュを含める必要があります
  • text/* MIME タイプの Gzip 圧縮は常に有効にする価値があります
  • レート制限には、server{} レベルではなく、http{} レベルで定義された limit_req_zone が必要です
  • Let's Encrypt 証明書は Certbot で自動更新されます。更新後に Nginx をリロードするための cron ジョブを追加します
  • WebSocket プロキシには特定のヘッダーが必要です: Upgrade および Connection

ディレクトリ構造

Nginx 構成を整理しておきます。

/etc/nginx/
  nginx.conf                    — Main config (rarely touch this)
  conf.d/
    ecosire-production.conf     — All your server blocks in ONE file
  snippets/
    ssl-params.snippet          — SSL hardening (included by server blocks)
    proxy-params.snippet        — Common proxy headers
    security-headers.snippet    — Security headers

重要: すべての設定を conf.d/ecosire-production.conf に入れます。これを sites-enabled/ にも追加しないでください。これにより、Nginx が構成を 2 回処理することになり、重複した limit_req_zone エラーや予期しない動作が発生します。


レート制限ゾーン

構成内の http{} レベルでレート制限ゾーンを定義します。 server{} ブロック内で定義することはできません。

# /etc/nginx/conf.d/ecosire-production.conf

# Must be at http{} level — these are in the main conf or conf.d root
limit_req_zone $binary_remote_addr zone=api_general:10m rate=60r/m;
limit_req_zone $binary_remote_addr zone=api_auth:10m rate=10r/m;
limit_req_zone $binary_remote_addr zone=api_public:10m rate=30r/m;
limit_req_zone $binary_remote_addr zone=static_files:10m rate=200r/m;

メイン アプリケーション サーバー ブロック

# Main web application — Next.js on port 3000
server {
    listen 80;
    listen [::]:80;
    server_name ecosire.com www.ecosire.com;

    # Cloudflare handles SSL termination — Nginx only sees HTTP
    # (If direct SSL, add listen 443 ssl and certificate paths)

    # Security headers
    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
    # CSP — adjust based on your needs
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' https://js.stripe.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://js.stripe.com; connect-src 'self' https://api.ecosire.com;" always;

    # Remove server version from responses
    server_tokens off;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types
      text/plain
      text/css
      text/javascript
      application/javascript
      application/json
      application/xml
      image/svg+xml
      font/woff2;
    gzip_min_length 1024;

    # ─── Static Files: Next.js build output ─────────────────────────
    location /_next/static/ {
        proxy_pass http://127.0.0.1:3000;
        add_header Cache-Control "public, max-age=31536000, immutable";
        # Files include content hash in filename — safe to cache forever
    }

    # ─── Public static assets ────────────────────────────────────────
    location /assets/ {
        proxy_pass http://127.0.0.1:3000;
        add_header Cache-Control "public, max-age=86400";  # 1 day
    }

    # ─── Well-known files (no locale prefix) ─────────────────────────
    location /.well-known/ {
        proxy_pass http://127.0.0.1:3000;
        add_header Cache-Control "public, max-age=86400";
    }

    # ─── App routes (rate limited) ───────────────────────────────────
    location / {
        limit_req zone=api_general burst=20 nodelay;
        limit_req_status 429;

        proxy_pass http://127.0.0.1:3000;
        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-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_cache_bypass $http_upgrade;

        # Timeouts
        proxy_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }
}

APIサーバーブロック

# NestJS API — port 3001
server {
    listen 80;
    listen [::]:80;
    server_name api.ecosire.com;

    server_tokens off;

    add_header X-Frame-Options "DENY" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    # CORS — handled by NestJS, but Nginx can add preflight response
    # for performance (avoids reaching Node.js for OPTIONS)
    location / {
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin "https://ecosire.com";
            add_header Access-Control-Allow-Methods "GET, POST, PUT, PATCH, DELETE, OPTIONS";
            add_header Access-Control-Allow-Headers "Content-Type, Authorization";
            add_header Access-Control-Allow-Credentials "true";
            add_header Access-Control-Max-Age 1728000;
            add_header Content-Length 0;
            return 204;
        }

        limit_req zone=api_general burst=30 nodelay;

        proxy_pass http://127.0.0.1:3001;
        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_connect_timeout 60s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;
    }

    # Stricter rate limits for auth endpoints
    location ~ ^/api/auth/(login|exchange|callback) {
        limit_req zone=api_auth burst=5 nodelay;
        limit_req_status 429;

        proxy_pass http://127.0.0.1:3001;
        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;
    }

    # Health check — no rate limiting
    location /api/health {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        access_log off;  # Don't log health check spam
    }

    # Stripe webhook — needs raw body
    location /api/billing/webhook {
        proxy_pass http://127.0.0.1:3001;
        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;
        # No rate limiting on webhooks — Stripe IPs are trusted
        # NestJS validates the Stripe signature
    }
}

WebSocket プロキシ

アプリケーションが WebSocket (Socket.IO、NestJS WebSocket ゲートウェイ) を使用する場合、プロキシ構成には特定のヘッダーが必要です。

# WebSocket upgrade handling
location /ws/ {
    proxy_pass http://127.0.0.1:3001;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";  # Capital U required
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    # WebSocket connections can be long-lived
    proxy_read_timeout 3600s;
    proxy_send_timeout 3600s;
}

Upgrade ヘッダーと Connection ヘッダーがないと、ブラウザーの WebSocket ハンドシェイクは 426 Upgrade Required または 101 Switching Protocols エラーで失敗します。


ダイレクト SSL (Cloudflare なし)

Cloudflareを使用しない場合は、NginxでSSL終了を直接処理します。

server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name ecosire.com;

    ssl_certificate /etc/letsencrypt/live/ecosire.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ecosire.com/privkey.pem;

    # SSL hardening
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers off;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 1d;
    ssl_stapling on;
    ssl_stapling_verify on;
    resolver 8.8.8.8 8.8.4.4 valid=300s;

    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

    # ... rest of server block
}

# HTTP to HTTPS redirect
server {
    listen 80;
    listen [::]:80;
    server_name ecosire.com www.ecosire.com;
    return 301 https://ecosire.com$request_uri;
}

Cloudflare 固有の構成

Cloudflareの背後では、NginxはCloudflareのIPのみを認識します。実際のクライアント IP を保存するには:

# Real IP from Cloudflare — add this in http{} or at top of server{}
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/13;
set_real_ip_from 104.24.0.0/14;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2a06:98c0::/29;
set_real_ip_from 2c0f:f248::/32;
real_ip_header CF-Connecting-IP;

また: Cloudflare を使用する場合は、www.ecosire.comecosire.com を別々の Nginx サーバー ブロックに分割しないでください。 Cloudflareはエッジでwwwリダイレクトを処理します。 www 以外にリダイレクトする www サーバー ブロックを追加し、Cloudflare も www をリダイレクトすると、リダイレクト ループ (ERR_TOO_MANY_REDIRECTS) が発生します。


ログ設定

# Custom log format with useful fields
log_format json_combined escape=json
  '{'
  '"time":"$time_iso8601",'
  '"remote_addr":"$remote_addr",'
  '"cf_ip":"$http_cf_connecting_ip",'
  '"method":"$request_method",'
  '"uri":"$request_uri",'
  '"status":$status,'
  '"body_bytes":$body_bytes_sent,'
  '"response_time":$request_time,'
  '"referrer":"$http_referer",'
  '"user_agent":"$http_user_agent"'
  '}';

access_log /var/log/nginx/ecosire-access.log json_combined;
error_log /var/log/nginx/ecosire-error.log warn;

構成をテストする

# Test syntax before applying
nginx -t

# Reload without downtime
nginx -s reload

# Check which config file is active
nginx -T | grep "configuration file"

# Test a specific server block
curl -I https://ecosire.com
curl -I https://api.ecosire.com/api/health

# Check security headers
curl -I https://ecosire.com | grep -E "X-Frame|X-Content|Referrer|Content-Security"

よくある質問

新しいプロジェクトには Nginx と Caddy を使用するべきですか?

Caddy は設定が非常に簡単です。Let's Encrypt SSL を自動的に処理し、すぐに使用できる適切なデフォルト値を備えています。 Nginx はより強力で、モジュールとドキュメントのより大規模なエコシステムを備えています。ほとんどの新しいプロジェクトでは、Caddy が出発点として適しています。 SSL、複雑なアップストリーム ルーティング、または Nginx 固有のモジュールをきめ細かく制御する必要がある場合は、Nginx に切り替えてください。 Nginx がすでにインストールされている既存の Linux サーバーの場合は、Nginx をそのまま使用してください。

limit_req_zone の重複エラーを回避するにはどうすればよいですか?

limit_req_zone ディレクティブは、server{} ブロック内ではなく、http{} コンテキスト レベルで指定する必要があります。複数の構成ファイルをインクルードする Turborepo モノリポ デプロイメントでは、ディレクティブがインクルードされるすべてのファイルにわたって 1 回だけ表示されるようにしてください。エラーが表示された場合は、conf.d/app.confsites-enabled/ の両方のシンボリックリンクが同じファイルを指しているかどうかを確認してください。

Next.js ISR (増分静的再生成) 用に Nginx を構成するにはどうすればよいですか?

Next.js は ISR を内部で処理します。Nginx は、応答をキャッシュせずに、すべてのリクエストを Next.js にプロキシするだけで済みます。 Next.js の ISR キャッシュ ヘッダーを妨げる Cache-Control ヘッダーを追加しないでください。 /_next/static/ の静的アセットの場合、これらのファイルにはコンテンツがハッシュされた名前が付けられているため、immutable キャッシュ ヘッダーを追加します。他のすべてのルートについては、Next.js にキャッシュ ヘッダーを設定させます。

Cloudflare が有効になっていると、SSL スコアが低下するのはなぜですか?

Cloudflareがプロキシモードの場合、SSL LabsはあなたのSSLではなくCloudflareのSSLをテストします。 Cloudflareがオリジン証明書を確実に検証できるように、CloudflareのSSLモードを「フル(厳密)」に設定します。 Cloudflare Origin Certificate をサーバーにインストールします。これは無料で、15 年間有効です。 SSL Labs のスコアは Cloudflare のスコア (通常は A+) になり、実際には一般的な自己構成の Nginx SSL セットアップよりも改善されています。

同じサーバー上で複数の Node.js アプリを処理するにはどうすればよいですか?

各アプリを異なるポート (Next.js は 3000、NestJS は 3001、Docusaurus は 3002 など) で実行し、Nginx 構成内のサブドメインごとにサーバー ブロックを追加します。 PM2 を使用してすべての Node.js プロセスを管理します。 Nginx は、サブドメイン (またはパス プレフィックス) によって正しいポートにルーティングされます。各サーバー ブロックは、独自のレート制限、キャッシュ、セキュリティ ヘッダー構成を持つことができます。


次のステップ

本番環境の Nginx 構成は生きたドキュメントです。アプリケーションの要件の変化、トラフィック パターンの変化、新しいセキュリティのベスト プラクティスの出現に応じて進化します。 ECOSIRE は、実稼働環境で Nginx を実行し、複数のアプリケーションを提供し、SSL 終了、レート制限、およびすべてのドメインにわたる Cloudflare 統合を処理します。

DevOps コンサルティング、運用インフラストラクチャのセットアップ、または完全な導入アーキテクチャ設計が必要な場合でも、当社のサービスをご覧ください して、当社がどのようにお手伝いできるかをご確認ください。

E

執筆者

ECOSIRE Research and Development Team

ECOSIREでエンタープライズグレードのデジタル製品を開発。Odoo統合、eコマース自動化、AI搭載ビジネスソリューションに関するインサイトを共有しています。

WhatsAppでチャット