Un projet Laravel conteneurisé tient rarement sur une seule couche de configuration : il faut aligner les extensions PHP, les permissions, le cache, le mode d’exécution et la séparation entre développement et production. Dans cet article, je montre comment structurer un laravel dockerfile propre, quelles décisions font vraiment la différence, et comment éviter les images lourdes ou fragiles. L’objectif n’est pas de dockeriser pour cocher une case, mais d’obtenir un environnement reproductible que l’on peut maintenir sans douleur.
Les points qui comptent avant d’écrire la première ligne du build
- Le Dockerfile construit l’image PHP, mais les services voisins restent à part dans Docker Compose ou l’orchestrateur.
- Une base FPM légère suffit dans la majorité des cas, à condition d’installer seulement les extensions nécessaires.
- Le multi-stage build réduit la taille finale et évite d’embarquer les outils de compilation dans l’image runtime.
- Le développement local et la production ne doivent pas partager exactement les mêmes réglages.
- Les secrets ne doivent jamais être gravés dans l’image, et les dossiers `storage` et `bootstrap/cache` doivent rester écriables.
Construire une base Laravel propre sans confondre image et environnement
Je vois souvent des équipes traiter le Dockerfile comme si c’était tout l’environnement. En pratique, il ne gère que l’image de l’application; les services voisins restent dans Compose ou dans l’orchestrateur. Pour un backend Laravel, cette séparation est saine : PHP-FPM dans un conteneur, Nginx dans un autre, la base de données ailleurs, et éventuellement Redis ou un worker pour les queues.- Le Dockerfile doit rester centré sur le runtime PHP.
- Compose gère le lien entre l’application, la base et les services annexes.
- Le développement local peut monter le code en volume, alors que la production doit rester immuable.
- Les variables sensibles doivent venir de l’environnement, pas du build.
Cette découpe évite les conteneurs “fourre-tout” où chaque changement de service relance tout le reste. Une fois ce cadre posé, le choix de la base d’image et des extensions devient beaucoup plus simple.

