Skip to Content

Chapitre 7.3 — Servir du statique

⏱️ TL;DR — Avant de proxifier une app, apprends à Nginx ce qu’il fait de mieux : servir des fichiers. Un server block avec root (où sont les fichiers), index (le fichier par défaut), et le trio location + try_files $uri $uri/ =404 qui cherche le fichier demandé puis renvoie une 404 s’il n’existe pas. Pour une SPA (React, Vue), une variante clé : try_files $uri $uri/ /index.html; renvoie toujours index.html pour que le routeur JS prenne la main. On règle les types MIME, le cache des assets (expires), autoindex, et surtout les droits sur /var/www (l’utilisateur www-data doit pouvoir lire). Cas concret : servir un export statique Next ou Vite.

🎯 Objectifs

  • Écrire un server block qui sert un site statique de bout en bout.
  • Maîtriser try_files et connaître la variante SPA (fallback vers index.html).
  • Régler le cache navigateur des assets avec expires.
  • Comprendre les types MIME et l’option autoindex.
  • Poser les bons droits sur /var/www pour que Nginx puisse lire.
  • Déployer un export statique Next/Vite derrière Nginx.

Le server block statique de base

Servir un site statique, c’est dire à Nginx sont les fichiers et quoi faire quand une URL arrive. Voici un server block complet, commenté.

