Déployer une application Laravel derrière Nginx ne consiste pas seulement à faire répondre le serveur sur le port 80. Il faut surtout que chaque requête arrive au bon endroit, que les fichiers sensibles restent hors de portée, et que l’application soit prête pour une vraie mise en production. Je pars toujours d’une base simple: Nginx en façade, PHP-FPM pour exécuter PHP, puis quelques réglages Laravel qui évitent les erreurs de routage, les lenteurs inutiles et les incidents de déploiement.
Les points qui changent vraiment le résultat en production
- Le serveur web doit pointer vers `public/`, jamais vers la racine du projet.
- `try_files` et `public/index.php` sont les deux pièces qui font fonctionner le routage Laravel.
- `storage` et `bootstrap/cache` doivent être accessibles en écriture par le compte du service web.
- Les caches Laravel (`config`, `routes`, `views`, `events`) apportent un vrai gain si on les régénère au déploiement.
- `APP_DEBUG` doit rester à `false` en production, sinon on expose trop d’informations.
- Le point de santé `/up` sert bien au monitoring, aux load balancers et à Kubernetes.
Comprendre la logique d’une pile Nginx et Laravel
Je vois trop souvent des déploiements qui traitent Nginx comme un simple “serveur de fichiers”. En réalité, son rôle est plus précis: il reçoit la requête, sert les ressources statiques quand c’est possible, puis transmet le reste à PHP-FPM pour que Laravel prenne la main. C’est cette séparation qui rend la pile fiable, rapide et facile à maintenir.
Le point qui compte le plus est le front controller. Laravel attend que les requêtes dynamiques passent par `public/index.php`, pas par la racine du projet. C’est le bon compromis entre sécurité et simplicité: Nginx reste à l’entrée, Laravel gère la logique métier, et aucun fichier de configuration sensible ne se retrouve exposé par erreur.
La documentation Laravel insiste d’ailleurs sur ce point, et je suis d’accord avec cette approche: on construit la configuration autour du dossier `public/`, puis on laisse Nginx faire le tri entre fichiers réels, routes applicatives et code PHP. Une fois ce modèle bien compris, le reste devient beaucoup plus mécanique.
La suite logique, c’est de poser un bloc Nginx propre, lisible et adapté à la production.

