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 mars 20268 min de lecture1.8k Mots|

Fait partie de notre série Performance & Scalability

Lire le guide complet

Configuration de production Nginx : SSL, mise en cache et sécurité

Nginx est le serveur Web de production qui gère pratiquement tous les déploiements Web à fort trafic. Lorsqu'il est bien configuré, il fournit une terminaison SSL, un service de fichiers statiques efficace, un proxy WebSocket, une limitation de débit et des en-têtes de sécurité, le tout avant qu'une seule requête n'atteigne votre application Node.js. Lorsqu'il est mal configuré, il devient un goulot d'étranglement, un problème de sécurité ou la cause de mystérieuses boucles de redirection Cloudflare.

Ce guide couvre une configuration Nginx de niveau production pour un déploiement Node.js multi-applications : interface Next.js, API NestJS et documents Docusaurus, tous exécutés sur le même serveur derrière Cloudflare.

Points clés à retenir

  • Ne divisez jamais www et non-www en blocs de serveur distincts derrière Cloudflare - cela provoque des boucles de redirection
  • Utilisez un seul fichier de configuration Nginx dans /etc/nginx/conf.d/ — ne créez pas également de lien symbolique dans sites-enabled/
  • X-XSS-Protection est obsolète — utilisez plutôt CSP ; X-Frame-Options: DENY est toujours valide
  • proxy_pass doit inclure la barre oblique finale lors de l'utilisation d'un préfixe de chemin
  • La compression Gzip pour les types texte/* MIME vaut toujours la peine d'être activée
  • La limitation de débit nécessite limit_req_zone défini au niveau http{}, et non au niveau server{}
  • Les certificats Let's Encrypt se renouvellent automatiquement avec Certbot ; ajouter une tâche cron pour recharger Nginx après le renouvellement
  • Le proxy WebSocket nécessite des en-têtes spécifiques : Upgrade et Connection

Structure du répertoire

Gardez la configuration Nginx organisée :

/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

Critique : mettez toute votre configuration dans conf.d/ecosire-production.conf. Ne l'ajoutez PAS également à sites-enabled/ - cela oblige Nginx à traiter la configuration deux fois, ce qui entraîne des erreurs limit_req_zone en double et un comportement inattendu.


Zones de limitation de débit

Définissez des zones de limitation de débit au niveau http{} dans votre configuration. Ils ne peuvent pas être définis à l’intérieur des blocs 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;

Bloc du serveur d'application principal

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

Blocage du serveur 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
    }
}

Proxy WebSocket

Si votre application utilise des WebSockets (Socket.IO, passerelle NestJS WebSocket), la configuration du proxy nécessite des en-têtes spécifiques :

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

Sans les en-têtes Upgrade et Connection, la négociation WebSocket du navigateur échoue avec une erreur 426 Upgrade Required ou 101 Switching Protocols.


SSL direct (sans Cloudflare)

Si vous n'utilisez pas Cloudflare, gérez la résiliation SSL directement dans 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;
}

Configuration spécifique à Cloudflare

Derrière Cloudflare, Nginx ne voit que les IP de Cloudflare. Pour conserver les adresses IP réelles des clients :

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

De plus : ne divisez jamais www.ecosire.com et ecosire.com en blocs de serveur Nginx distincts lorsque vous utilisez Cloudflare. Cloudflare gère la redirection www à sa périphérie. Si vous ajoutez un bloc de serveur www qui redirige vers un site non-www et que Cloudflare redirige également www, vous obtenez une boucle de redirection (ERR_TOO_MANY_REDIRECTS).


Configuration du journal

# 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 de votre configuration

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

Questions fréquemment posées

Dois-je utiliser Nginx ou Caddy pour un nouveau projet ?

Caddy est nettement plus simple à configurer : il gère automatiquement Let's Encrypt SSL et possède des paramètres par défaut raisonnables. Nginx est plus puissant et dispose d’un plus grand écosystème de modules et de documentation. Pour la plupart des nouveaux projets, Caddy est le meilleur point de départ ; passez à Nginx si vous avez besoin d'un contrôle précis sur SSL, d'un routage en amont complexe ou de modules spécifiques à Nginx. Pour les serveurs Linux existants sur lesquels Nginx est déjà installé, restez fidèle à Nginx.

Comment éviter l'erreur limit_req_zone en double ?

La directive limit_req_zone doit apparaître au niveau du contexte http{}, et non à l'intérieur des blocs server{}. Dans un déploiement monorepo Turborepo dans lequel vous incluez plusieurs fichiers de configuration, assurez-vous que la directive n'apparaît qu'une seule fois dans tous les fichiers inclus. Si vous voyez l'erreur, vérifiez si vous avez à la fois un lien symbolique conf.d/app.conf et un lien symbolique sites-enabled/ pointant vers le même fichier.

Comment configurer Nginx pour Next.js ISR (régénération statique incrémentielle) ?

Next.js gère ISR en interne – Nginx a simplement besoin de transmettre par proxy toutes les requêtes à Next.js sans mettre en cache les réponses. N'ajoutez pas d'en-têtes Cache-Control qui interfèrent avec les en-têtes de cache ISR de Next.js. Pour les ressources statiques dans /_next/static/, ajoutez les en-têtes de cache immutable car ces fichiers ont des noms hachés au contenu. Pour toutes les autres routes, laissez Next.js définir les en-têtes de cache.

Pourquoi mon score SSL diminue-t-il lorsque Cloudflare est activé ?

Lorsque Cloudflare est en mode proxy, SSL Labs teste le SSL de Cloudflare, pas le vôtre. Définissez le mode SSL de Cloudflare sur « Complet (Strict) » pour garantir que Cloudflare valide votre certificat d'origine. Installez un certificat Cloudflare Origin sur votre serveur : c'est gratuit et valable 15 ans. Votre score SSL Labs devient le score de Cloudflare (généralement A+), ce qui constitue en fait une amélioration par rapport à une configuration SSL Nginx auto-configurée typique.

Comment gérer plusieurs applications Node.js sur le même serveur ?

Exécutez chaque application sur un port différent (Next.js sur 3000, NestJS sur 3001, Docusaurus sur 3002, etc.) et ajoutez un bloc serveur pour chaque sous-domaine dans votre configuration Nginx. Utilisez PM2 pour gérer tous les processus Node.js. Nginx achemine par sous-domaine (ou préfixe de chemin) vers le bon port. Chaque bloc de serveur peut avoir sa propre configuration de limitation de débit, de mise en cache et d'en-tête de sécurité.


Prochaines étapes

Une configuration de production Nginx est un document évolutif : elle évolue à mesure que les exigences de votre application changent, que vos modèles de trafic évoluent et que de nouvelles bonnes pratiques de sécurité émergent. ECOSIRE exécute Nginx en production au service de plusieurs applications, gérant la terminaison SSL, la limitation de débit et l'intégration Cloudflare dans tous les domaines.

Que vous ayez besoin de conseils DevOps, de configuration d'une infrastructure de production ou d'une conception d'architecture de déploiement complète, explorez nos services pour voir comment nous pouvons vous aider.

E

Rédigé par

ECOSIRE Research and Development Team

Création de produits numériques de niveau entreprise chez ECOSIRE. Partage d'analyses sur les intégrations Odoo, l'automatisation e-commerce et les solutions d'entreprise propulsées par l'IA.

Discutez sur WhatsApp