---
title: 'Dockerfile pour PHP et Laravel : bonnes pratiques | DevSense'
description: 'Construire une image Laravel de production : Dockerfile multi-stage pour PHP-FPM et les assets Vite, cache des couches Composer et npm, OPcache, non-root, secrets, healthcheck et une seule image pour le web, les workers et le scheduler.'
faq:
    - { question: "Peut-on utiliser l'image Laravel Sail en production ?", answer: "Non. L'image Sail est conçue pour le développement : elle embarque Xdebug, Node.js, des clients de bases de données et d'autres outils, et l'application est servie par php artisan serve, un serveur de développement monothread. Pour la production, on construit une image minimale distincte basée sur PHP-FPM (ou FrankenPHP), sans dépendances de développement." }
    - { question: 'À quoi sert un Dockerfile multi-stage pour PHP ?', answer: "Composer, Node.js, les paquets npm et les compilateurs ne servent qu'au moment du build. Dans un build multi-stage, les dépendances et les assets sont construits dans des étapes séparées, et seuls vendor/ et public/build sont copiés dans l'image finale. L'image est plus légère, contient moins de logiciels vulnérables, et ses couches se mettent mieux en cache." }
    - { question: "Quand exécuter php artisan config:cache : au build de l'image ou au démarrage du conteneur ?", answer: "Au démarrage du conteneur. config:cache fige les valeurs des variables d'environnement dans un fichier. Si on le fait au build, ce sont les variables de l'environnement de build qui finissent dans l'image, et les réglages de production passés au conteneur sont ignorés. Il en va de même pour route:cache si les routes dépendent de la configuration." }
    - { question: 'Alpine ou Debian pour une image PHP ?', answer: "Alpine offre une taille réduite, mais utilise musl au lieu de glibc : certaines extensions mettent plus de temps à compiler, et le comportement de certaines bibliothèques ainsi que les performances peuvent différer. L'image Debian (celle par défaut de l'image officielle php) est plus volumineuse, mais plus prévisible. Pour la plupart des projets Laravel, le choix raisonnable est Debian, et Alpine lorsque la taille est critique et que vous avez validé votre application sur musl." }
published: '2026-10-03'
---
# Dockerfile pour PHP et Laravel : une image de production sans superflu

En local, vous avez Sail : `sail up`, et tout fonctionne. Puis vient le moment de la production, et le chemin le plus rapide consiste à reprendre la même image, à ajouter `COPY . .` et à la lancer. Un mois plus tard, on découvre que l'image pèse plus d'un gigaoctet, qu'elle contient Xdebug et Node.js, que l'application est servie par `php artisan serve` sur un seul thread, que chaque build retélécharge tous les paquets Composer, et que le `.env` avec les identifiants de la base se trouve directement dans une couche de l'image.

Une bonne image Laravel de production est construite autrement : les dépendances et les assets sont produits dans des étapes séparées, l'image finale ne contient que ce qui est nécessaire à l'exécution, le processus ne tourne pas en root, et la configuration provient de l'environnement au démarrage. Cet article présente un Dockerfile complet en justifiant chaque décision, ainsi qu'un schéma pour lancer le web, les workers et le scheduler à partir d'une seule image.

