Nginx en production : 7 optimisations qui changent tout

Guide complet pour optimiser Nginx en production : workers, compression, cache, buffers, HTTP/2, rate limiting et monitoring. Configurations testées et prêtes à déployer.

Nginx est un serveur web redoutablement efficace dès son installation par défaut. Il gère sans broncher des milliers de connexions simultanées là où d'autres serveurs s'effondrent. Mais la configuration par défaut reste généraliste : elle doit fonctionner partout, du Raspberry Pi au serveur 64 cœurs. En monitoring Linux, on peut faire beaucoup mieux.

Après avoir optimisé Nginx sur plusieurs dizaines de serveurs de Docker en production, j'ai identifié 7 axes d'amélioration qui font systématiquement la différence. Chaque optimisation est accompagnée de configurations concrètes, testées et prêtes à déployer. On ne parle pas de micro-optimisations théoriques, mais de changements qui se mesurent dans les temps de réponse et la capacité de charge.

Règle d'or : toujours mesurer avant et après chaque modification. Une optimisation qui n'est pas mesurée n'est qu'une supposition.

1. Worker processes et connexions

La première chose à configurer, ce sont les workers. Par défaut, Nginx lance souvent un seul worker process, ce qui sous-exploite complètement un serveur multi-cœurs. Chaque worker est un processus indépendant capable de gérer des milliers de connexions grâce au modèle événementiel.

Configuration optimale des workers

# /etc/nginx/nginx.conf - Bloc principal

# Un worker par cœur CPU disponible
worker_processes auto;

# Lier chaque worker à un cœur spécifique (évite le context switching)
worker_cpu_affinity auto;

# Nombre max de fichiers ouverts par worker
worker_rlimit_nofile 65535;

events {
    # Connexions simultanées par worker
    # Règle : worker_connections * worker_processes = max connexions totales
    worker_connections 4096;

    # Utiliser epoll sur Linux (bien plus performant que select/poll)
    use epoll;

    # Accepter plusieurs connexions en une seule itération
    multi_accept on;
}

La directive worker_processes auto détecte automatiquement le nombre de cœurs CPU. Sur un serveur 8 cœurs avec 4096 connexions par worker, on obtient une capacité théorique de 32 768 connexions simultanées. En pratique, c'est largement suffisant pour la majorité des sites.

Vérifier les limites système

Ces réglages ne servent à rien si le système d'exploitation bride Nginx. Il faut ajuster les limites du noyau :

# Vérifier la limite actuelle de fichiers ouverts
ulimit -n

# Augmenter dans /etc/security/limits.conf
echo "nginx soft nofile 65535" | sudo tee -a /etc/security/limits.conf
echo "nginx hard nofile 65535" | sudo tee -a /etc/security/limits.conf

# Augmenter les connexions TCP en attente
sudo sysctl -w net.core.somaxconn=65535
sudo sysctl -w net.ipv4.tcp_max_syn_backlog=65535

2. Compression Gzip et Brotli

La compression réduit drastiquement la taille des réponses envoyées aux clients. Un fichier HTML de 100 Ko se compresse souvent à 15-20 Ko, soit une réduction de 80%. Sur des connexions mobiles ou des liens à faible bande passante, l'impact est immédiat.

Configuration Gzip optimisée

http {
    # Activer la compression Gzip
    gzip on;

    # Compresser aussi pour les proxies
    gzip_proxied any;

    # Niveau de compression (1-9) : 4-6 est le meilleur ratio CPU/compression
    gzip_comp_level 5;

    # Taille minimale pour déclencher la compression (inutile sous 256 octets)
    gzip_min_length 256;

    # Indiquer aux caches que le contenu varie selon Accept-Encoding
    gzip_vary on;

    # Types MIME à compresser
    gzip_types
        text/plain
        text/css
        text/javascript
        text/xml
        application/json
        application/javascript
        application/xml
        application/xml+rss
        application/atom+xml
        application/vnd.ms-fontobject
        font/opentype
        image/svg+xml
        image/x-icon;
}