server { listen 80; server_name formacampus.fr; # ce site répond pour ce domaine root /var/www/formacampus; # les fichiers vivent ici index index.html; # fichier par défaut d'un dossier location / { try_files $uri $uri/ =404; # cherche le fichier, sinon 404 } }

Ce qui se passe quand une requête arrive sur /a-propos.html :

  • root /var/www/formacampus; fixe la racine : le chemin d’URL est cherché sous ce dossier. /a-propos.html correspond donc au fichier /var/www/formacampus/a-propos.html.
  • try_files $uri $uri/ =404; est le cœur. $uri est le chemin demandé. Nginx essaie dans l’ordre : d’abord le fichier $uri (existe-t-il ?), sinon le répertoire $uri/ (avec son index), et si rien ne correspond, il renvoie une 404. C’est la façon propre et sûre de servir des fichiers : on ne sert que ce qui existe réellement.

💡 Réflexe — Lis try_files $uri $uri/ =404; comme une liste de secours évaluée de gauche à droite : « essaie ce fichier ; sinon ce dossier ; sinon abandonne en 404 ». Le dernier argument est ce qui se passe quand rien n’a marché — soit un code (=404), soit une URL de repli (voir la variante SPA juste après).

La variante SPA : fallback vers index.html

Une Single Page Application (React, Vue, Svelte en mode client) n’a qu’un seul vrai fichier HTML : index.html. Les « pages » comme /dashboard ou /cours/42 n’existent pas sur le disque — c’est le routeur JavaScript qui les affiche côté navigateur. Problème : si l’utilisateur recharge /dashboard ou arrive dessus par un lien direct, Nginx cherche un fichier /dashboard qui n’existe pas… et renvoie 404 avec le try_files précédent.

La solution : dire à Nginx de retomber sur index.html quand le fichier n’existe pas, pour que l’app JS se charge et laisse son routeur gérer l’URL.

server { listen 80; server_name app.formacampus.fr; root /var/www/formacampus-spa; index index.html; location / { try_files $uri $uri/ /index.html; # fallback SPA : sinon, index.html } }

La seule différence est le dernier argument : /index.html au lieu de =404. Désormais, /dashboard cherche le fichier (absent), le dossier (absent), puis sert /index.html — l’app démarre, le routeur JS lit l’URL et affiche le tableau de bord. C’est LA ligne à connaître pour héberger une SPA correctement.

⚠️ Piège — Oublier ce fallback est l’erreur numéro un des SPA en prod. Symptôme classique : « ça marche quand je navigue depuis l’accueil, mais 404 quand je recharge une page interne ou que je colle une URL directe ». La cause n’est jamais un bug de ton app : c’est Nginx qui cherche un fichier qui n’existe pas. Le try_files ... /index.html; règle tout. Attention à ne pas mettre ce fallback sur un site classique multi-pages, où une vraie 404 est le bon comportement.

Le cache des assets avec expires

Les fichiers d’une app buildée (JS, CSS, images) portent souvent un hash dans leur nom (app.9f3c1a.js) : quand le contenu change, le nom change. On peut donc dire au navigateur de les garder très longtemps en cache — il ne les redemandera pas, et ton site est plus rapide. On ajoute un location qui cible les extensions d’assets et pose un expires long.

location / { try_files $uri $uri/ /index.html; } # cache long pour les assets versionnés location ~* \.(?:js|css|png|jpg|jpeg|gif|svg|webp|woff2?|ico)$ { expires 1y; # dit au navigateur de garder 1 an add_header Cache-Control "public"; # cache partageable try_files $uri =404; # un asset absent = 404, pas de fallback }

Détail des directives :

  • location ~* \.(...)$ — le ~* déclenche une correspondance par expression régulière insensible à la casse ; ici toutes les URL finissant par une extension d’asset.
  • expires 1y; — Nginx pose les en-têtes Cache-Control et Expires pour une durée d’un an. Le navigateur ne redemandera pas ces fichiers tant que leur URL ne change pas.
  • add_header Cache-Control "public"; — précise que le cache est partageable (utile aussi pour un CDN devant).
  • Le try_files $uri =404; ici ne retombe pas sur index.html : un asset manquant doit être une vraie 404 (sinon tu servirais du HTML là où on attend du JS, avec des erreurs déroutantes).

💡 Réflexe — Cache agressif pour les fichiers versionnés par hash (leur nom garantit qu’un nouveau contenu = nouvelle URL) ; cache court ou nul pour le HTML (index.html), qui doit rester frais pour pointer vers les nouveaux hashes après un déploiement. Ce couple — HTML jamais caché, assets cachés à fond — est le secret d’un site à la fois rapide et qui se met à jour instantanément.

Types MIME et autoindex

Nginx envoie pour chaque fichier un en-tête Content-Type (le type MIME) qui dit au navigateur comment l’interpréter : text/html, application/javascript, image/png… Il le déduit de l’extension grâce au fichier /etc/nginx/mime.types, inclus par défaut. Tu n’as rien à faire dans 99 % des cas — mais si un .js est servi en text/plain (le navigateur refuse alors de l’exécuter), c’est souvent que le bloc mime.types n’est pas inclus ou qu’une extension exotique manque.

autoindex génère automatiquement une page de listing du contenu d’un dossier (comme un explorateur de fichiers) quand il n’y a pas d’index. C’est désactivé par défaut — et il faut le laisser ainsi en prod sauf besoin explicite.

location /telechargements/ { autoindex on; # affiche la liste des fichiers du dossier (à utiliser avec prudence) }

🔒 Sécurité — N’active autoindex on; que sur un dossier volontairement public (une page de téléchargements assumée). Sur ta racine, il exposerait l’arborescence de ton site — fichiers de sauvegarde oubliés, exports, dumps — à quiconque devine le chemin. Par défaut il est off ; laisse-le off partout où tu n’en as pas explicitement besoin.

Les droits sur /var/www

Nginx tourne sous l’utilisateur système www-data (sur Debian/Ubuntu). Pour servir un fichier, ce compte doit pouvoir le lire — et traverser (x) tous les dossiers du chemin. Si les droits sont mauvais, tu récoltes un 403 Forbidden, et l’error.log te dira « Permission denied ».

# le contenu web appartient à www-data et reste lisible sudo chown -R www-data:www-data /var/www/formacampus sudo find /var/www/formacampus -type d -exec chmod 755 {} \; # dossiers traversables sudo find /var/www/formacampus -type f -exec chmod 644 {} \; # fichiers lisibles

Ce que ça fait : chown -R www-data:www-data donne la propriété à www-data ; 755 sur les dossiers les rend traversables et lisibles par tous ; 644 sur les fichiers les rend lisibles par tous, modifiables par le seul propriétaire. Un site statique n’a jamais besoin que Nginx puisse écrire : lecture seule suffit, et c’est plus sûr.

⚠️ Piège — Un 403 Forbidden sur un site statique est presque toujours un problème de droits ou de chemin, pas de config Nginx. Vérifie dans l’ordre : le fichier existe-t-il vraiment sous root ? www-data peut-il lire le fichier et traverser chaque dossier parent (un 700 sur /var/www ou sur ton home bloque tout le sous-arbre) ? L’error.log (/var/log/nginx/error.log) nomme le fichier exact et la raison — lis-le avant de trafiquer la config.

Servir un export statique Next ou Vite

Deux cas très fréquents dont tu tomberas amoureux, parce qu’ils n’ont même pas besoin de Node en prod :

  • Vite / Create React App : npm run build produit un dossier dist/ (Vite) ou build/ (CRA) de fichiers purement statiques. Tu les déposes dans /var/www/... et Nginx les sert. C’est une SPA → utilise le try_files ... /index.html;.
  • Next.js en export statique : si le projet le permet (output: 'export'), next build génère un dossier out/ de HTML/CSS/JS statiques, sans serveur Node. Tu le sers exactement comme un site statique classique.

Le déploiement se résume à copier le dossier de build vers le serveur (via scp/rsync, Partie 4) puis à recharger Nginx.

# depuis ta machine : build puis envoi du dossier statique npm run build rsync -avz --delete dist/ deploy@formacampus.fr:/var/www/formacampus/ # côté serveur : droits + reload ssh deploy@formacampus.fr 'sudo chown -R www-data:www-data /var/www/formacampus && sudo systemctl reload nginx'

🧭 Sur FormaCampus — Toutes les images de cours (vignettes, illustrations, PDF publics) sont servies en statique par Nginx depuis /var/www/formacampus/media/, avec un expires 1y; : elles ne changent jamais une fois publiées, autant les cacher à fond côté navigateur et CDN. Ça décharge complètement l’API Symfony de la livraison des médias — Symfony ne s’occupe que du dynamique, Nginx crache les fichiers à pleine vitesse. Le front, lui, reste une app Next dynamique qu’on proxifiera au chapitre suivant (ce n’est pas un export statique ici).

📚 La doc — Les directives root, index, try_files, expires et add_header ont chacune leur page sur nginx.org, avec leur contexte de validité. Pour les modes de output de Next (export vs serveur), c’est la doc Next.js qui fait autorité — vérifie que ton app est compatible export (pas d’API routes, pas de rendu serveur) avant de la traiter en statique.

✏️ Exercices

Exercice 1 — Statique ou SPA ? Tu déploies un site vitrine multi-pages classique (index.html, a-propos.html, contact.html), chaque page étant un vrai fichier. Quel try_files mets-tu dans location /, et pourquoi pas l’autre ?

✅ Solution

try_files $uri $uri/ =404;. Chaque page existe en tant que fichier réel : si l’URL ne correspond à aucun fichier, une vraie 404 est le bon comportement. Le fallback SPA try_files $uri $uri/ /index.html; serait une erreur ici : il servirait la page d’accueil pour n’importe quelle URL inexistante (/nimporte-quoi afficherait l’accueil au lieu d’une 404), masquant les liens cassés.

Exercice 2 — 403 au chargement. Tu déposes ton build dans /var/www/monsite, la config est bonne (nginx -t OK), mais le site renvoie 403 Forbidden. Quelles vérifications fais-tu, dans l’ordre ?

✅ Solution

  1. Lire /var/log/nginx/error.log — il nomme le fichier et la raison (« Permission denied »). 2. Vérifier que www-data peut lire les fichiers et traverser chaque dossier parent (un dossier en 700 quelque part dans le chemin bloque tout). 3. Corriger : sudo chown -R www-data:www-data /var/www/monsite, 755 sur les dossiers, 644 sur les fichiers. 4. Vérifier qu’un index existe bien (sinon 403 sur le dossier si autoindex est off) ou que le fichier demandé existe réellement sous root. Le 403 vient des droits/chemin, pas de la config.

🧠 Quiz de révision

1. Que fait try_files $uri $uri/ =404; ?

Il essaie dans l’ordre : le fichier $uri, puis le dossier $uri/ (avec son index), et si rien n’existe, renvoie une 404. C’est la façon sûre de ne servir que des fichiers réellement présents.

2. Quel try_files faut-il pour une SPA React, et pourquoi ?

try_files $uri $uri/ /index.html;. Les routes internes (/dashboard) n’existent pas comme fichiers : on retombe sur index.html pour que l’app JS se charge et que son routeur gère l’URL. Sans ça, recharger une page interne donne un 404.

3. Pourquoi cacher les assets (expires 1y) mais pas le HTML ?

Les assets sont versionnés par hash (nouveau contenu = nouvelle URL), donc on peut les garder très longtemps sans risque. Le HTML (index.html) doit rester frais pour pointer vers les nouveaux hashes après un déploiement — on le cache peu ou pas.

4. Sous quel utilisateur tourne Nginx, et quelle conséquence sur les droits ?

Sous www-data (Debian/Ubuntu). Ce compte doit pouvoir lire les fichiers et traverser (x) tous les dossiers du chemin, sinon c’est un 403. Un site statique n’a besoin que de droits de lecture (644 fichiers, 755 dossiers), jamais d’écriture.

5. Quand active-t-on autoindex on; et quel est le risque ?

Uniquement sur un dossier volontairement public (page de téléchargements). Ailleurs, il exposerait l’arborescence du site (sauvegardes, dumps, exports) à qui devine le chemin. Il est off par défaut : on le laisse off partout où on n’en a pas explicitement besoin.


Chapitre suivant : Proxy vers une app — le cœur du reverse proxy : proxy_pass, les en-têtes indispensables et les WebSockets.

Last updated on