Le bloc Nginx que j’utilise comme base fiable
Je pars d’un `server` minimal, puis j’ajuste le nom de domaine, le socket PHP et les chemins du projet selon l’environnement. L’idée n’est pas de copier un fichier “magique”, mais d’avoir une base cohérente que je peux faire évoluer sans casser le routage.
server {
listen 80;
listen [::]:80;
server_name exemple.fr www.exemple.fr;
root /var/www/mon-app/public;
index index.php;
charset utf-8;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location = /favicon.ico {
access_log off;
log_not_found off;
}
location = /robots.txt {
access_log off;
log_not_found off;
}
error_page 404 /index.php;
location ~ ^/index\.php(/|$) {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
fastcgi_hide_header X-Powered-By;
}
location ~ /\.(?!well-known).* {
deny all;
}
}La documentation NGINX rappelle qu’un `server` définit un serveur virtuel, et que les `location` déterminent comment chaque URI est traitée. Dans ce contexte, trois directives font l’essentiel du travail: `root`, `try_files` et `fastcgi_pass`. Le reste sert surtout à sécuriser et à rendre le comportement plus prévisible.
| Directive | Rôle | Ce que je vérifie |
|---|---|---|
root /var/www/mon-app/public |
Expose uniquement le dossier public | Le projet n’est jamais servi depuis sa racine |
try_files $uri $uri/ /index.php?$query_string |
Fait passer les requêtes non statiques vers Laravel | Les routes profondes fonctionnent sans 404 inutile |
fastcgi_pass unix:/run/php/php8.3-fpm.sock |
Relie Nginx à PHP-FPM | Le socket ou le port correspond bien à la version PHP installée |
deny all sur les fichiers cachés |
Bloque l’accès aux dotfiles |
.env et les fichiers système restent inaccessibles |
X-Frame-Options et X-Content-Type-Options
|
Ajoutent une base de durcissement | Le comportement côté navigateur est moins exposé aux abus classiques |
Je laisse volontairement le TLS hors de cet extrait pour garder le noyau lisible. En pratique, vous ajoutez ensuite un bloc HTTPS ou une redirection propre de 80 vers 443, mais le cœur de la configuration ne change pas. Ce qui m’amène au point souvent négligé: les permissions et l’environnement du projet.
Les permissions et l’environnement doivent rester sobres
Laravel a besoin d’écrire dans `bootstrap/cache` et `storage`. Ce n’est pas un détail administratif, c’est une condition de fonctionnement. Les logs, les fichiers temporaires, les caches internes et certaines compilations passent par là, donc le compte du service web doit avoir les droits nécessaires, sans transformer le serveur en zone ouverte.
Je préfère toujours éviter les solutions brutales du type `chmod 777`. Elles donnent l’impression que tout marche, puis elles créent des problèmes de sécurité et de maintenance. Il vaut mieux identifier le vrai utilisateur du service Nginx ou PHP-FPM, puis lui accorder les droits strictement nécessaires sur les répertoires utiles.
- `storage` pour les logs, les sessions, les fichiers temporaires et certains caches.
- `bootstrap/cache` pour les caches de configuration et d’autoload.
- `.env` hors du web root, avec des secrets gérés côté environnement et non exposés par Nginx.
Autre point que je contrôle systématiquement: `APP_DEBUG` doit rester à `false` en production. Après un `config:cache`, Laravel ne recharge plus le `.env` pendant les requêtes, donc il faut que les variables sensibles soient lues proprement dans les fichiers de configuration, pas disséminées partout dans le code. Une fois cette discipline en place, l’optimisation du déploiement devient beaucoup plus fiable.
Les caches Laravel que j’active au déploiement
Dans les petites applications, on sous-estime souvent le coût des allers-retours vers le système de fichiers. Sur un projet avec beaucoup de routes, de vues Blade et de paramètres de configuration, le cache fait une vraie différence. Je ne l’utilise pas comme un réflexe automatique, mais comme une étape de déploiement assumée.
| Commande | Effet concret | Quand l’utiliser |
|---|---|---|
php artisan optimize |
Regroupe les caches de configuration, événements, routes et vues | Au moment du déploiement, sur une application prête pour la production |
php artisan config:cache |
Réduit les lectures répétées des fichiers de configuration | Dès que la configuration est stable |
php artisan event:cache |
Mémorise les correspondances événement → listener | Quand les événements auto-découverts ne changent pas en continu |
php artisan route:cache |
Accélère l’enregistrement des routes | Surtout sur les applications avec des centaines de routes |
php artisan view:cache |
Précompile les vues Blade | Pour éviter la compilation à la première requête utile |
php artisan optimize:clear |
Supprime les caches générés | Avant un diagnostic ou une correction de déploiement |
Le point de vigilance le plus important concerne `config:cache`: une fois la configuration cacheée, les appels à `env()` doivent rester confinés aux fichiers de configuration. Pour `route:cache`, je le fais seulement au déploiement, puis je regénère le cache à chaque modification de route. C’est une habitude simple, mais elle évite des incohérences très pénibles à déboguer.
Quand ces caches sont intégrés au pipeline, le serveur répond plus vite et les écarts entre environnement de test et production diminuent nettement. Il reste alors à surveiller ce qui tourne après la mise en ligne, car un déploiement réussi ne suffit pas à lui seul à garantir la stabilité.
Surveiller ce qui tourne après le déploiement
Je considère le déploiement comme incomplet tant que les services longue durée n’ont pas été pris en compte. Si vous exécutez des queue workers, Laravel Reverb ou Laravel Octane, ils doivent être redémarrés ou rechargés après une nouvelle version de l’application. Sinon, ils continuent parfois à exécuter de vieux fichiers en mémoire, et les bugs ressemblent alors à des fantômes.
Laravel fournit un point de santé natif, généralement exposé sur `/up`. Il renvoie une réponse HTTP 200 si l’application a démarré correctement, et 500 si elle n’a pas booté sans exception. C’est exactement le type de signal que j’aime brancher à un uptime monitor, à un load balancer ou à un système d’orchestration comme Kubernetes.
- Nginx pour les erreurs de proxy, de routage et d’accès aux fichiers.
- Les logs Laravel pour les exceptions applicatives et les problèmes métier.
- Le redémarrage des workers après chaque déploiement de code.
- Le point `/up` pour savoir si l’application a réellement démarré.
Je garde aussi un œil sur le moniteur de processus quand je ne suis pas sur une plateforme gérée. C’est là que beaucoup de déploiements “fonctionnent” en apparence mais cassent à la première montée de charge, parce qu’aucun service n’est réellement supervisé. Cette logique de surveillance me conduit naturellement à la question du choix d’architecture.
Choisir la pile qui vous évite le plus d’entretien
En 2026, je continue de voir Nginx + PHP-FPM comme le choix le plus robuste pour la majorité des équipes. C’est la pile la plus prévisible, la plus documentée et celle qui s’intègre le plus facilement dans des environnements variés, du VPS classique à l’infrastructure plus structurée. Mais ce n’est pas la seule option raisonnable.
| Option | Quand elle a du sens | Compromis principal |
|---|---|---|
| Nginx + PHP-FPM | Besoin de contrôle, de stabilité et d’un modèle très connu | Plus de pièces à configurer et à superviser |
| Nginx + FrankenPHP | Envie de simplifier la pile et d’explorer un serveur PHP moderne | Moins standard dans certaines équipes et certains déploiements |
| Plateforme gérée | Priorité à la vitesse de mise en ligne et à la réduction de la charge ops | Moins de liberté fine sur chaque réglage serveur |
Je conseille rarement de changer de pile juste par curiosité. Si votre équipe connaît déjà PHP-FPM, que vos déploiements sont stables et que vos besoins sont classiques, rester sur Nginx est souvent le meilleur choix. Si, en revanche, vous cherchez à réduire la complexité opérationnelle ou à intégrer un serveur PHP plus moderne, alors il faut évaluer le coût réel de migration au lieu de suivre la mode.
C’est cette logique de décision qui m’amène au dernier contrôle que je fais toujours avant de déclarer un déploiement propre.
La vérification finale que je fais avant de mettre en ligne
Avant de considérer qu’une installation Nginx pour Laravel est terminée, je passe une checklist courte mais stricte. Elle prend peu de temps, et elle évite la majorité des incidents que l’on retrouve ensuite dans les tickets de production.
- Le `root` pointe bien vers `public/`.
- Toutes les requêtes passent correctement par `public/index.php`.
- Les fichiers cachés sont bloqués, à l’exception de ce qui doit rester public.
- `storage` et `bootstrap/cache` sont écrivable par le bon compte système.
- `APP_DEBUG` est désactivé sur l’environnement public.
- Les caches Laravel ont été régénérés au déploiement.
- Le point de santé `/up` est surveillé.
- Les workers et services longue durée ont bien été relancés.
Quand ces huit points tiennent ensemble, la configuration est solide, lisible et facile à maintenir. À ce stade, je ne cherche plus à “faire marcher” Laravel derrière Nginx, je cherche surtout à faire en sorte qu’il reste fiable quand la charge, les déploiements et les petites erreurs humaines s’accumulent.