Ajouter Brotli pour aller plus loin

Brotli offre un taux de compression 15 à 20% supérieur à Gzip pour les contenus textuels. Tous les navigateurs modernes le supportent. L'idéal est de pré-compresser les fichiers statiques avec brotli_static pour éviter la compression à la volée :

# Module Brotli (nécessite ngx_brotli compilé)
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml image/svg+xml;

# Servir les fichiers .br pré-compressés s'ils existent
brotli_static on;
# Pré-compresser les assets statiques avec Brotli
find /var/www/site/assets -type f \( -name "*.css" -o -name "*.js" -o -name "*.svg" \) \
    -exec brotli --best --keep {} \;

3. Cache statique agressif

Le cache navigateur est l'optimisation la plus sous-estimée. Un visiteur qui revient sur votre site ne devrait jamais re-télécharger les fichiers CSS, JavaScript ou les images qui n'ont pas changé. Le gain est double : temps de chargement réduit pour l'utilisateur, et charge serveur allégée.

Stratégie de cache par type de fichier

server {
    # Assets versionnés (avec hash dans le nom) : cache très long
    location ~* \.(css|js)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
        add_header X-Cache-Strategy "immutable-asset";
    }

    # Images et polices : cache long
    location ~* \.(jpg|jpeg|png|gif|webp|avif|ico|woff2|woff|ttf|eot)$ {
        expires 6M;
        add_header Cache-Control "public, no-transform";
        access_log off;
    }

    # Fichiers SVG et favicons
    location ~* \.(svg|svgz)$ {
        expires 1y;
        add_header Cache-Control "public";
        add_header Content-Encoding gzip;
    }

    # Pages HTML : cache court ou validation obligatoire
    location ~* \.html$ {
        expires 1h;
        add_header Cache-Control "public, must-revalidate";
    }
}

Le header immutable est particulièrement puissant : il indique au navigateur que le fichier ne changera jamais à cette URL. Combiné avec un système de versioning des assets (hash dans le nom de fichier), c'est la stratégie de cache la plus efficace possible.

4. Buffers et timeouts

Les buffers contrôlent comment Nginx gère la mémoire pour chaque connexion. Des buffers trop petits forcent des écritures disque temporaires (lent), des buffers trop grands gaspillent la RAM. Les timeouts protègent contre les connexions fantômes qui consomment des ressources.

Configuration des buffers

http {
    # Buffers pour le corps des requêtes clients
    client_body_buffer_size 16k;
    client_max_body_size 50m;
    client_header_buffer_size 1k;
    large_client_header_buffers 4 16k;

    # Buffers pour les réponses des backends (proxy_pass)
    proxy_buffer_size 128k;
    proxy_buffers 4 256k;
    proxy_busy_buffers_size 256k;

    # Fichiers temporaires (si les buffers débordent)
    proxy_temp_file_write_size 256k;
}

Timeouts ajustés

http {
    # Timeout de lecture du header client
    client_header_timeout 15s;

    # Timeout de lecture du body client
    client_body_timeout 15s;

    # Timeout d'envoi de la réponse au client
    send_timeout 15s;

    # Connexions keep-alive : garder ouvertes mais pas indéfiniment
    keepalive_timeout 30s;
    keepalive_requests 1000;

    # Timeouts pour les backends
    proxy_connect_timeout 10s;
    proxy_read_timeout 30s;
    proxy_send_timeout 15s;
}

Le keepalive_timeout est crucial. Trop bas (5s), les clients doivent constamment rouvrir des connexions TCP, ce qui coûte cher avec TLS. Trop haut (120s), les connexions inactives s'accumulent et saturent les workers. La valeur de 30 secondes est un bon compromis pour la plupart des cas.

5. HTTP/2 et TLS optimisé