**Navigation :** [Tous les outils](../) · [Sail : présentation](sail) · [Sail : environnement et déploiement](sail-env-deploy) · [Sail : files d'attente](sail-queues) · [Zero-downtime deployment](../architecture/zero-downtime-deployment-laravel)

## Sommaire

* [Pourquoi l'image Sail ne convient pas à la production](#sail-vs-prod)
* [Architecture : une image, plusieurs processus](#image-layout)
* [.dockerignore : ce qui ne doit pas entrer dans le contexte](#dockerignore)
* [Le Dockerfile multi-stage complet](#multistage)
* [Ordre des couches et cache de build](#layer-cache)
* [PHP-FPM et OPcache pour la production](#php-config)
* [Configuration et secrets](#config)
* [Web, workers et scheduler à partir d'une seule image](#processes)
* [Healthcheck et arrêt en douceur](#healthchecks)
* [Sécurité de l'image](#security)
* [Alpine, Debian ou FrankenPHP](#base-image)
* [Erreurs fréquentes](#common-mistakes)
* [Checklist](#checklist)
* [Quiz d'auto-évaluation](#self-test-quiz)

---

<a id="sail-vs-prod"></a>
## Pourquoi l'image Sail ne convient pas à la production

Sail est un excellent outil de développement, et c'est précisément pour cela qu'il ne convient pas à un environnement de production :

* l'application est lancée via `php artisan serve` sous Supervisor : c'est le serveur intégré de PHP destiné au développement, pas PHP-FPM ;
* l'image embarque Xdebug, Node.js, npm, les clients MySQL/PostgreSQL et d'autres utilitaires : taille accrue et surface d'attaque élargie ;
* le code est monté depuis l'hôte via un volume au lieu d'être copié dans l'image : l'image en elle-même ne contient pas l'application ;
* les droits et l'UID sont ajustés à l'utilisateur de l'hôte au démarrage du conteneur.

L'image de production est un artefact distinct, avec son propre Dockerfile. Sail reste, lui, l'outil du développement local.

---

<a id="image-layout"></a>
## Architecture : une image, plusieurs processus

Pour Laravel, le principe « un processus par conteneur » signifie : **une seule image applicative**, à partir de laquelle on lance différents processus.

```
                 ┌──────────────────────┐
  HTTP ────────► │ web (nginx)          │ static files from public/
                 └──────────┬───────────┘
                            │ FastCGI :9000
                 ┌──────────▼───────────┐
                 │ app (php-fpm)        │ ┐
                 └──────────────────────┘ │
                 ┌──────────────────────┐ │  same image,
                 │ queue (queue:work)   │ ├─ different command
                 └──────────────────────┘ │
                 ┌──────────────────────┐ │
                 │ scheduler            │ ┘
                 │ (schedule:work)      │
                 └──────────────────────┘
```

Tous les processus disposent ainsi à coup sûr du même code et des mêmes extensions, tout en se scalant indépendamment : davantage de workers pendant un import, davantage de PHP-FPM aux heures de pointe. nginx est construit dans une petite étape séparée, dans laquelle on ne copie que `public/`.

---

<a id="dockerignore"></a>
## .dockerignore : ce qui ne doit pas entrer dans le contexte

Tout ce qui se trouve dans le répertoire de build est envoyé au démon Docker en tant que contexte. Sans `.dockerignore`, un `COPY . .` fera entrer dans l'image `.git`, le `vendor/` local, `node_modules/` et, plus dangereux encore, le `.env`.

```gitignore
# .dockerignore
.git
.env
.env.*
!.env.example
node_modules
vendor
public/build
public/hot
public/storage
storage/logs/*
storage/framework/cache/*
storage/framework/sessions/*
storage/framework/views/*
bootstrap/cache/*.php
tests
docker-compose*.yml
*.log
```

`vendor/` et `public/build/` sont exclus parce qu'ils sont construits dans Docker : avec la bonne version de PHP, sans paquets de développement et de façon reproductible.

---

<a id="multistage"></a>
## Le Dockerfile multi-stage complet

```dockerfile
# syntax=docker/dockerfile:1
ARG PHP_VERSION=8.5

# ---------- base: PHP runtime with the extensions the app needs ----------
FROM php:${PHP_VERSION}-fpm AS base

COPY --from=mlocati/php-extension-installer /usr/bin/install-php-extensions /usr/local/bin/
RUN install-php-extensions pdo_pgsql redis intl zip bcmath pcntl \
 && mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"

COPY docker/php/conf.d/ $PHP_INI_DIR/conf.d/
COPY docker/php-fpm/zz-app.conf /usr/local/etc/php-fpm.d/zz-app.conf

WORKDIR /var/www/html

# ---------- vendor: production Composer dependencies ----------
FROM base AS vendor

ENV COMPOSER_HOME=/tmp/composer \
    COMPOSER_CACHE_DIR=/tmp/composer/cache
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer

COPY composer.json composer.lock ./
RUN --mount=type=cache,target=/tmp/composer/cache \
    composer install --no-dev --no-scripts --no-autoloader \
        --prefer-dist --no-interaction --no-progress

# ---------- assets: Vite build ----------
FROM node:24-alpine AS assets
WORKDIR /app

COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci --no-audit --no-fund

COPY vite.config.* ./
COPY resources/ ./resources/
RUN npm run build

# ---------- runtime: the image that runs in production ----------
FROM base AS runtime

COPY --from=vendor /var/www/html/vendor ./vendor
COPY . .
COPY --from=assets /app/public/build ./public/build

# Composer is mounted only for this step and never ends up in the image.
RUN --mount=type=bind,from=composer:2,source=/usr/bin/composer,target=/usr/local/bin/composer \
    composer dump-autoload --optimize --no-dev --no-interaction \
 && mkdir -p storage/framework/cache storage/framework/sessions storage/framework/views \
             storage/logs bootstrap/cache \
 && chown -R www-data:www-data storage bootstrap/cache

COPY --chmod=755 docker/entrypoint.sh /usr/local/bin/entrypoint

USER www-data
EXPOSE 9000
ENTRYPOINT ["entrypoint"]
CMD ["php-fpm"]

# ---------- web: nginx with public assets only ----------
FROM nginx:stable-alpine AS web
COPY docker/nginx/default.conf /etc/nginx/conf.d/default.conf
COPY --from=runtime /var/www/html/public /var/www/html/public
```

Construire deux images à partir d'un seul fichier :

```bash
docker build --target runtime -t registry.example.com/shop:1.42.0 .
docker build --target web     -t registry.example.com/shop-web:1.42.0 .
```

Détail des étapes :

* **`base`** : PHP avec ses extensions. `install-php-extensions` installe lui-même les bibliothèques système et supprime les compilateurs après la compilation de l'extension. `php.ini-production` active les réglages de production : les erreurs ne sont pas affichées dans la réponse, `expose_php` est désactivé. En PHP 8.5, OPcache fait toujours partie du cœur ; en PHP 8.4 et antérieur, ajoutez `opcache` à la liste des extensions.
* **`vendor`** : uniquement `composer.json` et `composer.lock`, sans le code de l'application. `--no-scripts --no-autoloader` sont nécessaires car les scripts de Laravel (`package:discover`) ont besoin d'un code qui n'existe pas encore à cette étape. L'étape hérite de `base`, si bien que Composer vérifie les exigences de version de PHP et d'extensions : `--ignore-platform-reqs` est inutile et même nuisible.
* **`assets`** : Node.js n'est nécessaire qu'ici. Si Tailwind analyse des templates Blade ou des classes de pagination situés dans `vendor/`, copiez-les aussi dans cette étape.
* **`runtime`** : l'image finale, avec le code, `vendor/` et les assets compilés. `composer dump-autoload --optimize` génère la classmap et lance `package:discover`. Composer n'est monté via `--mount=type=bind` que le temps de cette commande.
* **`web`** : nginx et le contenu de `public/`. Aucun code PHP à l'intérieur.

La configuration nginx correspondant à ce schéma :

```nginx
# docker/nginx/default.conf
server {
    listen 80;
    root /var/www/html/public;
    index index.php;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        fastcgi_pass app:9000;
        fastcgi_param SCRIPT_FILENAME /var/www/html/public/index.php;
        include fastcgi_params;
    }

    location ~ /\.(?!well-known) {
        deny all;
    }
}
```

---

<a id="layer-cache"></a>
## Ordre des couches et cache de build

Docker reconstruit une couche si ses données d'entrée ont changé, ainsi que toutes les couches qui la suivent. D'où la règle principale : **d'abord ce qui change rarement, ensuite ce qui change souvent**.

* `composer.json` et `composer.lock` sont copiés séparément du code : modifier un template Blade ne relance pas `composer install`.
* `package.json` et `package-lock.json` : même principe pour npm.
* `COPY . .` se place tout à la fin, après toutes les étapes lourdes.

Second niveau : les **cache mounts de BuildKit**, `RUN --mount=type=cache,target=...`. Même lorsque la couche des dépendances est reconstruite (vous avez ajouté un paquet), Composer et npm récupèrent les archives déjà téléchargées depuis le cache plutôt que depuis le réseau. Le cache n'entre pas dans l'image.

En CI, on conserve le cache d'une exécution à l'autre via `docker buildx build --cache-to/--cache-from` (par exemple dans le registry ou dans le cache GitHub Actions). Sans cela, chaque build sur un runner vierge repart de zéro.

---

<a id="php-config"></a>
## PHP-FPM et OPcache pour la production

```ini
; docker/php/conf.d/opcache.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
; Code never changes inside a running container: skip file timestamp checks.
opcache.validate_timestamps=0
```

```ini
; docker/php/conf.d/app.ini
memory_limit=256M
upload_max_filesize=20M
post_max_size=25M
expose_php=Off
```

```ini
; docker/php-fpm/zz-app.conf
[www]
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 8
pm.max_requests = 1000
ping.path = /ping
clear_env = no
```

* **`opcache.validate_timestamps=0`** : dans le conteneur, le code est immuable, PHP n'a donc pas besoin de vérifier la date de modification des fichiers à chaque requête. Le nouveau code n'arrive qu'avec un nouveau conteneur.
* **`pm.max_children`** se calcule à partir de la mémoire : si le conteneur dispose de 1 Go et qu'un worker PHP-FPM occupe environ 50 Mo, il ne supportera pas plus de 15 à 18 processus. Mesurez la consommation réelle sous charge.
* **`pm.max_requests`** redémarre un processus FPM après N requêtes : c'est un filet de sécurité contre les fuites mémoire lentes.
* **`clear_env = no`** : par défaut, PHP-FPM vide les variables d'environnement de ses processus. Si la configuration n'est pas mise en cache, l'application ne les verra pas.

Dans un conteneur, les logs de l'application doivent aller sur stdout/stderr et non dans un fichier à l'intérieur du conteneur : `LOG_CHANNEL=stderr`. Ils sont alors collectés par Docker ou par l'orchestrateur, et ne se perdent pas lorsque le conteneur est recréé.

---

<a id="config"></a>
## Configuration et secrets

**L'image ne contient pas de `.env`.** La même image doit pouvoir tourner en staging et en production : seules diffèrent les variables d'environnement passées au conteneur. Une image qui contient un `.env` est liée à un seul environnement et distribue les secrets à quiconque a accès au registry.

**Les caches Laravel sont construits au démarrage du conteneur, pas au build.** `config:cache` fige les valeurs courantes des variables d'environnement dans `bootstrap/cache/config.php`. Exécuté dans le Dockerfile, il ferait entrer dans l'image les valeurs de l'environnement de build. Même chose pour les routes si elles dépendent de la configuration. Par exemple, chez DevSense, la route de la clé IndexNow n'est enregistrée que si `config('seo.indexnow_key')` est défini : un `route:cache` au moment du build figerait un ensemble de routes qui ne la contient pas.

```sh
#!/bin/sh
# docker/entrypoint.sh
set -e

# Build config, route, view and event caches from the runtime environment.
if [ "${LARAVEL_OPTIMIZE:-true}" = "true" ]; then
    php artisan optimize
fi

# exec replaces the shell, so PHP becomes PID 1 and receives SIGTERM directly.
exec "$@"
```

**Les migrations sont une étape distincte de la release, pas une partie de l'entrypoint.** Si `migrate --force` s'exécute au démarrage de chaque conteneur, dix réplicas lanceront les migrations simultanément. Exécutez-les une seule fois avant la bascule du trafic : `docker compose run --rm app php artisan migrate --force` ou via un job dédié dans l'orchestrateur. Comment écrire des migrations compatibles avec le code en cours d'exécution : voir l'article sur le [déploiement sans interruption](../architecture/zero-downtime-deployment-laravel#migrations).

**Les secrets de build passent par `--mount=type=secret`.** Si `composer install` a besoin d'un token pour un dépôt privé, ne le passez pas via `ARG` ou `ENV` : leurs valeurs sont visibles dans l'historique de l'image (`docker history`). BuildKit monte le secret uniquement le temps d'une commande :

```dockerfile
RUN --mount=type=cache,target=/tmp/composer/cache \
    --mount=type=secret,id=composer_auth,target=/tmp/composer/auth.json \
    composer install --no-dev --no-scripts --no-autoloader --prefer-dist --no-interaction
```

```bash
docker build --secret id=composer_auth,src=$HOME/.config/composer/auth.json --target runtime .
```

---

<a id="processes"></a>
## Web, workers et scheduler à partir d'une seule image

```yaml
# docker-compose.prod.yml
services:
  app:
    image: registry.example.com/shop:${RELEASE}
    env_file: .env.production
    init: true
    restart: unless-stopped

  web:
    image: registry.example.com/shop-web:${RELEASE}
    ports: ["80:80"]
    depends_on: [app]
    restart: unless-stopped
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://127.0.0.1/up"]
      interval: 10s
      timeout: 3s
      retries: 3

  queue:
    image: registry.example.com/shop:${RELEASE}
    env_file: .env.production
    command: ["php", "artisan", "queue:work", "--tries=3", "--timeout=120", "--max-time=3600", "--memory=256"]
    init: true
    stop_grace_period: 180s
    restart: unless-stopped

  scheduler:
    image: registry.example.com/shop:${RELEASE}
    env_file: .env.production
    command: ["php", "artisan", "schedule:work"]
    init: true
    restart: unless-stopped
```

* `command` remplace `CMD`, tandis que l'`ENTRYPOINT` avec `optimize` s'exécute pour tous les processus.
* `restart: unless-stopped` remplace Supervisor : un `queue:work` qui s'est arrêté sur `--max-time` ou après un `queue:restart` redémarrera.
* `init: true` lance un petit processus init qui relaie les signaux et récupère les processus « zombies ».
* `schedule:work` est un scheduler sans cron : la commande lance `schedule:run` chaque minute. Dans Kubernetes, on lui préfère souvent un CronJob avec `schedule:run`.

Les paramètres des workers (timeouts, nouvelles tentatives, mémoire) sont détaillés dans l'article sur les [files d'attente en production](../architecture/laravel-queues-production).

---

<a id="healthchecks"></a>
## Healthcheck et arrêt en douceur

**Vérification de l'état de santé.** Laravel 11+ fournit une route intégrée `/up` (paramètre `health` dans `bootstrap/app.php`) : elle répond `200` si l'application a démarré, et `500` sinon. Un contrôle via nginx (`wget` est présent dans l'image Alpine) vérifie toute la chaîne nginx → PHP-FPM → Laravel. Pour contrôler PHP-FPM seul, il y a `ping.path = /ping` dans la configuration du pool ; on peut l'interroger avec l'utilitaire `cgi-fcgi` du paquet fcgi. Kubernetes ignore l'instruction `HEALTHCHECK` du Dockerfile et utilise ses propres sondes readiness et liveness ; les mêmes URL leur conviennent.

**Arrêt en douceur.** `docker stop` envoie `SIGTERM` au processus principal, puis `SIGKILL` après `stop_grace_period` (10 secondes par défaut). Pour que l'arrêt se fasse en douceur :

* le processus principal doit être PHP lui-même et non un shell : l'entrypoint contient `exec "$@"`, et `CMD` est écrit en forme exec (`["php-fpm"]`, et non `php-fpm` sous forme de chaîne) ;
* l'extension **pcntl** doit être installée pour le worker de file d'attente : `queue:work` reçoit `SIGTERM`, termine le job en cours et s'arrête ;
* `stop_grace_period` doit être supérieur au job le plus long, sinon Docker tuera le worker en plein travail. C'est le même principe que `stopwaitsecs` dans Supervisor.

---

<a id="security"></a>
## Sécurité de l'image

* **Pas de root.** `USER www-data` à la fin du Dockerfile. Si une RCE est découverte dans l'application, l'attaquant obtiendra les droits de `www-data` dans le conteneur, et non ceux de root. PHP-FPM écoute sur le port 9000, qui ne requiert aucun privilège. L'avertissement de FPM concernant la directive `user`, ignorée sans root, est sans danger.
* **Le minimum de logiciels.** Ni Xdebug, ni Node.js, ni Composer, ni git, ni compilateurs dans l'image finale. Ce qui n'existe pas ne peut pas être exploité.
* **Des versions figées.** `php:8.5-fpm` change à chaque release de correctif. Pour la reproductibilité, figez le digest : `FROM php:8.5-fpm@sha256:...`, et automatisez les mises à jour des images de base avec Dependabot ou Renovate.
* **Reconstruction régulière.** Les vulnérabilités d'OpenSSL ou de glibc sont corrigées par une nouvelle image de base : même si votre code n'a pas changé, il faut reconstruire l'image au moins toutes les une à deux semaines.
* **Scan.** `docker scout cves` ou `trivy image` en CI détectent les vulnérabilités connues dans les paquets système et les dépendances. Pour les dépendances PHP, restent `composer audit` et `npm audit`.
* **Lecture seule.** Au niveau suivant, on monte le système de fichiers racine du conteneur en `read_only: true`, en ne laissant inscriptibles que `storage/`, `bootstrap/cache` et `/tmp`.

Les réglages du serveur autour des conteneurs (en-têtes de sécurité, TLS, limites) sont décrits dans l'article sur le [hardening des serveurs et de l'infrastructure](../security/server-and-infrastructure-hardening).

---

<a id="base-image"></a>
## Alpine, Debian ou FrankenPHP

| Option | Avantages | Inconvénients |
|---------|-------|--------|
| `php:8.x-fpm` (Debian) | glibc prévisible, paquets précompilés rapides, moins de surprises | Image plus volumineuse |
| `php:8.x-fpm-alpine` | Taille réduite | musl au lieu de glibc : compilation des extensions plus longue, différences de comportement de certaines bibliothèques et de performances |
| FrankenPHP | Un seul processus : serveur web Caddy et PHP ensemble, HTTPS, mode worker pour Laravel Octane | Autre modèle d'exécution : en mode worker, l'état survit entre les requêtes, et les fuites comme les singletons « sales » deviennent votre problème |

Pour un projet Laravel typique, le point de départ raisonnable est une image Debian avec PHP-FPM et nginx, comme dans cet article. Alpine se justifie lorsque la taille compte vraiment et que vous avez validé l'application sur musl. Les modèles d'exécution de PHP (FPM, workers, event loop) sont comparés dans l'article [PHP côté serveur](../php/runtimes).

---

<a id="common-mistakes"></a>
## Erreurs fréquentes

**1. L'image Sail ou une image de dev en production.**
`artisan serve`, Xdebug et Node.js dans un environnement de production : lent et dangereux.

**2. Un `.env` dans l'image.**
Les secrets se retrouvent dans chaque couche et chez quiconque peut télécharger l'image. La configuration passe uniquement par l'environnement.

**3. `config:cache` dans le Dockerfile.**
Les variables de l'environnement de build sont figées dans l'image, les réglages de production sont ignorés.

**4. `COPY . .` avant `composer install`.**
La moindre modification du code invalide le cache des dépendances, et chaque build retélécharge tous les paquets.

**5. `--ignore-platform-reqs` dans Composer.**
Le build passe, mais il manque une extension PHP à l'exécution. Installez les dépendances avec le même PHP qu'en production.

**6. Des secrets via `ARG` ou `ENV`.**
Les valeurs sont visibles dans `docker history`. Utilisez `--mount=type=secret`.

**7. `CMD` en forme shell et entrypoint sans `exec`.**
C'est le shell qui reçoit `SIGTERM`, pas PHP. Le worker ne s'arrête pas en douceur et se fait tuer au bout de 10 secondes en plein job.

**8. `migrate --force` dans l'entrypoint de chaque réplica.**
Plusieurs conteneurs lancent les migrations simultanément. Les migrations sont une étape distincte de la release.

**9. Un processus qui tourne en root.**
La moindre vulnérabilité de l'application donne immédiatement les droits root dans le conteneur.

---

<a id="checklist"></a>
## Checklist

1. Un `.dockerignore` existe : `.env`, `.git`, `vendor/`, `node_modules/` n'entrent pas dans le contexte.
2. Build multi-stage : Composer et Node.js uniquement dans les étapes de build.
3. `composer.json`/`composer.lock` et `package*.json` sont copiés avant le code ; les cache mounts de BuildKit sont utilisés.
4. Les dépendances sont installées avec le même PHP et les mêmes extensions qu'à l'exécution, sans `--ignore-platform-reqs`.
5. `php.ini-production`, OPcache avec `validate_timestamps=0`, `pm.max_children` calculé d'après la mémoire.
6. L'image ne contient ni `.env` ni secrets ; les tokens privés passent par `--mount=type=secret`.
7. `php artisan optimize` s'exécute au démarrage du conteneur ; les migrations sont une étape distincte de la release.
8. Le processus tourne sous `www-data` ; l'image de base est figée par digest et reconstruite régulièrement.
9. web, queue et scheduler sont lancés à partir d'une seule image avec des commandes différentes.
10. Il existe un healthcheck sur `/up`, un `exec` dans l'entrypoint, pcntl et un `stop_grace_period` supérieur au job le plus long.
11. L'image est scannée en CI à la recherche de vulnérabilités.

---

## En résumé

Une image Laravel de production, ce n'est pas « Sail plus un COPY », mais un artefact à part entière : construite en plusieurs étapes, elle ne contient que le nécessaire à l'exécution, ne sait rien de l'environnement concret et tourne sans root. L'ordre des couches et le cache BuildKit rendent le build rapide, `optimize` au démarrage prend en compte la vraie configuration, et une image unique pour le web, les workers et le scheduler garantit que tous les processus tournent sur le même code.

---

<a id="self-test-quiz"></a>
## Quiz d'auto-évaluation

### Question 1 : Pourquoi copie-t-on `composer.json` et `composer.lock` dans l'image séparément et avant le reste du code ?
- A) Composer ne sait pas lire les fichiers copiés par la commande `COPY . .`.
- B) Pour que la couche contenant les dépendances installées soit réutilisée depuis le cache tant que les manifestes eux-mêmes n'ont pas changé, et que les modifications du code ne relancent pas `composer install`.
- C) C'est une exigence du format de build multi-stage.

<details>
<summary><b>Afficher la réponse</b></summary>

**Réponse : B**
Docker reconstruit une couche lorsque ses données d'entrée changent. Si les dépendances sont installées après `COPY . .`, la moindre modification d'un template invalide la couche de `vendor/`. Copier les manifestes séparément découple l'installation des dépendances des modifications du code.
</details>

### Question 2 : La commande `php artisan config:cache` est exécutée dans le Dockerfile. Que se passe-t-il en production ?
- A) Rien de particulier : Laravel relira les variables d'environnement au démarrage.
- B) L'application utilisera les valeurs présentes dans l'environnement au moment du build et ignorera les variables passées au conteneur.
- C) Le build échouera, car `config:cache` ne peut pas s'exécuter sans base de données.

<details>
<summary><b>Afficher la réponse</b></summary>

**Réponse : B**
Une configuration mise en cache ne relit pas `env()`. Le cache doit être construit au démarrage du conteneur, une fois les variables d'environnement transmises, par exemple avec `php artisan optimize` dans l'entrypoint.
</details>

### Question 3 : Lors d'un `docker stop`, le worker de file d'attente est interrompu en plein job. Laquelle de ces mesures n'aidera PAS ?
- A) `exec "$@"` dans l'entrypoint et la forme exec de `CMD`, pour que ce soit PHP qui reçoive `SIGTERM`.
- B) L'extension pcntl installée et un `stop_grace_period` supérieur au job le plus long.
- C) Augmenter `memory_limit` dans `php.ini`.

<details>
<summary><b>Afficher la réponse</b></summary>

**Réponse : C**
L'arrêt en douceur dépend de la transmission du signal au processus PHP, du traitement de `SIGTERM` (pcntl) et du délai que Docker accorde avant `SIGKILL`. La limite mémoire n'a rien à voir avec cela.
</details>