Aller au contenu principal

Nginx

· 9 minutes de lecture

Nginx est un serveur web événementiel conçu pour gérer un grand nombre de connexions simultanées avec une faible empreinte mémoire. Contrairement aux serveurs qui dédient un processus ou un thread à chaque connexion (Apache avec les MPM prefork ou worker), il utilise un modèle non-bloquant à base de boucle d'événements : chaque worker process gère des milliers de connexions sans créer un thread par connexion. Cette architecture le rend particulièrement adapté comme reverse proxy en frontal d'une infrastructure.

Modèle de configuration​

La configuration Nginx s'organise hiérarchiquement : contexte http → blocs server → blocs location. Chaque server écoute sur un port et un nom de domaine. Les location définissent comment traiter les requêtes selon leur URI.

http {
server {
listen 80;
server_name example.com;

location / {
root /var/www/html;
index index.html;
}
}
}

Les directives définies dans un contexte parent sont héritées par les contextes enfants, sauf si elles sont redéfinies localement.

Matching des blocs location​

Nginx sélectionne le bloc location le plus spécifique selon des règles de priorité précises :

location = /exact {        # 1. correspondance exacte (priorité maximale)
...
}

location ^~ /static/ { # 2. préfixe prioritaire (interrompt la recherche regex)
...
}

location ~* \.(jpg|png)$ { # 3. regex insensible à la casse
...
}

location /api/ { # 4. préfixe standard (priorité la plus longue gagne)
...
}

location / { # 5. fallback (correspond à tout)
...
}

L'algorithme réel se déroule en deux temps :

  1. Nginx cherche une correspondance exacte (=) ; si elle existe, la recherche s'arrête.
  2. Sinon, il identifie le préfixe le plus long parmi tous les blocs préfixes (avec ou sans ^~). Si ce préfixe porte le modificateur ^~, il est retenu immédiatement.
  3. Sinon, ce préfixe est mémorisé et les expressions régulières (~ sensible à la casse, ~* insensible) sont testées dans leur ordre d'apparition dans le fichier : la première qui correspond l'emporte.
  4. Si aucune regex ne correspond, le préfixe mémorisé à l'étape 2 est utilisé.