HTTP/2 apporte le multiplexage (plusieurs requêtes sur une seule connexion TCP), la compression des headers et le server push. Mais ses bénéfices dépendent d'une configuration TLS correcte. Un TLS mal configuré peut annuler tous les gains de HTTP/2.

Configuration TLS performante et sécurisée

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    # Certificats
    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    # Protocoles : TLS 1.2 et 1.3 uniquement
    ssl_protocols TLSv1.2 TLSv1.3;

    # Suites de chiffrement optimisées
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    # Cache de session TLS (évite le handshake complet à chaque connexion)
    ssl_session_cache shared:SSL:20m;
    ssl_session_timeout 1d;
    ssl_session_tickets off;

    # OCSP Stapling : le serveur fournit la preuve de validité du certificat
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
    resolver 1.1.1.1 8.8.8.8 valid=300s;
    resolver_timeout 5s;

    # Headers de sécurité
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
}

L'OCSP Stapling est souvent oublié. Sans lui, le navigateur doit contacter l'autorité de certification pour vérifier la validité du certificat, ajoutant 100 à 300 ms au premier chargement. Avec le stapling, Nginx fournit directement cette information, éliminant cette latence.

6. Rate limiting et protection

Un serveur en production sans rate limiting est une cible facile. Même sans attaque DDoS massive, un simple script qui boucle sur vos pages peut saturer vos backends. Nginx intègre nativement des mécanismes de limitation efficaces.

Définir les zones de limitation

http {
    # Zone de limitation par IP : 10 requêtes par seconde
    limit_req_zone $binary_remote_addr zone=general:10m rate=10r/s;

    # Zone spécifique pour les pages de login (plus restrictive)
    limit_req_zone $binary_remote_addr zone=login:10m rate=2r/s;

    # Limitation du nombre de connexions simultanées par IP
    limit_conn_zone $binary_remote_addr zone=addr:10m;
}

server {
    # Appliquer la limitation générale avec un burst autorisé
    limit_req zone=general burst=20 nodelay;

    # Maximum 50 connexions simultanées par IP
    limit_conn addr 50;

    # Page d'erreur personnalisée pour les requêtes limitées
    error_page 429 /429.html;

    # Protection renforcée sur le login
    location /login {
        limit_req zone=login burst=5 nodelay;
        limit_conn addr 10;
        proxy_pass http://backend;
    }

    # Bloquer les user-agents suspects
    if ($http_user_agent ~* (bot|crawler|scanner|nikto|sqlmap)) {
        return 403;
    }
}

Protection anti-DDoS basique

# Limiter la taille des requêtes pour éviter les attaques par saturation
client_max_body_size 10m;
client_body_timeout 10s;
client_header_timeout 10s;

# Fermer les connexions lentes (slowloris)
reset_timedout_connection on;

# Bloquer les requêtes sans Host header (scanners)
server {
    listen 80 default_server;
    server_name _;
    return 444;
}

Le code de retour 444 est spécifique à Nginx : il ferme la connexion sans envoyer aucune réponse. C'est plus efficace qu'un 403 car le serveur ne gaspille aucune bande passante pour répondre aux requêtes malveillantes.

7. Monitoring et observabilité

Optimiser sans monitorer, c'est piloter à l'aveugle. Nginx fournit des métriques natives avec le module stub_status, et on peut aller beaucoup plus loin avec des logs structurés et un export Prometheus.

Activer stub_status

# Endpoint de métriques basiques (accès restreint)
server {
    listen 127.0.0.1:8080;

    location /nginx_status {
        stub_status;
        allow 127.0.0.1;
        deny all;
    }
}
# Interroger les métriques
curl http://127.0.0.1:8080/nginx_status

# Résultat typique :
# Active connections: 291
# server accepts handled requests
#  16630948 16630948 31070465
# Reading: 6 Writing: 179 Waiting: 106

Logs structurés en JSON

