Nginx — Guide pratique
Reverse proxy, serveur de fichiers statiques, SSL/TLS et bonne marge de config — condensé de ce qui sert au quotidien.
Nginx utilise un modèle event-driven avec un processus maître et des workers. La config se lit globalement de haut en bas : un location qui correspond en premier gagne.
Structure du fichier de config
Un bloc serveur typique :
server {
listen 80;
server_name exemple.com www.exemple.com;
root /var/www/exemple.com/public;
index index.html index.htm;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
}Directives essentielles
| Directive | Rôle |
|---|---|
listen | Port et optionnellement adresse/IP à écouter |
server_name | Domaines associés à ce bloc (ou _ pour le default) |
root | Répertoire racine des fichiers servis |
index | Fichiers recherchés par défaut (liste d’essai) |
try_files | Recherche de fichiers, fallback optionnel |
server_name _; (souligné) crée un serveur default qui reçoit toutes les requêtes dont le Host ne correspond à aucun autre bloc. Utile pour renvoyer du 444 (connexion fermée) aux scripts automatisés qui scannent les IP.
Reverse proxy
Rediriger le trafic vers un backend (Node, Python, Go…) :
server {
listen 80;
server_name api.exemple.com;
location / {
proxy_pass http://127.0.0.1:3000;
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_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 90s;
proxy_connect_timeout 10s;
}
}Websockets
Ajouter les en-têtes Upgrade et Connection ci-dessus est indispensable pour que Nginx transmette le handshake WebSocket correctement.
Proxy avec chemin différent
location /api/ {
# Supprime "/api" avant de transmettre au backend
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://backend:8080;
}Oublier proxy_set_header Host $host; casse les URLs générées par le backend (liens, redirects) qui vont pointer vers l’IP interne au lieu du domaine public.
SSL / Let’s Encrypt
Installation de Certbot
# Avec le package Nginx installé
sudo apt install certbot python3-certbot-nginx
# Obtenir un certificat pour un domaine
sudo certbot --nginx -d exemple.com -d www.exemple.comCertbot modifie automatiquement le bloc Nginx existant : il ajoute un listen 443 ssl, les directives ssl_certificate, et un redirect HTTP → HTTPS.
Bloc SSL complet
server {
listen 443 ssl http2;
server_name exemple.com www.exemple.com;
ssl_certificate /etc/letsencrypt/live/exemple.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/exemple.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
ssl_prefer_server_ciphers on;
root /var/www/exemple.com/public;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
}
# Redirect HTTP → HTTPS
server {
listen 80;
server_name exemple.com www.exemple.com;
return 301 https://$host$request_uri;
}Renouvellement automatique
# Test à blanc (ne modifie rien)
sudo certbot renew --dry-run
# Recharger Nginx après renouvellement
sudo certbot renew --quiet --post-hook "systemctl reload nginx"Ajouter dans le crontab (sudo crontab -e) :
0 3 * * * certbot renew --quiet --post-hook "systemctl reload nginx"Let’s Encrypt émet des certificats de 90 jours. Le renouvellement se déclenche à partir de J-30. Tester chaque mois avec --dry-run pour s’assurer que le hook de reload fonctionne.
Locations courantes
Fichiers statiques avec cache
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2?)$ {
expires 30d;
add_header Cache-Control "public, no-transform";
access_log off;
gzip_static on;
}Blocs spécifiques par API/Path
location /health {
access_log off;
return 200 "ok\n";
add_header Content-Type text/plain;
}
location /admin {
allow 10.0.0.0/8;
deny all;
proxy_pass http://backend:8080;
}
location ~ /\. {
deny all;
access_log off;
log_not_found off;
}Serveur de fichiers statiques pur (site Vue/React)
server {
listen 80;
server_name app.exemple.com;
root /var/www/app/build;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2?)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
}Le pattern try_files $uri $uri/ /index.html; est incontournable pour les SPA : toute URL interne (par exemple /dashboard) renvoie index.html et le router client s’occupe du reste.
Bonnes pratiques
Headers de sécurité
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'" always;| Header | Protection |
|---|---|
X-Frame-Options | Empêche l’inclusion dans un <iframe> (clickjacking) |
X-Content-Type-Options | Empêche le MIME-sniffing du navigateur |
X-XSS-Protection | Active le filtre XSS du navigateur (legacy) |
Referrer-Policy | Contrôle l’envoi du referrer à des sites tiers |
Content-Security-Policy | White-liste les sources de scripts/styles |
L’attribut always est obligatoire sur add_header pour que les en-têtes soient envoyés même sur les réponses d’erreur (4xx, 5xx). Sans lui, les en-têtes personnalisés ne sont ajoutés qu’aux réponses 2xx/3xx — les en-têtes hérités des blocs parents sont écrasés dès qu’un add_header existe dans le bloc enfant.
Gzip
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_min_length 256;
gzip_types
text/plain
text/css
text/javascript
application/json
application/javascript
application/x-javascript
application/xml
image/svg+xml;| Directive | Note |
|---|---|
gzip_comp_level | 1-9, le niveau 6 est un bon compromis CPU/taille |
gzip_min_length | Ne gzippe pas les fichiers < cette taille (le gain est inférieur à l’overhead) |
gzip_vary | Ajoute Vary: Accept-Encoding — obligatoire si un proxy cache sert les réponses |
gzip_static on; sert directement les fichiers .gz pré-compressés si présents sur le disque. Idéalement, les builds CI produisent les assets déjà compressés. Évite le CPU au runtime.
Masquer la version
server_tokens off;Retire la version de Nginx des en-têtes Server: et des pages d’erreur par défaut. Un petit geste anti-reconnaissance.
Pièges fréquents
-
Oublier
try_filesdans un proxy : sanstry_files, Nginx renvoie du 404 sur les URLs SPA internes. Ajoutertry_files $uri $uri/ /index.html;avant lelocationqui fait le proxy. -
add_headerqui écrase les parents : une directiveadd_headerdans un bloclocationsupprime tous lesadd_headerdu bloc parent, pas seulement l’ajoute. Reproduire les headers au niveau souhaité. -
Port 80 déjà occupé : un deuxième bloc
serveraveclisten 80et le mêmeserver_nameprovoque l’erreurnginx: [emerg] "server" directive is not allowed here. Vérifier les doublons deserver_name. -
TLSv1.0/TLSv1.1 activés par défaut : sur les anciennes versions de Nginx (avant 1.19.x), les protocoles obsolètes sont autorisés. Forcer
ssl_protocols TLSv1.2 TLSv1.3;explicitement. -
Certbot qui échoue en
--nginx: si Nginx n’est pas dans le PATH de Certbot (certains environnements Docker minimalistes), utiliser--webroot -w /var/www/exemple.com -d exemple.comà la place. -
$urivs$request_uri:$uriest l’URI décodé et sans query-string ;$request_uricontient la requête brute telle que reçue. Utiliser$request_uridans un redirect pour préserver les paramètres. -
Recharger Nginx avec une config invalide :
nginx -tdoit passer avantsystemctl reload nginx. Un reload sur une config invalide ne redémarre pas — les anciens workers continuent de servir l’ancienne config.
Référence : Documentation officielle Nginx . Tester chaque modification avec nginx -t avant de recharger.