Conséquence pratique : avec la configuration ci-dessus, /static/logo.png est servi par ^~ /static/ (la regex \.(jpg|png)$ n'est jamais évaluée), alors que /api/logo.png est capturé par la regex, bien que /api/ soit un préfixe plus spécifique. try_files $uri $uri/ =404 est le pattern standard pour les SPA : tenter de servir le fichier exact, le répertoire, ou retourner 404.

Reverse proxy​

En tant que reverse proxy, Nginx reçoit les requêtes clients et les transmet à un backend applicatif. Le backend ne voit que l'IP de Nginx ; les en-têtes X-Forwarded-* propagent le contexte de la requête originale.

Un détail de syntaxe modifie l'URI transmise : si proxy_pass contient un chemin (même réduit à /), la partie de l'URI correspondant au préfixe de la location est remplacée par ce chemin. Sans chemin, l'URI est transmise telle quelle.

location /api/ {
proxy_pass http://127.0.0.1:8000; # /api/users → http://127.0.0.1:8000/api/users
}

location /v2/ {
proxy_pass http://127.0.0.1:8000/; # /v2/users → http://127.0.0.1:8000/users
}
server {
listen 80;
server_name api.example.com;

location / {
proxy_pass http://127.0.0.1:8000;

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;

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

# Buffers (évite d'ouvrir la connexion upstream trop longtemps)
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
}

proxy_buffering permet à Nginx de lire la réponse du backend dans des buffers mémoire (puis dans des fichiers temporaires sur disque si elle les dépasse) indépendamment du rythme de lecture du client, ce qui est utile quand les clients sont lents (mobile, connexion faible) pour libérer rapidement le worker backend. Le désactiver pour les flux continus (Server-Sent Events, streaming de réponses), sinon les données restent bloquées dans les buffers ; un backend peut aussi le désactiver requête par requête avec l'en-tête de réponse X-Accel-Buffering: no.

Load balancing​

Nginx distribue le trafic entre plusieurs instances d'un backend via un bloc upstream.

upstream api_backend {
# Algorithmes : round-robin (défaut), least_conn, ip_hash, hash
least_conn;

server 10.0.0.1:8000 weight=3; # 3x plus de trafic
server 10.0.0.2:8000;
server 10.0.0.3:8000 backup; # utilisé si les autres tombent
server 10.0.0.4:8000 max_fails=3 fail_timeout=30s; # health check passif
}

server {
listen 80;
server_name api.example.com;

location / {
proxy_pass http://api_backend;
}
}
AlgorithmeComportementUsage
round-robintour à tourcas général
least_conninstance avec le moins de connexions activesrequêtes longues (WebSocket, upload)
ip_hashmême backend pour un IP donnésessions applicatives sans sticky session
hash $request_urimême backend pour une URI donnéecache cohérent

max_fails et fail_timeout activent le health check passif : après max_fails erreurs en fail_timeout secondes, le backend est marqué indisponible pendant fail_timeout secondes. Le health check actif (sondage régulier) est disponible uniquement dans Nginx Plus (version commerciale).

Terminaison SSL​

Nginx centralise la terminaison TLS : les connexions clients sont chiffrées jusqu'à Nginx, qui communique ensuite en HTTP clair avec le backend sur le réseau interne.

server {
listen 80;
server_name example.com;
return 301 https://$host$request_uri;
}

server {
listen 443 ssl;
http2 on; # depuis Nginx 1.25.1 ; l'ancien paramètre « listen ... http2 » est déprécié
server_name example.com;

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

ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
ssl_prefer_server_ciphers off; # toutes les suites listées sont robustes : le client choisit (profil Mozilla « intermediate »)

ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off; # sans rotation de la clé de ticket, sa fuite compromet la forward secrecy

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options DENY;

location / {
proxy_pass http://127.0.0.1:8000;
}
}

ssl_session_cache shared:SSL:10m partage le cache de sessions TLS entre tous les workers, ce qui évite la répétition du handshake complet pour chaque requête. 10m supporte environ 40 000 sessions simultanées (environ 4 000 sessions par mégaoctet).

L'OCSP stapling (ssl_stapling on) n'a d'effet que si le certificat contient une URL de répondeur OCSP. Let's Encrypt a retiré ces URL de ses certificats et arrêté ses répondeurs OCSP en 2025 au profit des CRL : avec ces certificats, la directive ne produit qu'un avertissement au démarrage.

Les directives add_header suivent une règle d'héritage particulière : elles ne sont héritées du niveau parent que si le niveau courant n'en définit aucune. Un seul add_header dans une location fait disparaître tous ceux déclarés dans le server englobant. Le paramètre always ajoute l'en-tête quel que soit le code de réponse (par défaut, seulement pour 200, 201, 204, 206, 301, 302, 303, 304, 307 et 308).

Compression gzip​

http {
gzip on;
gzip_comp_level 5; # 1 (rapide, faible gain) à 9 (lent) ; au-delà de 5 le gain devient marginal
gzip_min_length 256; # ne pas compresser les très petits fichiers
gzip_proxied any; # compresser aussi les réponses aux requêtes arrivant via un proxy (en-tête Via)
gzip_vary on; # ajouter "Vary: Accept-Encoding" à la réponse

# text/html est toujours compressé : le lister provoque un avertissement "duplicate MIME type"
gzip_types
application/javascript
application/json
application/xml
text/css
text/plain
image/svg+xml;
}

gzip_vary on est important pour les caches intermédiaires (CDN, Nginx lui-même) : ils savent qu'ils doivent stocker deux versions de la ressource (compressée et non compressée) selon le client.

Cache des réponses proxy​

http {
proxy_cache_path /var/cache/nginx levels=1:2
keys_zone=api_cache:10m
max_size=1g
inactive=60m;

server {
location /api/ {
proxy_pass http://api_backend;
proxy_cache api_cache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_key $scheme$host$request_uri;
proxy_cache_use_stale error timeout updating;

add_header X-Cache-Status $upstream_cache_status; # HIT / MISS / BYPASS
}
}
}

proxy_cache_use_stale permet de servir une réponse expirée si le backend est indisponible ou lent, un comportement proche d'un « circuit breaker » basique. Avec updating, une seule requête rafraîchit l'entrée expirée pendant que les autres reçoivent la version en cache ; proxy_cache_lock on applique le même principe aux entrées absentes du cache, ce qui évite qu'une rafale de MISS simultanés n'atteigne le backend.

Rate limiting​

http {
# Définir une zone de limitation : 10 Mo de mémoire, 10 req/s par IP
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
# burst=20 : file d'attente de 20 requêtes
# nodelay : traiter les requêtes en burst immédiatement (sans délai artificiel)
proxy_pass http://api_backend;
}
}
}

Sans nodelay, les requêtes en burst sont traitées au rythme du rate (une toutes les 100ms pour 10r/s). Avec nodelay, les burst requêtes sont traitées immédiatement, mais les suivantes sont rejetées tant que la file est pleine. Les places de la file se libèrent au rythme défini par rate. Le code de rejet par défaut est 503 ; limit_req_status 429; renvoie un code plus explicite pour les clients d'API.

$binary_remote_addr occupe 4 octets (IPv4) ou 16 octets (IPv6), contre jusqu'à 15 ou 39 octets pour $remote_addr : une zone de 1 Mo stocke ainsi environ 16 000 états en IPv4. Derrière un autre proxy ou un load balancer, cette variable contient l'adresse du proxy et non celle du client : le module realip (set_real_ip_from, real_ip_header X-Forwarded-For) doit alors être configuré pour que la limitation porte sur les vrais clients.

Logs et format personnalisé​

http {
log_format main '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'rt=$request_time uct=$upstream_connect_time '
'uht=$upstream_header_time urt=$upstream_response_time';

access_log /var/log/nginx/access.log main;
error_log /var/log/nginx/error.log warn;
}

$request_time mesure le temps total de traitement de la requête côté Nginx. $upstream_response_time mesure le temps passé à attendre la réponse du backend, utile pour distinguer les lenteurs réseau des lenteurs applicatives.

Rechargement de la configuration​

# Vérifier la syntaxe avant de recharger
nginx -t

# Recharger sans interrompre les connexions en cours
sudo systemctl reload nginx

# Ou directement via le signal
sudo nginx -s reload

nginx -t teste la syntaxe et charge les fichiers includes sans appliquer les changements. À utiliser systématiquement avant un reload en production : une erreur de syntaxe avec reload n'interrompt pas le process en cours, mais un restart sur une config invalide stoppe le service.