---
title: 'Dockerfile per PHP e Laravel: best practice | DevSense'
description: "Come costruire un'immagine di produzione per Laravel: Dockerfile multi-stage per PHP-FPM e asset Vite, cache dei layer di Composer e npm, OPcache, utente non root, secret, healthcheck e un'unica immagine per web, worker e scheduler."
faq:
    - { question: "Si può usare l'immagine di Laravel Sail in produzione?", answer: "No. L'immagine di Sail è pensata per lo sviluppo: contiene Xdebug, Node.js, client dei database e altri strumenti, e l'applicazione viene avviata con php artisan serve, un server di sviluppo single-thread. Per la produzione si costruisce un'immagine minimale separata basata su PHP-FPM (o FrankenPHP), senza dipendenze di sviluppo." }
    - { question: 'A cosa serve un Dockerfile multi-stage per PHP?', answer: "Composer, Node.js, i pacchetti npm e i compilatori servono solo durante la build. In una build multi-stage le dipendenze e gli asset vengono costruiti in stage separati, e nell'immagine finale si copiano solo vendor/ e public/build. L'immagine risulta più piccola, contiene meno software vulnerabile e i layer vengono messi in cache meglio." }
    - { question: "Quando eseguire php artisan config:cache: durante la build dell'immagine o all'avvio del container?", answer: "All'avvio del container. config:cache cristallizza in un file i valori delle variabili d'ambiente. Se lo si fa durante la build, nell'immagine finiscono le variabili dell'ambiente di build, e le impostazioni di produzione passate al container vengono ignorate. Lo stesso vale per route:cache, se le route dipendono dalla configurazione." }
    - { question: "Cosa scegliere per l'immagine PHP: Alpine o Debian?", answer: "Alpine offre dimensioni minori, ma usa musl al posto di glibc: alcune estensioni richiedono più tempo per la compilazione, e il comportamento di certe librerie e le prestazioni possono differire. L'immagine Debian (il default dell'immagine ufficiale php) è più grande, ma più prevedibile. Per la maggior parte dei progetti Laravel la scelta sensata è Debian, mentre Alpine va bene quando la dimensione è critica e avete verificato la vostra applicazione su musl." }
published: '2026-10-03'
---
# Dockerfile per PHP e Laravel: un'immagine di produzione senza superfluo

In locale avete Sail: `sail up`, e tutto funziona. Poi arriva il momento della produzione, e la strada più rapida è prendere la stessa immagine, aggiungere `COPY . .` e avviarla. Un mese dopo si scopre che l'immagine pesa più di un gigabyte, contiene Xdebug e Node.js, l'applicazione è servita da `php artisan serve` su un solo thread, ogni build riscarica tutti i pacchetti Composer, e il `.env` con le chiavi del database sta direttamente in un layer dell'immagine.

Una buona immagine di produzione per Laravel è fatta diversamente: dipendenze e asset vengono costruiti in stage separati, nell'immagine finale finisce solo ciò che serve per l'esecuzione, il processo non gira come root e la configurazione arriva dall'ambiente all'avvio. In questo articolo: il Dockerfile completo con l'analisi di ogni scelta e lo schema per avviare web, worker e scheduler da un'unica immagine.

**Navigazione:** [Tutti gli strumenti](../) · [Sail: panoramica](sail) · [Sail: ambiente e deploy](sail-env-deploy) · [Sail: code](sail-queues) · [Zero-downtime deployment](../architecture/zero-downtime-deployment-laravel)

## Indice

