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 部署。如果配置得当,它可以提供 SSL 终止、高效的静态文件服务、WebSocket 代理、速率限制和安全标头 — 所有这些都在单个请求到达您的 Node.js 应用程序之前完成。如果配置不当,它就会成为瓶颈、安全责任或神秘的 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 必须包含尾部斜杠
  • 文本/* MIME 类型的 Gzip 压缩始终值得启用
  • 速率限制需要在 http{} 级别定义 limit_req_zone,而不是 server{} 级别
  • Let's Encrypt 证书使用 Certbot 自动续订;添加一个 cron 作业以在续订后重新加载 Nginx
  • WebSocket 代理需要特定标头:UpgradeConnection

目录结构

保持 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 处理配置两次,从而导致重复的 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 代理

如果您的应用程序使用 WebSockets(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;
}

如果没有 UpgradeConnection 标头,浏览器的 WebSocket 握手将失败并出现 426 Upgrade Required101 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 指令必须出现在 http{} 上下文级别,而不是出现在 server{} 块内。在包含多个配置文件的 Turborepo monorepo 部署中,请确保该指令在所有包含的文件中仅出现一次。如果您看到错误,请检查是否有 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 实验室会测试 Cloudflare 的 SSL,而不是您的 SSL。将 Cloudflare 的 SSL 模式设置为“完全(严格)”以确保 Cloudflare 验证您的原始证书。在您的服务器上安装 Cloudflare Origin 证书 — 它免费且有效期为 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 集成、电商自动化和 AI 驱动商业解决方案的洞见。

通过 WhatsApp 聊天