🐳 Docker : un multi-stage build propre pour vos applications Laravel
📚 Introduction
Quand on dockerise une application Laravel, la tentation est grande d’avoir une seule image qui contient à la fois Composer, Node.js, NPM, les dépendances de build et le runtime PHP. Ça marche, mais le résultat est :
- une image énorme (souvent plus de 1 Go),
- une surface d’attaque inutile en production (Composer, Git, outils de build),
- des builds lents parce qu’on réinstalle tout à chaque modification de code.
Le multi-stage build de Docker règle ces trois problèmes en séparant clairement les étapes de préparation et le runtime final. C’est aujourd’hui mon pattern par défaut pour toutes les applications Laravel.
🧱 La structure en trois étages
L’idée tient en trois FROM distincts dans le même Dockerfile :
- Une étape “vendor” qui installe les dépendances PHP via Composer.
- Une étape “assets” qui installe Node, compile les assets (Vite ou Mix).
- Une étape “runtime” finale, minimale, qui copie uniquement ce dont la production a besoin.
# syntax=docker/dockerfile:1.7
# 1. Étape vendor
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install \
--no-dev \
--no-scripts \
--no-autoloader \
--prefer-dist \
--no-interaction
# 2. Étape assets
FROM node:22-alpine AS assets
WORKDIR /app
COPY package.json package-lock.json vite.config.* ./
COPY resources resources
COPY public public
RUN npm ci && npm run build
# 3. Étape runtime
FROM php:8.3-fpm-alpine AS runtime
WORKDIR /var/www/html
RUN apk add --no-cache icu-libs \
&& docker-php-ext-install opcache pdo_mysql intl bcmath
COPY --chown=www-data:www-data . .
COPY --from=vendor /app/vendor ./vendor
COPY --from=assets /app/public/build ./public/build
RUN php artisan optimize \
&& php artisan config:cache \
&& php artisan route:cache
USER www-data
CMD ["php-fpm"]L’image finale n’embarque ni Composer, ni Node, ni les caches d’NPM. Sur les projets que je gère, on passe typiquement de 1.2 Go à 280 Mo.
🎯 Pourquoi chaque étape compte
L’étape vendor isole l’installation Composer. En passant --no-scripts et --no-autoloader, on évite que Composer tente d’exécuter php artisan package:discover, qui aurait besoin de tout le code applicatif. L’autoload sera régénéré dans le runtime.
L’étape assets ne touche jamais au code PHP. Elle ne dépend que des fichiers de configuration front (package.json, vite.config.js) et des assets sources. Tant que vous ne touchez pas à ces fichiers, le cache Docker se réutilise et le build est instantané.
L’étape runtime est minimaliste. On part d’une image officielle php:fpm-alpine, on installe uniquement les extensions PHP nécessaires, on copie le strict minimum, et on exécute les commandes artisan optimize / config:cache / route:cache qui n’ont plus besoin de Composer.
🚀 Quelques optimisations qui font la différence
- Activer BuildKit (
DOCKER_BUILDKIT=1) si vous êtes encore sur un Docker ancien. Les caches montés (--mount=type=cache) accélèrent énormément les builds Composer et NPM. - Utiliser
--mount=type=cachepour les caches Composer et NPM :
RUN --mount=type=cache,target=/root/.composer/cache \
composer install --no-dev --no-scripts --prefer-dist- Vérifier la taille avec
diveoudocker historypour repérer les couches inutilement lourdes. - Pinner les images de base sur des digests
sha256pour des builds reproductibles à 100 %.
🔒 Côté sécurité
Quelques règles que je m’impose :
- Tourner en non-root en production :
USER www-data(ou un user dédié) à la fin du Dockerfile. - Ne jamais inclure
.envdans l’image. Les secrets passent par les variables d’environnement du runtime ou un secret manager. - Scanner les images avec
trivyougrypedans la CI pour repérer les CVE connues sur les dépendances système. - Garder un
.dockerignorestrict pour ne pas exfiltrer node_modules, vendor, tests ou fichiers de dev dans le contexte de build.
🎉 Conclusion
Un Dockerfile multi-stage propre, c’est un investissement de quelques heures qui paie sur tout le cycle de vie d’un projet : builds plus rapides, images plus légères, surface d’attaque réduite. Sur une équipe qui pousse plusieurs fois par jour, le gain cumulé en temps de CI et en bande passante de pull est loin d’être négligeable.
Si votre image Laravel pèse encore plus de 800 Mo, ouvrez votre Dockerfile, prenez 30 minutes, et appliquez la structure ci-dessus : vous gagnerez bien plus que ce temps en quelques semaines.