* [Perché l'immagine di Sail non è adatta alla produzione](#sail-vs-prod)
* [Architettura: un'immagine, più processi](#image-layout)
* [.dockerignore: cosa non deve finire nel contesto](#dockerignore)
* [Il Dockerfile multi-stage completo](#multistage)
* [Ordine dei layer e cache di build](#layer-cache)
* [PHP-FPM e OPcache per la produzione](#php-config)
* [Configurazione e secret](#config)
* [Web, worker e scheduler da un'unica immagine](#processes)
* [Healthcheck e arresto graceful](#healthchecks)
* [Sicurezza dell'immagine](#security)
* [Alpine, Debian o FrankenPHP](#base-image)
* [Errori comuni](#common-mistakes)
* [Checklist](#checklist)
* [Quiz di autovalutazione](#self-test-quiz)

---

<a id="sail-vs-prod"></a>
## Perché l'immagine di Sail non è adatta alla produzione

Sail è un ottimo strumento di sviluppo, ed è proprio per questo che non va bene per l'ambiente di produzione:

* l'applicazione viene avviata con `php artisan serve` sotto Supervisor: è il server integrato di PHP per lo sviluppo, non PHP-FPM;
* contiene Xdebug, Node.js, npm, client MySQL/PostgreSQL e altre utility: dimensioni maggiori e superficie d'attacco più ampia;
* il codice viene montato dall'host come volume, non copiato nell'immagine: l'immagine di per sé non contiene l'applicazione;
* permessi e UID vengono adattati all'utente dell'host all'avvio del container.

L'immagine di produzione è un artefatto separato con il proprio Dockerfile. Sail resta comunque per lo sviluppo locale.

---

<a id="image-layout"></a>
## Architettura: un'immagine, più processi

Il principio «un processo per container», per Laravel, significa: **un'unica immagine dell'applicazione**, da cui si avviano processi diversi.

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

Così tutti i processi hanno la garanzia di avere lo stesso codice e le stesse estensioni, e scalano in modo indipendente: più worker durante un import, più PHP-FPM nelle ore di punta. nginx viene costruito in un piccolo stage separato, in cui si copia solo `public/`.

---

<a id="dockerignore"></a>
## .dockerignore: cosa non deve finire nel contesto

Tutto ciò che si trova nella directory di build viene inviato al demone Docker come contesto. Senza `.dockerignore`, tramite `COPY . .` nell'immagine finiscono `.git`, il `vendor/` locale, `node_modules/` e, cosa più pericolosa, `.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/` e `public/build/` vengono esclusi perché sono costruiti dentro Docker: con la versione di PHP corretta, senza pacchetti di sviluppo e in modo riproducibile.

---

<a id="multistage"></a>
## Il Dockerfile multi-stage completo

```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
```

Build di due immagini dallo stesso file:

```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 .
```

Analisi degli stage:

* **`base`**: PHP con le estensioni. `install-php-extensions` installa da solo le librerie di sistema e rimuove i compilatori dopo la build dell'estensione. `php.ini-production` attiva le impostazioni di produzione: gli errori non vengono mostrati nella risposta, `expose_php` è disattivato. In PHP 8.5 OPcache fa sempre parte del core; su PHP 8.4 e precedenti aggiungete `opcache` all'elenco delle estensioni.
* **`vendor`**: solo `composer.json` e `composer.lock`, senza il codice dell'applicazione. `--no-scripts --no-autoloader` servono perché gli script di Laravel (`package:discover`) richiedono codice che in questo stage non c'è ancora. Lo stage eredita da `base`, quindi Composer verifica i requisiti sulla versione di PHP e sulle estensioni: `--ignore-platform-reqs` non serve ed è dannoso.
* **`assets`**: Node.js serve solo qui. Se Tailwind scansiona i template Blade o le classi della paginazione in `vendor/`, copiate anche quelli in questo stage.
* **`runtime`**: l'immagine finale: codice, `vendor/`, asset compilati. `composer dump-autoload --optimize` genera la classmap ed esegue `package:discover`. Composer viene collegato tramite `--mount=type=bind` solo per la durata di questo comando.
* **`web`**: nginx e il contenuto di `public/`. Non contiene codice PHP.

La configurazione di nginx per questo schema:

```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>
## Ordine dei layer e cache di build

Docker ricostruisce un layer se sono cambiati i suoi input, e con lui tutti i layer successivi. Da qui la regola principale: **prima ciò che cambia raramente, poi ciò che cambia spesso**.

* `composer.json` e `composer.lock` vengono copiati separatamente dal codice: la modifica di un template Blade non rilancia `composer install`.
* `package.json` e `package-lock.json`: lo stesso per npm.
* `COPY . .` sta in fondo, dopo tutti i passaggi pesanti.

Il secondo livello sono i **cache mount di BuildKit**: `RUN --mount=type=cache,target=...`. Anche quando il layer con le dipendenze viene ricostruito (avete aggiunto un pacchetto), Composer e npm prendono gli archivi già scaricati dalla cache e non dalla rete. La cache non finisce nell'immagine.

In CI la cache tra un'esecuzione e l'altra si conserva con `docker buildx build --cache-to/--cache-from` (per esempio nel registry o nella cache di GitHub Actions). Senza questo, ogni build su un runner pulito parte da zero.

---

<a id="php-config"></a>
## PHP-FPM e OPcache per la produzione

```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`**: nel container il codice è immutabile, quindi PHP non ha bisogno di controllare la data di modifica dei file a ogni richiesta. Il nuovo codice arriva solo con un nuovo container.
* **`pm.max_children`** si calcola in base alla memoria: se al container è assegnato 1 GB e un worker PHP-FPM occupa circa 50 MB, non reggerà più di 15–18 processi. Misurate il consumo reale sotto carico.
* **`pm.max_requests`** riavvia il processo FPM dopo N richieste: una protezione contro i memory leak lenti.
* **`clear_env = no`**: per impostazione predefinita PHP-FPM ripulisce le variabili d'ambiente per i propri processi. Se la configurazione non è in cache, l'applicazione non le vedrà.

I log dell'applicazione nel container devono andare su stdout/stderr, non in un file dentro il container: `LOG_CHANNEL=stderr`. Così li raccoglie Docker o l'orchestratore, e non vanno persi quando il container viene ricreato.

---

<a id="config"></a>
## Configurazione e secret

**Nell'immagine non c'è il `.env`.** La stessa immagine deve poter girare in staging e in produzione: la differenza sta solo nelle variabili d'ambiente passate al container. Un'immagine che contiene il `.env` è legata a un solo ambiente e distribuisce i secret a chiunque abbia accesso al registry.

**Le cache di Laravel si costruiscono all'avvio del container, non durante la build.** `config:cache` cristallizza i valori correnti delle variabili d'ambiente in `bootstrap/cache/config.php`. Se lo si esegue nel Dockerfile, nell'immagine finiscono i valori dell'ambiente di build. Con le route la storia è la stessa, se dipendono dalla configurazione. Per esempio, in DevSense la route della chiave IndexNow viene registrata solo se è impostato `config('seo.indexnow_key')`: un `route:cache` in fase di build fisserebbe un insieme di route che non la contiene.

```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 "$@"
```

**Le migration sono un passo separato del rilascio, non parte dell'entrypoint.** Se `migrate --force` viene eseguito all'avvio di ogni container, dieci repliche lanceranno le migration contemporaneamente. Eseguitele una volta sola prima di spostare il traffico: `docker compose run --rm app php artisan migrate --force` o con un job separato nell'orchestratore. Come scrivere migration compatibili con il codice in esecuzione è spiegato nell'articolo sul [deploy senza downtime](../architecture/zero-downtime-deployment-laravel#migrations).

**I secret in fase di build passano da `--mount=type=secret`.** Se per `composer install` serve il token di un repository privato, non passatelo con `ARG` o `ENV`: i valori sono visibili nella cronologia dell'immagine (`docker history`). BuildKit monta il secret solo per la durata di un singolo comando:

```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, worker e scheduler da un'unica immagine

```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` sovrascrive `CMD`, mentre l'`ENTRYPOINT` con `optimize` scatta per tutti i processi.
* `restart: unless-stopped` sostituisce Supervisor: un `queue:work` terminato per `--max-time` o dopo `queue:restart` ripartirà.
* `init: true` avvia un piccolo processo init che inoltra i segnali e raccoglie i processi «zombie».
* `schedule:work` è lo scheduler senza cron: il comando esegue `schedule:run` ogni minuto. In Kubernetes al suo posto si usa spesso un CronJob con `schedule:run`.

I parametri dei worker (timeout, retry, memoria) sono analizzati in dettaglio nell'articolo sulle [code in produzione](../architecture/laravel-queues-production).

---

<a id="healthchecks"></a>
## Healthcheck e arresto graceful

**Controllo dello stato di salute.** Laravel 11+ ha una route integrata `/up` (il parametro `health` in `bootstrap/app.php`): risponde `200` se l'applicazione si è avviata e `500` in caso contrario. Il controllo tramite nginx (`wget` è presente nell'immagine Alpine) verifica l'intera catena nginx → PHP-FPM → Laravel. Per un controllo separato di PHP-FPM c'è `ping.path = /ping` dalla configurazione del pool; lo si può interrogare con l'utility `cgi-fcgi` del pacchetto fcgi. Kubernetes ignora l'istruzione `HEALTHCHECK` del Dockerfile e usa le proprie readiness e liveness probe: gli stessi URL vanno bene anche per quelle.

**Arresto graceful.** `docker stop` invia `SIGTERM` al processo principale e, dopo `stop_grace_period` (10 secondi per impostazione predefinita), `SIGKILL`. Cosa serve perché l'arresto sia graceful:

* il processo principale deve essere PHP stesso, non la shell: nell'entrypoint c'è `exec "$@"`, e `CMD` è scritto in exec form (`["php-fpm"]`, non `php-fpm` come stringa);
* nel worker della coda è installata l'estensione **pcntl**: `queue:work` riceve `SIGTERM`, completa il job corrente ed esce;
* `stop_grace_period` è maggiore del job più lungo, altrimenti Docker ucciderà il worker a metà lavoro. È lo stesso principio di `stopwaitsecs` in Supervisor.

---

<a id="security"></a>
## Sicurezza dell'immagine

* **Niente root.** `USER www-data` in fondo al Dockerfile. Se nell'applicazione viene trovata una RCE, l'attaccante ottiene i permessi di `www-data` dentro il container, non di root. PHP-FPM ascolta sulla porta 9000, per la quale non servono privilegi. L'avviso di FPM sulla direttiva `user`, che senza root viene ignorata, è innocuo.
* **Software minimo.** Niente Xdebug, Node.js, Composer, git o compilatori nell'immagine finale. Ciò che non c'è non si può sfruttare.
* **Versioni fissate.** `php:8.5-fpm` cambia a ogni patch release. Per la riproducibilità fissate il digest: `FROM php:8.5-fpm@sha256:...`, e automatizzate gli aggiornamenti delle immagini base con Dependabot o Renovate.
* **Rebuild regolari.** Le vulnerabilità in OpenSSL o glibc si chiudono con una nuova immagine base: anche se il vostro codice non è cambiato, l'immagine va ricostruita almeno una volta ogni una o due settimane.
* **Scansione.** `docker scout cves` o `trivy image` in CI trovano le vulnerabilità note nei pacchetti di sistema e nelle dipendenze. Per le dipendenze PHP restano `composer audit` e `npm audit`.
* **Sola lettura.** A un livello successivo, il filesystem root del container si monta con `read_only: true`, lasciando scrivibili solo `storage/`, `bootstrap/cache` e `/tmp`.

Le impostazioni del server attorno ai container (header di sicurezza, TLS, limiti) sono descritte nell'articolo sull'[hardening di server e infrastruttura](../security/server-and-infrastructure-hardening).

---

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

| Opzione | Pro | Contro |
|---------|-------|--------|
| `php:8.x-fpm` (Debian) | glibc prevedibile, pacchetti precompilati veloci, meno sorprese | Immagine più grande |
| `php:8.x-fpm-alpine` | Dimensioni ridotte | musl al posto di glibc: build delle estensioni più lunga, differenze nel comportamento di alcune librerie e nelle prestazioni |
| FrankenPHP | Un solo processo: web server Caddy e PHP insieme, HTTPS, modalità worker per Laravel Octane | Un modello di esecuzione diverso: in modalità worker lo stato vive tra una richiesta e l'altra, e leak e singleton «sporchi» diventano un problema vostro |

Per un tipico progetto Laravel il punto di partenza sensato è un'immagine Debian con PHP-FPM e nginx, come in questo articolo. Alpine ha senso quando la dimensione conta davvero e avete verificato l'applicazione su musl. I modelli di esecuzione di PHP (FPM, worker, event loop) sono messi a confronto nell'articolo [PHP sul server](../php/runtimes).

---

<a id="common-mistakes"></a>
## Errori comuni

**1. Immagine di Sail o immagine di sviluppo in produzione.**
`artisan serve`, Xdebug e Node.js in ambiente di produzione: lento e insicuro.

**2. `.env` dentro l'immagine.**
Secret in ogni layer e nelle mani di chiunque possa scaricare l'immagine. Configurazione solo tramite l'ambiente.

**3. `config:cache` nel Dockerfile.**
Nell'immagine vengono cristallizzate le variabili dell'ambiente di build, e le impostazioni di produzione vengono ignorate.

**4. `COPY . .` prima di `composer install`.**
Qualsiasi modifica al codice invalida la cache delle dipendenze, e ogni build riscarica tutti i pacchetti.

**5. `--ignore-platform-reqs` in Composer.**
La build passa, ma a runtime manca un'estensione PHP. Installate le dipendenze sullo stesso PHP della produzione.

**6. Secret tramite `ARG` o `ENV`.**
I valori sono visibili in `docker history`. Usate `--mount=type=secret`.

**7. `CMD` in shell form ed entrypoint senza `exec`.**
`SIGTERM` lo riceve la shell, non PHP. Il worker non si arresta in modo graceful e viene ucciso dopo 10 secondi a metà di un job.

**8. `migrate --force` nell'entrypoint di ogni replica.**
Più container lanciano le migration contemporaneamente. Le migration sono un passo separato del rilascio.

**9. Processo eseguito come root.**
Qualsiasi vulnerabilità nell'applicazione dà subito i permessi di root dentro il container.

---

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

1. Esiste un `.dockerignore`: `.env`, `.git`, `vendor/`, `node_modules/` non finiscono nel contesto.
2. Build multi-stage: Composer e Node.js solo negli stage di build.
3. `composer.json`/`composer.lock` e `package*.json` vengono copiati prima del codice; si usano i cache mount di BuildKit.
4. Le dipendenze vengono installate sullo stesso PHP con le stesse estensioni del runtime, senza `--ignore-platform-reqs`.
5. `php.ini-production`, OPcache con `validate_timestamps=0`, `pm.max_children` calcolato in base alla memoria.
6. Nell'immagine non ci sono `.env` né secret; i token privati passano da `--mount=type=secret`.
7. `php artisan optimize` viene eseguito all'avvio del container; le migration sono un passo separato del rilascio.
8. Il processo gira come `www-data`; l'immagine base è fissata tramite digest e ricostruita regolarmente.
9. web, queue e scheduler vengono avviati dalla stessa immagine con comandi diversi.
10. Ci sono un healthcheck su `/up`, `exec` nell'entrypoint, pcntl e uno `stop_grace_period` maggiore del job più lungo.
11. L'immagine viene scansionata per le vulnerabilità in CI.

---

## Conclusione

Un'immagine di produzione per Laravel non è «Sail più COPY», ma un artefatto a sé: costruita in più stage, contiene solo ciò che serve per l'esecuzione, non sa nulla dell'ambiente specifico e gira senza root. L'ordine dei layer e la cache di BuildKit rendono la build veloce, `optimize` all'avvio recepisce la configurazione reale, e un'unica immagine per web, worker e scheduler garantisce che tutti i processi girino sullo stesso codice.

---

<a id="self-test-quiz"></a>
## Quiz di autoverifica

### Domanda 1: Perché `composer.json` e `composer.lock` vengono copiati nell'immagine separatamente e prima del resto del codice?
- A) Composer non è in grado di leggere i file copiati con il comando `COPY . .`.
- B) Perché il layer con le dipendenze installate venga riutilizzato dalla cache finché i manifest non cambiano, e le modifiche al codice non rilancino `composer install`.
- C) Lo richiede il formato della build multi-stage.

<details>
<summary><b>Mostra la risposta</b></summary>

**Risposta: B**
Docker ricostruisce un layer quando cambiano i suoi input. Se le dipendenze vengono installate dopo `COPY . .`, qualsiasi modifica a un template invalida il layer con `vendor/`. Copiare i manifest separatamente svincola l'installazione delle dipendenze dalle modifiche al codice.
</details>

### Domanda 2: Il comando `php artisan config:cache` viene eseguito nel Dockerfile. Cosa succede in produzione?
- A) Niente di particolare: Laravel rilegge le variabili d'ambiente all'avvio.
- B) L'applicazione userà i valori presenti nell'ambiente al momento della build e ignorerà le variabili passate al container.
- C) La build fallirà, perché `config:cache` non può essere eseguito senza database.

<details>
<summary><b>Mostra la risposta</b></summary>

**Risposta: B**
La configurazione in cache non rilegge `env()`. La cache va costruita all'avvio del container, quando le variabili d'ambiente sono già state passate: per esempio con `php artisan optimize` nell'entrypoint.
</details>

### Domanda 3: Con `docker stop` il worker della coda viene interrotto a metà di un job. Quale delle seguenti misure NON aiuterà?
- A) `exec "$@"` nell'entrypoint e `CMD` in exec form, così che `SIGTERM` arrivi a PHP.
- B) L'estensione pcntl installata e uno `stop_grace_period` maggiore del job più lungo.
- C) Aumentare `memory_limit` in `php.ini`.

<details>
<summary><b>Mostra la risposta</b></summary>

**Risposta: C**
L'arresto graceful dipende dalla consegna del segnale al processo PHP, dalla gestione di `SIGTERM` (pcntl) e dal tempo che Docker concede prima di `SIGKILL`. Il limite di memoria non c'entra.
</details>