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
|19. März 20268 Min. Lesezeit1.6k Wörter|

Teil unserer Performance & Scalability-Serie

Den vollständigen Leitfaden lesen

Nginx-Produktionskonfiguration: SSL, Caching und Sicherheit

Nginx ist der Produktions-Webserver, der praktisch alle Webbereitstellungen mit hohem Datenverkehr abwickelt. Bei guter Konfiguration bietet es SSL-Terminierung, effiziente Bereitstellung statischer Dateien, WebSocket-Proxy, Ratenbegrenzung und Sicherheitsheader – und das alles, bevor eine einzelne Anfrage Ihre Node.js-Anwendung erreicht. Bei schlechter Konfiguration kann es zu einem Engpass, einem Sicherheitsrisiko oder zur Ursache mysteriöser Cloudflare-Umleitungsschleifen kommen.

Dieser Leitfaden behandelt eine Nginx-Konfiguration in Produktionsqualität für eine Node.js-Bereitstellung mit mehreren Apps: Next.js-Frontend, NestJS-API und Docusaurus-Dokumente – alle laufen auf demselben Server hinter Cloudflare.

Wichtige Erkenntnisse

– Teilen Sie WWW und Nicht-WWW niemals in separate Serverblöcke hinter Cloudflare auf – dies führt zu Weiterleitungsschleifen – Verwenden Sie eine einzelne Nginx-Konfigurationsdatei in /etc/nginx/conf.d/ – verknüpfen Sie sie nicht auch mit einem Symlink in sites-enabled/X-XSS-Protection ist veraltet – verwenden Sie stattdessen CSP; X-Frame-Options: DENY ist noch gültig – proxy_pass muss den abschließenden Schrägstrich enthalten, wenn ein Pfadpräfix verwendet wird

  • Die Aktivierung der Gzip-Komprimierung für Text/*-MIME-Typen lohnt sich immer – Die Ratenbegrenzung erfordert die Definition von limit_req_zone auf der Ebene http{}, nicht auf der Ebene server{}
  • Let's Encrypt-Zertifikate erneuern sich automatisch mit Certbot; Fügen Sie einen Cron-Job hinzu, um Nginx nach der Erneuerung neu zu laden – WebSocket-Proxying erfordert bestimmte Header: Upgrade und Connection

Verzeichnisstruktur

Halten Sie die Nginx-Konfiguration organisiert:

/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

Kritisch: Geben Sie Ihre gesamte Konfiguration in conf.d/ecosire-production.conf ein. Fügen Sie es NICHT auch zu sites-enabled/ hinzu – dies führt dazu, dass Nginx die Konfiguration zweimal verarbeitet, was zu doppelten limit_req_zone-Fehlern und unerwartetem Verhalten führt.


Ratenbegrenzungszonen

Definieren Sie in Ihrer Konfiguration Ratenbegrenzungszonen auf der Ebene http{}. Sie können nicht innerhalb von server{}-Blöcken definiert werden:

# /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;

Hauptanwendungsserverblock

# 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-Serverblock

# 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-Proxying

Wenn Ihre Anwendung WebSockets (Socket.IO, NestJS WebSocket Gateway) verwendet, benötigt die Proxy-Konfiguration bestimmte Header:

# 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;
}

Ohne Upgrade- und Connection-Header schlägt der WebSocket-Handshake des Browsers mit einem 426 Upgrade Required- oder 101 Switching Protocols-Fehler fehl.


Direktes SSL (ohne Cloudflare)

Wenn Sie Cloudflare nicht verwenden, behandeln Sie die SSL-Beendigung direkt in Nginx:

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-spezifische Konfiguration

Hinter Cloudflare sieht Nginx nur die IPs von Cloudflare. So bewahren Sie echte Client-IPs:

# 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;

Außerdem: Niemals www.ecosire.com und ecosire.com in separate Nginx-Serverblöcke aufteilen, wenn Sie Cloudflare verwenden. Cloudflare übernimmt die WWW-Umleitung an seinem Rand. Wenn Sie einen www-Serverblock hinzufügen, der auf Nicht-www umleitet, und Cloudflare auch www umleitet, erhalten Sie eine Umleitungsschleife (ERR_TOO_MANY_REDIRECTS).


Protokollkonfiguration

# 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;

Testen Sie Ihre Konfiguration

# 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"

Häufig gestellte Fragen

Soll ich Nginx oder Caddy für ein neues Projekt verwenden?

Caddy ist wesentlich einfacher zu konfigurieren – es verarbeitet Let's Encrypt SSL automatisch und verfügt standardmäßig über sinnvolle Standardeinstellungen. Nginx ist leistungsfähiger und verfügt über ein größeres Ökosystem an Modulen und Dokumentation. Für die meisten neuen Projekte ist Caddy der bessere Ausgangspunkt; Wechseln Sie zu Nginx, wenn Sie eine detaillierte Kontrolle über SSL, komplexes Upstream-Routing oder Nginx-spezifische Module benötigen. Bleiben Sie bei vorhandenen Linux-Servern, auf denen Nginx bereits installiert ist, bei Nginx.

Wie vermeide ich den doppelten limit_req_zone-Fehler?

Die limit_req_zone-Direktive muss auf der Kontextebene http{} erscheinen, nicht innerhalb von server{}-Blöcken. Stellen Sie in einer Turborepo-Monorepo-Bereitstellung, in der Sie mehrere Konfigurationsdateien einschließen, sicher, dass die Anweisung in allen enthaltenen Dateien nur einmal vorkommt. Wenn der Fehler angezeigt wird, prüfen Sie, ob sowohl conf.d/app.conf als auch ein sites-enabled/-Symlink auf dieselbe Datei verweisen.

Wie konfiguriere ich Nginx für Next.js ISR (Inkrementelle statische Regeneration)?

Next.js verarbeitet ISR intern – Nginx muss lediglich alle Anfragen an Next.js weiterleiten, ohne die Antworten zwischenzuspeichern. Fügen Sie keine Cache-Control-Header hinzu, die die ISR-Cache-Header von Next.js beeinträchtigen. Fügen Sie für statische Assets in /_next/static/ immutable-Cache-Header hinzu, da diese Dateien inhaltsgehashte Namen haben. Lassen Sie für alle anderen Routen Next.js die Cache-Header festlegen.

Warum sinkt mein SSL-Score, wenn Cloudflare aktiviert ist?

Wenn sich Cloudflare im Proxy-Modus befindet, testet SSL Labs das SSL von Cloudflare, nicht Ihres. Stellen Sie den SSL-Modus von Cloudflare auf „Vollständig (streng)“ ein, um sicherzustellen, dass Cloudflare Ihr Ursprungszertifikat validiert. Installieren Sie ein Cloudflare-Ursprungszertifikat auf Ihrem Server – es ist kostenlos und 15 Jahre gültig. Ihr SSL Labs-Score wird zum Cloudflare-Score (normalerweise A+), was tatsächlich eine Verbesserung gegenüber einem typischen selbstkonfigurierten Nginx-SSL-Setup darstellt.

Wie gehe ich mit mehreren Node.js-Apps auf demselben Server um?

Führen Sie jede App auf einem anderen Port aus (Next.js auf 3000, NestJS auf 3001, Docusaurus auf 3002 usw.) und fügen Sie einen Serverblock für jede Subdomäne in Ihrer Nginx-Konfiguration hinzu. Verwenden Sie PM2, um alle Node.js-Prozesse zu verwalten. Nginx leitet nach Subdomain (oder Pfadpräfix) an den richtigen Port weiter. Jeder Serverblock kann über eine eigene Konfiguration für Ratenbegrenzung, Caching und Sicherheitsheader verfügen.


Nächste Schritte

Eine Nginx-Produktionskonfiguration ist ein lebendiges Dokument – ​​sie entwickelt sich weiter, wenn sich die Anforderungen Ihrer Anwendung ändern, sich Ihre Datenverkehrsmuster ändern und neue Best Practices für die Sicherheit entstehen. ECOSIRE führt Nginx in der Produktion aus und bedient mehrere Anwendungen, wobei es SSL-Terminierung, Ratenbegrenzung und Cloudflare-Integration über alle Domänen hinweg übernimmt.

Ganz gleich, ob Sie DevOps-Beratung, die Einrichtung einer Produktionsinfrastruktur oder einen vollständigen Entwurf einer Bereitstellungsarchitektur benötigen: [erkunden Sie unsere Dienste] (/services), um zu erfahren, wie wir Ihnen helfen können.

E

Geschrieben von

ECOSIRE Research and Development Team

Entwicklung von Enterprise-Digitalprodukten bei ECOSIRE. Einblicke in Odoo-Integrationen, E-Commerce-Automatisierung und KI-gestützte Geschäftslösungen.

Chatten Sie auf WhatsApp