📍 Article principal du parcours : Nginx : reverse proxy, HTTPS et configuration de A à Z
Cet article fait partie du parcours Nginx. Pour la vue d’ensemble, lisez d’abord le guide principal.
Colis Express est en ligne et chiffré, mais chaque visite retélécharge le même CSS, le même JavaScript, les mêmes réponses d’API — et tout ce trafic se paie, en bande passante côté serveur et en forfait data côté visiteur. Sur une connexion mobile à Abidjan ou à Lomé, une page allégée et mise en cache, c’est la différence entre un site qui s’affiche en une seconde et un site qu’on abandonne. Dans ce tutoriel, vous activez la compression, vous dites au navigateur de garder les fichiers statiques, et vous mettez en cache certaines réponses de l’API. À la fin, votre site transférera nettement moins d’octets pour le même contenu.
🎯 Ce que vous allez apprendre
- Activer et régler la compression gzip pour les contenus textuels.
- Poser des en-têtes de cache navigateur sur les fichiers statiques avec
expires. - Comprendre la différence entre cache navigateur et cache mandataire.
- Mettre en place un cache mandataire (
proxy_cache) devant l’API. - Vérifier les gains avec
curlet les en-têtes de réponse.
🛠️ Ce que vous allez construire
Vous aurez un site dont les réponses textuelles sont compressées, dont les fichiers statiques sont gardés en cache par le navigateur pendant des mois, et dont les réponses d’API mises en cache sont servies sans solliciter Node à chaque fois. Vous saurez prouver chaque gain avec une simple commande.
Prérequis
- Nginx servant un front statique et relayant une API (voir Mettre une API Node.js derrière un reverse proxy Nginx).
- Un site idéalement déjà en HTTPS (voir HTTPS avec Nginx et Let’s Encrypt).
- Niveau intermédiaire. Test express : si vous savez lire les en-têtes d’une réponse avec
curl -I, vous êtes prêt. - ⏱️ Temps estimé : ~30 minutes.
Étape 1 — Activer la compression gzip
La compression réduit le poids des fichiers texte (HTML, CSS, JavaScript, JSON) avant de les envoyer ; le navigateur les décompresse à la réception. Les gains sont spectaculaires sur le texte — souvent 70 à 80 % de réduction — pour un coût processeur dérisoire. Réglons-la globalement dans /etc/nginx/nginx.conf, dans le bloc http.
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 5;
gzip_types
text/plain
text/css
application/json
application/javascript
text/xml
application/xml
image/svg+xml;
Détaillons les choix qui comptent. gzip on; active la compression. gzip_vary on; ajoute l’en-tête Vary: Accept-Encoding, indispensable pour que les caches intermédiaires distinguent version compressée et version brute. gzip_min_length 1024; évite de compresser les tout petits fichiers, pour lesquels le gain ne couvre pas le surcoût. gzip_comp_level 5; est un bon compromis : monter à 9 ne gagne presque rien tout en consommant bien plus de CPU. Enfin, gzip_types liste les types à compresser — le HTML l’est toujours par défaut, inutile de le lister. On ne compresse pas les images JPEG/PNG ni les vidéos, déjà compressées. Un point pratique : sur Debian et Ubuntu, une ligne gzip on; est déjà présente par défaut dans nginx.conf — modifiez le bloc existant plutôt que d’en ajouter un second, faute de quoi nginx -t signalera une directive en double. Validez et rechargez : sudo nginx -t && sudo systemctl reload nginx.
✅ Point d’étape —
curl -H "Accept-Encoding: gzip" -I https://colis-express.net/renvoie un en-têteContent-Encoding: gzip. La compression est active.
Étape 2 — Mettre en cache les fichiers statiques côté navigateur
Un logo ou un fichier CSS ne change quasiment jamais entre deux visites : le retélécharger à chaque page est du gaspillage pur. L’en-tête Cache-Control dit au navigateur de garder ces fichiers localement pendant une longue durée. On cible ces ressources par leur extension dans un location dédié.
location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|woff2)$ {
add_header Cache-Control "public, max-age=2592000";
}
Le location ~* \.(...)$ utilise une expression régulière insensible à la casse pour attraper les fichiers par extension. L’en-tête Cache-Control: public, max-age=2592000 autorise la mise en cache publique pendant 2 592 000 secondes, soit 30 jours. On obtiendrait un résultat équivalent avec la directive expires 30d;, qui génère seule l’en-tête Cache-Control — mais il ne faut pas combiner les deux mécanismes, sous peine d’émettre un en-tête Cache-Control en double. Résultat : lors de la deuxième visite, le navigateur ressort ces fichiers de son cache local sans même contacter le serveur. Pour gérer les mises à jour malgré ce cache long, la technique usuelle est le cache busting : on intègre un hash dans le nom du fichier (app.4f3a.js), que le build régénère à chaque changement.
✅ Point d’étape —
curl -I https://colis-express.net/app.jsrenvoie un en-têteCache-Control: public, max-age=2592000. Le navigateur gardera ce fichier 30 jours.
Étape 3 — Comprendre cache navigateur et cache mandataire
Avant d’aller plus loin, distinguons deux caches très différents. Le cache navigateur (étape précédente) vit chez le visiteur : il évite de retélécharger ce qu’il a déjà. Le cache mandataire (proxy_cache) vit sur votre serveur, dans Nginx : il mémorise la réponse d’un backend pour la resservir aux visiteurs suivants sans rappeler l’application. Le premier soulage le réseau du visiteur ; le second soulage votre application et votre base de données.
Le cache mandataire ne convient qu’aux réponses identiques pour tous les utilisateurs : une liste de tarifs de livraison, des zones desservies, un catalogue public. Il ne faut jamais mettre en cache une réponse personnalisée (panier, profil, données d’un compte), sous peine de servir les données d’un client à un autre. C’est la règle d’or à garder en tête à l’étape suivante.
Étape 4 — Mettre en cache des réponses d’API
Imaginons un point d’API public et stable : /api/zones, qui renvoie la liste des quartiers desservis. Inutile de réveiller Node à chaque appel. Déclarons d’abord une zone de cache dans le bloc http de nginx.conf.
proxy_cache_path /var/cache/nginx/colis levels=1:2
keys_zone=colis_cache:10m max_size=200m inactive=60m;
Cette directive définit où stocker le cache sur le disque, une zone mémoire nommée colis_cache de 10 Mo pour les clés, une taille maximale de 200 Mo, et une éviction des entrées inutilisées depuis 60 minutes. Ensuite, dans le location de l’API publique concernée, on active ce cache :
location /api/zones {
proxy_pass http://127.0.0.1:3000/zones;
proxy_cache colis_cache;
proxy_cache_valid 200 10m;
add_header X-Cache-Status $upstream_cache_status;
}
proxy_cache colis_cache; branche la zone déclarée. proxy_cache_valid 200 10m; garde 10 minutes les réponses de code 200. La ligne add_header X-Cache-Status $upstream_cache_status; est un outil de diagnostic précieux : elle expose dans la réponse si Nginx a servi depuis le cache (HIT), interrogé le backend (MISS), ou contourné le cache (BYPASS). Rechargez après nginx -t.
✅ Point d’étape — Deux appels successifs à
/api/zones: le premier renvoieX-Cache-Status: MISS, le secondHIT. Le cache mandataire fonctionne.
Étape 5 — Mesurer les gains
Optimiser sans mesurer, c’est avancer à l’aveugle. Comparons le poids transféré avec et sans compression sur une même ressource.
# Taille brute
curl -s -o /dev/null -w "%{size_download} octets\n" https://colis-express.net/app.css
# Taille compressée
curl -s -H "Accept-Encoding: gzip" -o /dev/null \
-w "%{size_download} octets\n" https://colis-express.net/app.css
La seconde valeur doit être nettement plus basse que la première — c’est le poids réel que vos visiteurs téléchargeront. Pour une vue d’ensemble, l’onglet « Réseau » des outils de développement du navigateur affiche, colonne par colonne, la taille transférée, le statut du cache, et le temps de chargement. C’est là que se vérifie, concrètement, le travail de ce tutoriel.
Étape 6 — Aller plus loin : descripteurs de fichiers et micro-cache
Deux réglages moins connus complètent le tableau. Le premier, open_file_cache, mémorise les métadonnées des fichiers servis (leur existence, leur taille, leur date) pour éviter à Nginx d’interroger le disque à chaque requête. Sur un site qui sert beaucoup de fichiers statiques, le gain est mesurable. On le règle dans le bloc http.
open_file_cache max=1000 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
Nginx garde ici en mémoire les informations de jusqu’à 1000 fichiers, oublie ceux inutilisés depuis 20 secondes, et revalide les entrées toutes les 30 secondes. C’est transparent pour le visiteur, mais cela allège le travail du serveur sous charge.
Le second concept est le micro-cache : mettre en cache une réponse dynamique pour une durée très courte, une seconde par exemple. L’idée surprend, mais elle est puissante. Si la page d’accueil de Colis Express est interrogée 200 fois par seconde lors d’un pic, la mettre en cache une seule seconde signifie que le backend ne la calcule qu’une fois au lieu de 200 — une réduction de charge spectaculaire, pour une fraîcheur que personne ne perçoit. On réutilise la zone proxy_cache déjà déclarée, avec un proxy_cache_valid très bref.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_cache colis_cache;
proxy_cache_valid 200 1s;
add_header X-Cache-Status $upstream_cache_status;
}
Cette technique ne convient qu’aux pages identiques pour tous les visiteurs anonymes — la règle d’or de l’étape 3 reste valable. Mais sur les pages publiques très sollicitées, le micro-cache est l’un des leviers les plus efficaces pour tenir un pic de trafic sans renforcer le serveur.
✅ Point d’étape — Sous une rafale de requêtes sur la page d’accueil,
X-Cache-Statusaffiche majoritairementHIT: le backend n’est sollicité qu’une fois par seconde.
🐞 Pièges fréquents
| Symptôme / erreur | Cause probable | Correctif |
|---|---|---|
Pas de Content-Encoding: gzip |
Type MIME absent de gzip_types |
Ajouter le type ; vérifier que le client envoie Accept-Encoding |
| Les mises à jour de CSS/JS ne s’affichent plus | Cache navigateur trop long sans cache busting | Intégrer un hash dans le nom de fichier au build |
| Un utilisateur voit les données d’un autre | Réponse personnalisée mise en cache mandataire | Ne mettre en cache que les réponses publiques et identiques pour tous |
X-Cache-Status toujours MISS |
Le backend envoie un Cache-Control: no-cache |
Ajuster côté API, ou forcer avec proxy_ignore_headers |
| Erreur de permission sur le dossier de cache | /var/cache/nginx non accessible à www-data |
Créer le dossier et ajuster le propriétaire |
🌍 Adaptation au contexte ouest-africain
C’est sans doute le tutoriel le plus directement utile pour vos visiteurs. Sur un forfait data compté au mégaoctet, la compression gzip divise par trois ou quatre le poids des pages, ce qui se traduit en francs CFA économisés à chaque visite et en pages qui s’affichent même quand le réseau faiblit. Le cache navigateur, lui, fait qu’un visiteur régulier ne retélécharge presque rien : seul le contenu vraiment nouveau passe sur le réseau. Sur des liaisons à latence élevée, c’est souvent ce qui transforme un site « lent » en site « réactif ». Investir dans ces réglages rapporte davantage, en perception, qu’un serveur plus puissant.
✅ Récapitulatif
Vous avez activé la compression gzip, posé des en-têtes de cache navigateur sur les ressources statiques, compris la différence entre cache navigateur et cache mandataire, et mis en cache des réponses d’API publiques avec un en-tête de diagnostic. Surtout, vous savez mesurer chaque gain. Votre site est désormais rapide ; reste à le rendre capable d’encaisser la charge quand le trafic grimpe — c’est l’objet du tutoriel suivant.
🧾 Aide-mémoire
| Directive | Rôle |
|---|---|
gzip on; + gzip_types |
Compresser les contenus textuels |
expires 30d; |
Durée de cache navigateur des statiques |
proxy_cache_path ... |
Déclarer une zone de cache mandataire |
proxy_cache + proxy_cache_valid |
Mettre en cache des réponses de backend |
X-Cache-Status $upstream_cache_status |
Diagnostiquer HIT / MISS / BYPASS |
💪 À vous de jouer
Faites en sorte qu’une réponse d’API mise en cache ne soit jamais servie à un utilisateur connecté : si la requête porte un cookie de session, le cache doit être contourné. Indice : la directive proxy_cache_bypass et la variable $cookie_session.
Voir une solution
location /api/zones {
proxy_pass http://127.0.0.1:3000/zones;
proxy_cache colis_cache;
proxy_cache_valid 200 10m;
proxy_cache_bypass $cookie_session;
proxy_no_cache $cookie_session;
add_header X-Cache-Status $upstream_cache_status;
}
proxy_cache_bypass contourne le cache (va chercher au backend) quand le cookie est présent, et proxy_no_cache empêche d’enregistrer cette réponse. Ainsi, un visiteur anonyme profite du cache, un utilisateur connecté reçoit toujours une réponse fraîche.
Tutoriels frères
- Load balancing avec Nginx : répartir la charge — encaisser plus de trafic avec plusieurs backends.
- Mettre une API Node.js derrière un reverse proxy Nginx — la base du relais, en amont du cache.
Pour aller plus loin
- 🔝 Retour au guide principal : Nginx : reverse proxy, HTTPS et configuration de A à Z
- Documentation officielle : Module gzip et guide du cache de contenu.
- Étape suivante du parcours : répartir la charge entre plusieurs instances.
FAQ
Faut-il préférer Brotli à gzip ?
Brotli compresse un peu mieux que gzip sur le texte, mais il n’est pas inclus dans les paquets Nginx officiels : il faut ajouter un module (souvent compilé dynamiquement). Pour débuter, gzip offre l’essentiel du gain sans cette complexité. Brotli est une optimisation de second tour.
Le cache mandataire remplace-t-il un cache applicatif type Redis ?
Non, ils sont complémentaires. Le cache Nginx mémorise des réponses HTTP entières et publiques ; un cache applicatif comme Redis stocke des données fines et personnalisables côté application. Beaucoup d’architectures utilisent les deux.
Mon site est dynamique : le cache a-t-il un intérêt ?
Oui, sur la partie statique (CSS, JS, polices, images) et sur les réponses publiques stables. Même un site très dynamique sert une majorité d’octets statiques ; la compression et le cache navigateur s’appliquent toujours.