Servir des fichiers avec Nginx, c'est faire correspondre une URL à un fichier du disque, lui donner le bon type MIME, et refuser tout le reste. Cette leçon s'adresse à une personne qui sait choisir le server et la location d'une requête et qui veut publier un site statique ou une application monopage (SPA, une application dont le navigateur exécute le JavaScript et dont toutes les routes sont servies par un seul index.html). Elle couvre root et alias (les deux directives qui rattachent une URL à un répertoire), try_files (la liste de candidats que Nginx essaie pour répondre), les types MIME (l'étiquette qui dit au navigateur la nature d'un fichier), le fallback d'une SPA sans avaler les erreurs, le cache du navigateur, les fichiers sensibles et les permissions de lecture.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »La leçon suit le chemin d'une URL jusqu'au fichier, puis ce qu'il faut interdire.
- Choisir entre
rootetaliaset prédire le fichier lu. - Écrire un
try_filesqui sert un fichier, un répertoire ou une erreur. - Servir chaque ressource avec le bon type MIME et la bonne durée de cache.
- Distinguer le fallback légitime d'une SPA d'une vraie absence de ressource.
- Refuser les fichiers cachés et privés sans bloquer le défi ACME.
- Donner aux workers (les processus de Nginx qui lisent les fichiers et répondent aux clients) la lecture sans ouvrir les fichiers à tous.
Prérequis
Section intitulée « Prérequis »La leçon suppose que vous savez prédire quel location Nginx choisit, et que vous comprenez les droits Unix de base.
- Les
locationexactes, préfixes et regex, et leur ordre de choix : Nginx : choisir le bon server et la bonne location. - Propriétaire, groupe et bits de lecture, d'écriture et d'exécution : Permissions Linux.
- Le défi HTTP-01 des certificats automatiques : Certificats automatiques.
Comment Nginx trouve-t-il le fichier d'une URL : root ou alias ?
Section intitulée « Comment Nginx trouve-t-il le fichier d'une URL : root ou alias ? »root ajoute l'URL entière à un répertoire, alors qu'alias remplace le préfixe de la location par un répertoire. Les confondre est une cause courante d'erreur 404 sur un site statique : dans les deux cas Nginx lit un fichier, mais il ne le cherche pas au même endroit.
server { listen 80; server_name vitrine.lab.test;
# root : /assets/app.js -> /srv/vitrine/assets/app.js location /assets/ { root /srv/vitrine; }
# alias : /telechargements/guide.pdf -> /srv/documents/guide.pdf location /telechargements/ { alias /srv/documents/; }}Avec root, le chemin du fichier est la concaténation du répertoire et de l'URL complète : le préfixe /assets/ se retrouve dans le chemin. Avec alias, le préfixe disparaît : /telechargements/ est remplacé par /srv/documents/. Quand l'URL publique n'a pas la même forme que le dossier, on utilise alias ; sinon root suffit, et on le pose de préférence une fois au niveau du server pour qu'il vaille partout.
Pour alias, une règle de cohérence évite un défaut sérieux : la location et l'alias doivent finir tous deux par une barre oblique, ou aucun des deux. C'est une bonne pratique bien connue, pas une règle énoncée par la documentation de nginx.org, qui se borne à dire que alias remplace le préfixe. Une location /img (sans barre finale) associée à alias /srv/images/; (avec barre) fait correspondre l'URL /img../secret : Nginx remplace /img par /srv/images/ et lit /srv/images/../secret, donc un fichier hors du dossier. C'est le défaut classique de traversée de chemin appelé off-by-slash. Posez la barre des deux côtés, par exemple location /img/ avec alias /srv/images/;.
Que font index et try_files ?
Section intitulée « Que font index et try_files ? »index désigne le fichier servi quand l'URL est un répertoire, et try_files essaie plusieurs candidats dans l'ordre jusqu'au premier qui existe. Les deux sont des directives de fichiers, pas de routage : elles s'exécutent une fois la location retenue.
server { listen 80; server_name vitrine.lab.test; root /srv/vitrine; index index.html;
location / { try_files $uri $uri/ =404; }}Lisez try_files de gauche à droite. $uri teste le fichier de même chemin que l'URL. $uri/ teste un répertoire, et Nginx y sert alors le fichier d'index. Le dernier argument n'est pas testé, il est la réponse de secours : soit un code comme =404, soit une URL vers laquelle Nginx fait une redirection interne, comme /index.html. Un répertoire demandé sans barre finale reçoit d'abord une redirection 301 vers la même adresse avec la barre.
Ce try_files $uri $uri/ =404 est la forme saine d'un site statique : un fichier absent produit une vraie 404. Gardez cette forme en tête, car la section sur les applications monopages explique pourquoi on ne la remplace pas à la légère.
Pourquoi le type MIME compte-t-il ?
Section intitulée « Pourquoi le type MIME compte-t-il ? »Nginx déduit le type de contenu (Content-Type) de l'extension du fichier, grâce à une table : sans elle, tout sort en application/octet-stream et le navigateur télécharge au lieu d'exécuter. Un fichier app.js servi sans type JavaScript est refusé par les navigateurs dans plusieurs cas (script de type module, en-tête X-Content-Type-Options: nosniff) et téléchargé au lieu d'être affiché quand on ouvre son adresse : l'application reste blanche, sans erreur côté serveur.
http { include /etc/nginx/mime.types; default_type application/octet-stream;}La directive include mime.types charge la table qui associe .html, .css, .js, .svg et des dizaines d'autres extensions à leur type. Le paquet Nginx de l'éditeur (Debian 13, Nginx 1.31.8) la charge dans nginx.conf ; une configuration minimale écrite à la main l'oublie souvent. default_type s'applique à tout ce que la table ne reconnaît pas, et vaut application/octet-stream par défaut. Pour le JavaScript, le type du fichier mime.types varie selon la version et la distribution : application/javascript figure dans le conf/mime.types du dépôt amont de Nginx, alors que la RFC 9239 enregistre text/javascript comme type à utiliser et déclare l'autre obsolète. Un navigateur accepte les deux ; lisez le type réellement envoyé plutôt que de le supposer.
Vérifiez le type réellement envoyé, et pas seulement le code de statut, avec curl :
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' -H 'Host: vitrine.lab.test' http://127.0.0.1/assets/app.jsLa commande affiche le code puis le type. Un 200 accompagné de application/octet-stream est un défaut : la ressource est trouvée mais inutilisable.
Comment servir une application monopage sans cacher les erreurs ?
Section intitulée « Comment servir une application monopage sans cacher les erreurs ? »Une SPA a besoin que toute route inconnue du serveur (/accueil, /compte/12) retombe sur index.html, car c'est le JavaScript du navigateur qui l'interprète ; mais ce fallback ne doit s'appliquer qu'aux routes de l'application, jamais aux ressources ni à l'API. Un fallback posé sur toute la racine transforme chaque erreur en succès : une ressource absente répond 200 avec du HTML, et un appel d'API inconnu aussi.
# ❌ fallback trop général : /assets/absent.js et /api/inconnu répondent 200 avec du HTMLlocation / { try_files $uri $uri/ /index.html;}Le défaut est invisible à l'œil : la page s'affiche, tout semble fonctionner. Il se découvre des semaines après la mise en ligne, quand un fetch reçoit du HTML à la place de son JSON, ou quand un fichier manquant n'est signalé par aucune erreur dans les journaux ni dans la supervision. Les 404 disparaissent des mesures, alors qu'elles étaient le signal utile.
La parade est de limiter le fallback à son périmètre en s'appuyant sur le choix de location : une location plus spécifique prend la main pour les ressources et l'API, et laisse le fallback au reste.
Suivez le schéma de haut en bas : chaque question écarte un cas avant que le fallback n'intervienne. La configuration correspondante est la suivante, avec des noms de dossiers d'exemple :
server { listen 80; server_name vitrine.lab.test; root /srv/vitrine; index index.html;
location /assets/ { try_files $uri =404; }
location /api/ { proxy_pass http://127.0.0.1:9000; }
location / { try_files $uri $uri/ /index.html; }}Trois location se partagent le travail : /assets/ sert un fichier ou une 404 franche, /api/ envoie la requête à l'application via proxy_pass et le backend répond lui-même, et seule la racine garde le fallback. Le détail de proxy_pass (chemin, en-têtes, serveurs multiples) est le sujet de Publier une application avec Nginx. Pour qu'un appel d'API inconnu reçoive une 404 en JSON plutôt que du HTML, il suffit que ce soit le backend qui le renvoie : Nginx transmet sa réponse telle quelle.
Comment refuser les fichiers cachés et les fichiers privés ?
Section intitulée « Comment refuser les fichiers cachés et les fichiers privés ? »Un répertoire web ne doit contenir que ce qui peut être publié, et ce qui s'y trouve malgré tout se refuse explicitement. Le premier réflexe est de ne pas y mettre les secrets. Le second est de les refuser, y compris les fichiers cachés (dotfiles, dont le nom commence par un point), parce qu'un .env, un répertoire .git ou une sauvegarde .bak finissent par atterrir sous la racine web.
# Fichiers cachés (dotfiles) : refusés, sauf le défi ACMElocation ^~ /.well-known/acme-challenge/ { root /var/www/acme;}
location ~ /\. { return 404;}
# Un fichier privé précislocation = /deploiement.interne { return 404;}La règle location ~ /\. est une regex qui capte tout chemin contenant /. : /.env, /.git/config, /.htpasswd. Trop large, elle bloque aussi /.well-known/acme-challenge/, le chemin où l'autorité de certification vient lire le jeton du défi HTTP-01 : le renouvellement du certificat échouerait en silence. C'est pourquoi la location du défi est un préfixe prioritaire ^~, qui l'emporte sur la regex, comme vu dans la leçon sur la sélection. Le refus par return 404 donne la même réponse qu'un fichier absent ; deny all; répondrait 403 Forbidden, qui révèle qu'une règle protège ce chemin.
Les traversées de chemin (/../../etc/passwd, /..%2f..%2fetc%2fpasswd) sont déjà neutralisées pour un root bien posé, parce que Nginx décode et normalise l'URL avant de la comparer : un .. ne remonte pas au-dessus de la racine. Le danger vient des cas où la normalisation ne voit rien à corriger, comme l'alias dont la barre finale n'est pas symétrique de celle de sa location, vu plus haut : img.. est pour elle un nom de segment ordinaire, et le .. n'apparaît qu'après le remplacement du préfixe. L'autre cas est celui des liens symboliques (des entrées du disque qui renvoient vers un autre chemin) pointant hors de la racine. La directive disable_symlinks vaut off par défaut : Nginx suit alors les liens sans les contrôler. Avec disable_symlinks on;, l'accès est refusé dès qu'un composant du chemin est un lien, racine comprise ; disable_symlinks on from=$document_root; ne contrôle que les composants situés sous la racine. Elle ajoute un coût de traitement, et le module autoindex l'ignore.
Pour tester un refus, cherchez le secret dans le corps, pas seulement le code de statut : un 200 qui rend index.html sur /.env n'est pas une fuite, un 200 qui rend la clé en est une.
curl -s -H 'Host: vitrine.lab.test' http://127.0.0.1/.env | grep -c 'CLE_API' || trueLa commande doit afficher 0 : aucune ligne du corps ne contient la clé. Répétez le contrôle sur chaque fichier privé et sur les URL de traversée, en ajoutant --path-as-is à curl : sans lui, curl simplifie lui-même les .. avant d'envoyer la requête, et le serveur ne voit jamais l'attaque.
Comment régler le cache du navigateur ?
Section intitulée « Comment régler le cache du navigateur ? »Les ressources dont le nom change à chaque version (app.4f2a1c.js) se mettent en cache longtemps ; la page d'entrée index.html, dont le nom ne change pas, ne se met pas en cache sans revalidation. Nginx envoie déjà Last-Modified et ETag pour un fichier, ce qui permet au navigateur de redemander seulement si le fichier a changé (réponse 304). Les directives de cache ajoutent la durée.
location /assets/ { try_files $uri =404; expires 1y; add_header Cache-Control "public, immutable";}
location = /index.html { add_header Cache-Control "no-cache";}La directive expires 1y fixe Cache-Control: max-age à un an ; immutable dit au navigateur que le fichier ne changera jamais sous ce nom. Cette durée n'est sûre que si le nom du fichier contient une empreinte de son contenu : sans cela, un correctif déployé n'atteindrait pas les visiteurs. no-cache n'interdit pas le cache : il oblige à revalider avant de réutiliser. Attention enfin à un piège : un add_header écrit dans une location remplace ceux du niveau supérieur au lieu de s'y ajouter.
Quels droits donner aux fichiers servis ?
Section intitulée « Quels droits donner aux fichiers servis ? »Les workers de Nginx tournent sous un compte non privilégié (nginx avec le paquet de l'éditeur, créé à l'installation, comme l'indique user nginx; en tête de nginx.conf) : il leur faut la lecture sur chaque fichier servi et le droit de traverser chaque répertoire du chemin (le droit x d'un répertoire, qui permet de le franchir pour atteindre son contenu), mais personne d'autre n'a à pouvoir lire le site. La bonne pratique est donc de donner la lecture par le groupe, et de retirer les droits « autres ».
Un fichier est lisible par un worker si l'une de ces conditions est vraie : il appartient à nginx et le propriétaire a la lecture, ou son groupe est nginx et le groupe a la lecture, ou « autres » a la lecture. La troisième est celle à éviter : elle ouvre le fichier à tout compte de la machine, y compris un service compromis qui n'a rien à voir avec le site. Ouvrir tout en 0777 règle toujours le problème de lecture, et crée un problème de sécurité bien plus grave.
sudo chown -R deploy:nginx /srv/vitrinesudo find /srv/vitrine -type d -exec chmod 750 {} +sudo find /srv/vitrine -type f -exec chmod 640 {} +Ici deploy est le compte qui publie, et nginx est le groupe des workers. Les répertoires reçoivent 750 (le droit x pour traverser, nécessaire au groupe) et les fichiers 640 (lecture pour le groupe, rien pour les autres). Pour contrôler, listez ce qui reste lisible par tous, et prouvez que les workers lisent bien :
sudo find /srv/vitrine -type f -perm -o=r -printf '%M %p\n'sudo -u nginx test -r /srv/vitrine/assets/app.js && echo "lisible par nginx"namei -l /srv/vitrine/assets/app.jsLe premier find doit ne rien afficher. Le deuxième doit afficher lisible par nginx : un durcissement qui retire aussi la lecture aux workers est une panne, pas une protection. namei -l détaille les droits de chaque répertoire du chemin, ce qui révèle le dossier parent qui bloque. Un 403 accompagné dans le journal d'erreurs de open() ... failed (13: Permission denied) désigne ce cas ; un 404 avec (2: No such file or directory) signale plutôt un root ou un alias mal calculé.
Mettre en pratique
Section intitulée « Mettre en pratique »Le lab publie une application monopage, ses ressources, un fichier privé et une API par une configuration qui sert tout sans distinction : aucun type MIME correct, un fallback qui avale les erreurs, des fichiers privés accessibles et des permissions ouvertes à tous. Vous publiez ce qui est prévu avec son type, vous conservez les vraies erreurs, vous refusez le privé et vous resserrez les droits sans cesser de servir. La validation mesure le corps et le type MIME de chaque ressource, le fallback sur les routes de l'application et lui seul, le refus des fichiers privés et des traversées en cherchant le secret dans le corps, et l'absence de tout fichier lisible par tous, ressources toujours servies.
À retenir
Section intitulée « À retenir »rootajoute l'URL entière au répertoire,aliasremplace le préfixe de lalocation; unaliasgarde la même barre finale que salocation(bonne pratique), sinonlocation /imgavecalias /srv/images/;ouvre la traversée/img../.try_filesessaie ses candidats dans l'ordre, et son dernier argument est la réponse de secours :=404ou une URL interne.include mime.typesdonne à chaque ressource son type : sans lui, tout sort enapplication/octet-streamet un script risque de ne plus être exécuté ; vérifiez le type envoyé aveccurl,text/javascriptétant le type recommandé par la RFC 9239.- Un fallback sur
index.htmlposé sur toute la racine transforme les erreurs en succès : limitez-le aux routes de l'application, avec deslocationpropres aux ressources et à l'API. - Un dotfile se refuse par regex, mais la règle ne doit pas bloquer
/.well-known/acme-challenge/: un préfixe^~l'en protège. - Un refus se teste en cherchant le secret dans le corps, pas seulement le code de statut.
- Les workers lisent par le groupe (
640et750), jamais par un droit ouvert à tous :find -perm -o=rdoit être vide et les ressources toujours servies. - Cache :
immutablepour les fichiers à empreinte,no-cachepourindex.html.
Contrôle de connaissances
Section intitulée « Contrôle de connaissances »Vérifiez que l'essentiel de ce guide est acquis. Les questions portent uniquement sur ce qui vient d'être expliqué ici.
Contrôle de connaissances
Validez vos connaissances avec ce quiz interactif
Informations
- Le chronomètre démarre au clic sur Démarrer
- Questions à choix multiples, vrai/faux et réponses courtes
- Vous pouvez naviguer entre les questions
- Les résultats détaillés sont affichés à la fin
Lance le quiz et démarre le chronomètre
Vérification
(0/0)Score par domaine
Un domaine regroupe plusieurs guides et reçoit plusieurs questions : son score est plus fiable qu'un score par thème, qui porte souvent sur une seule question.
Bilan
Points forts
Aucun sujet réussi à 80 % sur au moins deux questions pour l’instant.
Points faibles
Aucun sujet sous le seuil de réussite.
À faire ensuite
Des indices pour retenter votre chance ?
Continuer le cours : Publier une application avec Nginx
Nouveau quiz complet avec des questions aléatoires
Retravailler uniquement les questions ratées
Retour à la liste des certifications