Choisir la bonne base et les bonnes extensions
Je pars généralement d’une image PHP-FPM légère, puis j’ajoute seulement les extensions réellement utilisées. Pour une API Laravel, je préfère garder une surface minimale et éviter d’empiler des paquets “au cas où”. Le bon choix dépend surtout du niveau de confort de l’équipe avec la compilation des dépendances système et du type de charge attendu.
| Base | Quand je la choisis | Avantages | Limites |
|---|---|---|---|
php:8.4-fpm-bookworm-slim |
Par défaut pour la plupart des projets Laravel | Bon compromis entre taille, compatibilité et facilité de maintenance | Un peu plus volumineuse qu’une variante Alpine |
php:8.4-fpm-alpine |
Quand chaque mégaoctet compte et que l’équipe maîtrise bien l’écosystème Alpine | Image plus petite, démarrage souvent rapide | Compilation et débogage parfois plus pénibles, surtout avec certaines extensions |
dunglas/frankenphp |
Quand je veux un runtime moderne avec un serveur intégré, ou quand Octane fait partie du plan | Stack plus compacte, bonne option pour certains déploiements Laravel | Change le modèle d’exécution, donc demande plus de validation opérationnelle |
Côté extensions, je garde une logique de besoin réel : pdo_mysql ou pdo_pgsql pour la base, intl pour l’internationalisation, zip pour les archives, opcache pour la performance, et redis si les queues ou le cache l’exigent. Si le projet manipule des montants ou des calculs précis, j’ajoute aussi bcmath. Le réflexe à éviter, c’est d’installer tout ce qui semble “utile un jour” : on alourdit le build sans améliorer le service rendu.
Avec ces choix verrouillés, le Dockerfile lui-même devient beaucoup plus simple à lire et à maintenir.
Un exemple de Dockerfile de production prêt à adapter
Voici la structure que je privilégie quand je veux une image Laravel sérieuse pour la production. Elle sépare les dépendances applicatives du runtime final, ce qui réduit la taille de l’image et limite les outils inutiles dans l’environnement d’exécution.FROM php:8.4-fpm-bookworm AS base
WORKDIR /var/www/html
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
git \
unzip \
libicu-dev \
libzip-dev \
libpng-dev \
&& docker-php-ext-install \
pdo_mysql \
intl \
zip \
opcache \
&& rm -rf /var/lib/apt/lists/*
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
FROM base AS vendor
COPY composer.json composer.lock ./
RUN composer install \
--no-interaction \
--no-progress \
--prefer-dist \
--no-dev \
--optimize-autoloader
FROM base AS app
COPY . .
COPY --from=vendor /var/www/html/vendor ./vendor
RUN chown -R www-data:www-data storage bootstrap/cache \
&& chmod -R ug+rwX storage bootstrap/cache
USER www-data
EXPOSE 9000
CMD ["php-fpm"]
La logique ici est volontairement simple. Je copie d’abord uniquement `composer.json` et `composer.lock` pour profiter du cache de build, puis je construis le répertoire `vendor` dans un stage séparé. Ensuite seulement, je copie le code applicatif complet. Ce détail change beaucoup de choses dès que les dépendances changent moins souvent que le code métier.
Je n’active pas systématiquement `config:cache` ou `route:cache` dans le build lui-même, parce que je préfère garder ce genre d’optimisation au moment du déploiement, quand les variables d’environnement sont réellement présentes et stables. C’est plus sûr, surtout quand plusieurs environnements coexistent.
Une fois ce socle en place, la vraie question devient l’adaptation au local, parce qu’on n’exécute pas la même image de la même manière selon l’environnement.
Adapter le même socle au développement local
En local, j’accepte un peu plus de souplesse : code monté en volume, Xdebug si nécessaire, et parfois des commandes interactives pour simplifier le diagnostic. En production, je veux l’inverse : image figée, pas de dépendances inutiles, pas de code modifié à la volée depuis l’hôte.
| Aspect | Développement | Production |
|---|---|---|
| Code source | Monté en volume pour voir les changements immédiatement | Copié dans l’image pour garder un artefact immuable |
| Dépendances PHP | Installées avec les besoins du débogage et parfois les paquets de dev | Installées en mode `--no-dev` |
| Debug | Xdebug, logs plus bavards, cycle de correction rapide | Debug désactivé, journalisation maîtrisée |
| Cache Laravel | Souvent régénéré ou laissé souple pour éviter les surprises | Activé au déploiement quand l’environnement est stable |
| Modèle d’exécution | FPM + Nginx, ou stack plus simple pour itérer vite | FPM, Octane ou FrankenPHP selon les besoins réels |
Si l’équipe travaille beaucoup sur une API, je préfère souvent un `docker-compose.override.yml` plutôt qu’un second Dockerfile. Le Dockerfile reste unique; l’override ajuste seulement les volumes, les ports, les variables et la commande de démarrage. C’est plus lisible, et surtout plus facile à faire évoluer quand la stack grossit.
Et même avec une bonne organisation, quelques erreurs reviennent sans cesse; c’est là qu’il faut être strict.
Les erreurs qui cassent les déploiements
Les incidents les plus agaçants ne viennent pas de Laravel lui-même, mais d’un Dockerfile trop permissif ou mal découpé. Je retrouve les mêmes pièges dans beaucoup de projets, et ils coûtent du temps au moment où l’on veut livrer vite.
| Erreur | Effet concret | Correction que j’applique |
|---|---|---|
| Copier tout le projet avant `composer install` | Le cache saute à chaque modification du code | Je copie d’abord les fichiers de dépendances, puis le reste du projet |
| Graver des secrets dans l’image | Risque de fuite et difficulté à refaire un build propre | Je sors les secrets du Dockerfile et je passe par les variables d’environnement ou un gestionnaire de secrets |
| Laisser le conteneur en root | Surface d’attaque plus large et problèmes de permissions plus fréquents | Je bascule vers `www-data` dès que le runtime est prêt |
| Oublier `.dockerignore` | Contexte de build gonflé par `vendor`, `node_modules`, `.git` ou les logs | Je retire tout ce qui n’a rien à faire dans l’image |
| Mal gérer `storage` et `bootstrap/cache` | Laravel ne peut plus écrire ses fichiers temporaires ou ses caches | Je corrige les droits avant de lancer PHP-FPM |
| Vouloir mélanger build frontend et runtime backend sans séparation | Image plus lourde, plus lente à construire, plus difficile à maintenir | Je sors le build JS dans un stage dédié ou dans un pipeline séparé |
Quand je nettoie un projet, j’ajoute presque toujours un `.dockerignore` minimal comme celui-ci :
.git
node_modules
vendor
storage/logs
.env
Le message est simple : le Dockerfile doit contenir ce qui sert à exécuter l’application, pas tout l’historique du dépôt. Une fois ces pièges éliminés, il reste les derniers arbitrages qui font la différence au quotidien.
Ce que je garde en tête avant de livrer l’image
Avant de pousser une image Laravel en production, je vérifie toujours quelques points très concrets. Ils ne sont pas spectaculaires, mais ils évitent la majorité des mauvaises surprises.
- Je reconstruis l’image depuis zéro pour m’assurer que rien ne dépend d’un état local caché.
- Je lance le conteneur avec seulement les variables nécessaires, sans `.env` embarqué dans l’image.
- Je sépare les rôles si l’application doit aussi exécuter des queues, des tâches planifiées ou Octane.
- Je surveille la taille finale et la vitesse de démarrage, parce qu’un conteneur trop lourd finit toujours par coûter plus cher.
- Je garde un tag d’image immuable pour pouvoir reproduire exactement le même build si un rollback devient nécessaire.
Si je devais résumer ma méthode, je partirais d’un socle PHP-FPM minimal, je séparerais nettement le build des dépendances de l’exécution, et je laisserais Docker Compose gérer les services adjacents. C’est, à mon sens, la manière la plus fiable de faire tenir Laravel dans un conteneur sans transformer le fichier en usine à gaz.