Les logs texte classiques sont difficiles à parser. Un format JSON permet une intégration directe avec des outils comme Loki, Elasticsearch ou Datadog :

log_format json_combined escape=json
    '{"time":"$time_iso8601",'
    '"remote_addr":"$remote_addr",'
    '"request_method":"$request_method",'
    '"request_uri":"$request_uri",'
    '"status":$status,'
    '"body_bytes_sent":$body_bytes_sent,'
    '"request_time":$request_time,'
    '"upstream_response_time":"$upstream_response_time",'
    '"http_user_agent":"$http_user_agent",'
    '"http_referer":"$http_referer",'
    '"ssl_protocol":"$ssl_protocol",'
    '"ssl_cipher":"$ssl_cipher"}';

access_log /var/log/nginx/access.json json_combined buffer=32k flush=5s;

Métriques pour Prometheus

Pour un monitoring avancé, le module nginx-prometheus-exporter expose les métriques au format Prometheus. Il se base sur stub_status et ajoute des métriques détaillées :

# Installer l'exporter
wget https://github.com/nginxinc/nginx-prometheus-exporter/releases/latest/download/nginx-prometheus-exporter_linux_amd64.tar.gz
tar xzf nginx-prometheus-exporter_linux_amd64.tar.gz

# Lancer l'exporter
./nginx-prometheus-exporter -nginx.scrape-uri=http://127.0.0.1:8080/nginx_status

# Les métriques sont disponibles sur http://localhost:9113/metrics
# nginx_connections_active, nginx_http_requests_total, etc.

Récapitulatif et résultats

Voici un résumé des 7 optimisations et leur impact typique mesuré sur un serveur de production (4 cœurs, 8 Go RAM, SSD NVMe) servant un site à environ 50 000 visiteurs par jour :

  • Workers et connexions : capacité de traitement multipliée par le nombre de cœurs
  • Compression Gzip/Brotli : réduction de 70 à 85% de la bande passante pour les contenus textuels
  • Cache statique agressif : suppression de 60 à 80% des requêtes serveur pour les visiteurs récurrents
  • Buffers et timeouts : élimination des écritures disque temporaires et des connexions zombie
  • HTTP/2 et TLS optimisé : réduction de 200 à 400 ms sur le premier chargement grâce à l'OCSP Stapling et au cache de session
  • Rate limiting : protection contre les abus sans impact sur le trafic légitime
  • Monitoring : visibilité complète pour détecter les problèmes avant qu'ils ne deviennent critiques

Sur nos benchmarks, l'ensemble de ces optimisations a permis de passer de 850 requêtes/seconde à 3 200 requêtes/seconde sur le même matériel, avec un Time to First Byte (TTFB) moyen divisé par trois.

# Benchmark rapide avec wrk
wrk -t4 -c200 -d30s https://example.com/

# Avant optimisation :
# Requests/sec: 847.23 | Avg Latency: 236ms

# Après optimisation :
# Requests/sec: 3214.56 | Avg Latency: 62ms

Appliquez ces optimisations une par une, en mesurant l'impact à chaque étape. Commencez par les workers et la compression : ce sont les gains les plus immédiats. Puis ajustez les buffers, le cache et le TLS. Le rate limiting et le monitoring viennent en dernier, mais ne les négligez pas.

Chaque serveur est différent, chaque application a ses spécificités. Les valeurs proposées ici sont des points de départ solides, mais la meilleure configuration est toujours celle qui est mesurée et ajustée en fonction de votre charge réelle.

Cet article vous a plu ?

Commentaires

Morgann Riu
Morgann Riu

Expert en cybersécurité et administration Linux. J'aide les entreprises à sécuriser et optimiser leurs infrastructures critiques.

Retour au blog

Checklist Sécurité Linux

30 points essentiels pour sécuriser un serveur Linux. Recevez aussi les nouveaux tutoriels par email.

Pas de spam. Désabonnement en 1 clic.