Symfony e Docker nel 2026: ambiente di sviluppo e deploy in produzione
Un workflow completo Symfony Docker per il 2026: ambiente di sviluppo FrankenPHP, immagine di produzione multi-stage, worker mode, segreti cifrati, ottimizzazione dell'immagine e deploy senza downtime con migrazioni.

Questo tutorial su Symfony e Docker costruisce un workflow a container completo: un ambiente di sviluppo locale riproducibile e un'immagine di produzione irrobustita, entrambi imperniati su FrankenPHP e Symfony 7.4 LTS. Eseguire Symfony dentro i container elimina il classico scarto di ambiente tra portatili e server, e trasforma ogni rilascio nella spedizione di un singolo artefatto immutabile anziché nell'esecuzione di una fragile sequenza di comandi manuali sul server.
Il template ufficiale di Symfony Docker adotta ora FrankenPHP come runtime, sostituendo la tradizionale coppia Nginx più PHP-FPM. Un unico binario serve HTTP/2 e HTTP/3 ed esegue Symfony in worker mode persistente, mantenendo il kernel avviato tra una richiesta e l'altra invece di ricostruirlo a ogni chiamata.
Ambiente di sviluppo Docker Compose per Symfony
Una buona configurazione di sviluppo offre a ogni collaboratore la stessa versione di PHP, le stesse estensioni e lo stesso database senza toccare la macchina host. Docker Compose descrive quello stack in modo dichiarativo. L'esempio seguente affianca un container FrankenPHP costruito da un Dockerfile locale a un servizio PostgreSQL 17, collegati tra loro sulla rete Compose predefinita.
# compose.yaml
services:
php:
build:
context: .
target: frankenphp_dev
ports:
- "80:80"
- "443:443"
volumes:
- ./:/app # bind mount: edits on the host appear instantly
- caddy_data:/data # persist TLS certificates across restarts
environment:
DATABASE_URL: "postgresql://app:app@database:5432/app?serverVersion=17"
depends_on:
- database
database:
image: postgres:17-alpine
environment:
POSTGRES_DB: app
POSTGRES_USER: app
POSTGRES_PASSWORD: app
volumes:
- db_data:/var/lib/postgresql/data
volumes:
caddy_data:
db_data:Il bind mount sul servizio php mappa la radice del progetto dentro il container, così che le modifiche ai file abbiano effetto senza ricostruire l'immagine. FrankenPHP genera un certificato TLS locale al primo avvio: per questo la porta 443 è esposta e caddy_data è un volume nominato, in modo che il certificato sopravviva a docker compose down. L'indicazione serverVersion=17 nel DSN consente a Doctrine di saltare una query di rilevamento versione a ogni connessione.
Una volta avviato lo stack, ogni comando Symfony viene eseguito dentro il container php anziché sull'host, il che garantisce la versione e le estensioni di PHP corrette. Creare un database, lanciare le migrazioni o aprire una shell seguono tutti lo stesso schema tramite docker compose exec.
Anteponi docker compose exec php alle chiamate a Symfony e Composer per eseguirle sul runtime PHP del container, per esempio docker compose exec php bin/console make:entity oppure docker compose exec php composer require symfony/uid. L'host non ha mai bisogno di PHP installato.
Dockerfile multi-stage per la produzione con Symfony
Un Dockerfile multi-stage condivide una base comune e poi si dirama in un target di sviluppo e in un target di produzione. Lo stage di produzione installa solo le dipendenze di runtime, rimuove i pacchetti dev di Composer e scalda la cache in fase di build, così che il container in esecuzione si avvii istantaneamente.
# Dockerfile
FROM dunglas/frankenphp:1-php8.4 AS base
WORKDIR /app
# Install the PHP extensions Symfony relies on
RUN install-php-extensions \
intl \
opcache \
pdo_pgsql \
zip
COPY /composer /usr/bin/composer
# --- Development target ---
FROM base AS frankenphp_dev
ENV APP_ENV=dev
RUN mv "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini"
# --- Production target ---
FROM base AS frankenphp_prod
ENV APP_ENV=prod
ENV FRANKENPHP_CONFIG="worker ./public/index.php"
ENV APP_RUNTIME="Runtime\FrankenPhpSymfony\Runtime"
RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini"
# Layer caching: copy manifests first, install, then copy the source
COPY composer.json composer.lock symfony.lock ./
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist
COPY . .
RUN composer dump-autoload --no-dev --classmap-authoritative \
&& composer dump-env prod \
&& bin/console cache:warmupCopiare composer.json e i file di lock prima del resto del sorgente è l'ottimizzazione chiave: Docker mette in cache il layer di installazione delle dipendenze e lo riesegue solo quando cambiano i manifest, non a ogni modifica del codice. Il flag --classmap-authoritative dice a Composer che la classmap è completa, così l'autoloader non ricade mai su ricerche nel filesystem a runtime. Scaldare la cache durante la build fa sì che la prima richiesta in produzione colpisca un container già compilato per intero.
FrankenPHP worker mode e il Runtime di Symfony
Il tradizionale PHP-FPM avvia il kernel Symfony, gestisce una richiesta e butta via tutto. Il worker mode mantiene vivi il kernel e il container di dependency injection attraverso le richieste, ed è da qui che arriva la maggior parte del risparmio di latenza. Il pacchetto runtime/frankenphp-symfony rende possibile tutto ciò tramite il componente Runtime di Symfony e il bootstrap standard public/index.php.
use App\Kernel;
require_once dirname(__DIR__).'/vendor/autoload_runtime.php';
return function (array $context) {
return new Kernel($context['APP_ENV'], (bool) $context['APP_DEBUG']);
};La riga FRANKENPHP_CONFIG="worker ./public/index.php" impostata nel Dockerfile attiva questo loop. Poiché il container viene riutilizzato, qualsiasi servizio che conservi stato specifico della richiesta deve azzerarsi tra una richiesta e l'altra. Symfony gestisce automaticamente i servizi del framework, ma i servizi stateful personalizzati dovrebbero implementare Symfony\Contracts\Service\ResetInterface ed essere contrassegnati con kernel.reset, così che lo stato accumulato non trapeli nella richiesta successiva. È la stessa disciplina richiesta dai worker Messenger a lunga esecuzione, e la ricompensa è un throughput di richieste diverse volte superiore a PHP-FPM sullo stesso hardware.
In fase di sviluppo locale, eseguire FrankenPHP con il flag --watch riavvia automaticamente il worker quando i file cambiano, così il worker mode non si intromette nel ciclo modifica-aggiornamento.
FrankenPHP contro PHP-FPM in Symfony
La scelta tra FrankenPHP e il classico stack Nginx più PHP-FPM plasma sia il Dockerfile sia il comportamento a runtime. La tabella seguente riassume le differenze pratiche per una configurazione di produzione Symfony.
| Aspetto | FrankenPHP worker mode | Nginx + PHP-FPM | |--------|------------------------|-----------------| | Container | Uno | Due (web server + PHP) | | Avvio del kernel | Una volta per worker | Una volta per richiesta | | HTTP/3 | Integrato | Richiede configurazione extra | | TLS | Automatico (Caddy) | Impostazione manuale del certificato | | Gestione dello stato | Da azzerare tra le richieste | Isolato per richiesta | | Costo di cold start | Pagato una volta all'avvio | Pagato a ogni richiesta |
Il worker mode vince sul throughput perché ammortizza il costoso passo di avvio del kernel e compilazione del container su migliaia di richieste. PHP-FPM resta rilevante quando un'applicazione si basa su variabili globali o su librerie di terze parti che presuppongono un processo fresco per ogni richiesta, dato che queste si rompono sotto un worker riutilizzato.
Un servizio che memorizza dati della richiesta in una proprietà privata servirà i dati di una richiesta alla successiva a meno che non si azzeri. Verifica singleton, event subscriber e qualunque cosa contenga un token di sicurezza o la richiesta corrente prima di passare al worker mode in produzione.
Pronto a superare i tuoi colloqui su Symfony?
Pratica con i nostri simulatori interattivi, flashcards e test tecnici.
Gestire variabili d'ambiente e segreti in produzione
Symfony legge la configurazione dalle variabili d'ambiente, e ci sono due modi validi per fornirle in produzione. Il primo è iniettare variabili semplici tramite l'orchestratore o il file Compose. Il secondo è il vault dei segreti cifrati di Symfony, che conserva i valori sensibili nel repository come ciphertext e li decifra a runtime con un'unica chiave privata.
# Generate the keypair; commit the public key, keep the private key out of the image
php bin/console secrets:generate-keys
# Encrypt a value into config/secrets/prod/
php bin/console secrets:set DATABASE_URL
# In production, expose only the decryption key to the container
export SYMFONY_DECRYPTION_SECRET="$(cat config/secrets/prod/prod.decrypt.private.php)"Il passo composer dump-env prod nel Dockerfile compila i file .env in un unico .env.local.php ottimizzato, il che elimina il costo a runtime del parsing dei file dotenv all'avvio. Qualunque variabile impostata nell'ambiente reale del container continua comunque a prevalere sui default compilati, così i segreti forniti dall'orchestratore vincono sempre. La guida al deployment di Symfony documenta l'intero ordine di precedenza, ma la regola pratica è semplice: non incorporare mai APP_SECRET o le credenziali del database nell'immagine, e iniettarli all'avvio del container.
Ottimizzare l'immagine di produzione di Symfony
Due impostazioni di OPcache spiegano la maggior parte del divario di prestazioni in produzione: disattivare la validazione dei timestamp e abilitare il preloading. Poiché un'immagine di container è immutabile, il sorgente non cambia mai a runtime, quindi OPcache non dovrebbe mai eseguire uno stat dei file per controllare le modifiche. Il preloading si spinge oltre caricando le classi di Symfony e dell'applicazione in memoria condivisa una sola volta all'avvio del server.
; docker/php/prod.ini
opcache.enable=1
opcache.preload=/app/var/cache/prod/App_KernelProdContainer.preload.php
opcache.preload_user=root
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; immutable image: never re-check the filesystem
realpath_cache_size=4096K
realpath_cache_ttl=600Symfony genera il file preload.php indicato sopra durante cache:warmup, quindi il file esiste già nell'immagine dopo la build. Il preloading di OPcache collega queste classi all'avvio e salta il passo di compilazione alla prima richiesta che ne ha bisogno. Impostare validate_timestamps=0 è sicuro solo per deployment immutabili in cui una nuova release significa una nuova immagine; su un host mutabile servirebbe codice obsoleto. Dimensionare max_accelerated_files al di sopra del conteggio reale dei file mantiene l'intero framework in cache senza espulsioni.
Ridurre le dimensioni dell'immagine Symfony con .dockerignore
Un file .dockerignore mancante gonfia silenziosamente la build. Senza di esso, il passo COPY . . spedisce nell'immagine e nel contesto di build la directory vendor locale, la var/cache dello sviluppo, l'output di build di node e la cronologia .git. Escluderli riduce l'immagine, velocizza l'upload del contesto di build al daemon e impedisce agli artefatti di sviluppo di sovrascrivere le dipendenze di produzione appena installate.
# .dockerignore
/.git/
/vendor/
/node_modules/
/var/
/.env.local
/.env.*.local
/tests/
/docker/
compose*.yaml
DockerfileIgnorare /vendor/ è ciò che conta di più: lo stage di produzione esegue composer install --no-dev, quindi spedire la directory vendor dell'host, orientata allo sviluppo, vanificherebbe del tutto quel passo. Escludere /var/ tiene fuori dall'immagine la cache e i log di sviluppo, lasciando che il cache:warmup in fase di build produca una cache di produzione pulita. Combinata con la build multi-stage e le estensioni basate su Alpine, un'immagine Symfony snella si assesta di norma ben al di sotto dei 150 MB.
Deploy dei container Symfony ed esecuzione delle migrazioni
Il file Compose di produzione fa riferimento a un'immagine pre-costruita da un registry anziché costruirla localmente, e definisce un health check così che l'orchestratore instradi il traffico solo verso un container che risponde. I segreti arrivano come variabili d'ambiente, mai come layer dell'immagine.
# compose.prod.yaml
services:
php:
image: registry.example.com/app:${TAG}
environment:
APP_ENV: prod
APP_SECRET: ${APP_SECRET}
DATABASE_URL: ${DATABASE_URL}
healthcheck:
test: ["CMD", "curl", "-fsS", "http://localhost/health"]
interval: 10s
timeout: 3s
retries: 3
restart: unless-stoppedL'health check esegue una curl su una rotta /health, quindi l'applicazione deve esporne una. Un controller minimale che restituisce un 200 senza toccare il database mantiene il controllo veloce ed evita di marcare il container come non integro durante un momentaneo intoppo del database. Quando conta la prontezza del database, una seconda sonda più profonda può eseguire una query leggera, ma l'endpoint di liveness resta banale.
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
final class HealthController extends AbstractController
{
#[Route('/health', name: 'health', methods: ['GET'])]
public function __invoke(): JsonResponse
{
return new JsonResponse(['status' => 'ok']);
}
}Le migrazioni del database dovrebbero girare come passo distinto prima che i nuovi container ricevano traffico, non dentro il boot dell'applicazione. Eseguirle in un container usa e getta garantisce che lo schema sia aggiornato esattamente una volta per release, anche quando diverse repliche dell'applicazione si avviano in parallelo.
# deploy.sh
set -euo pipefail
docker compose -f compose.prod.yaml pull
# Apply migrations once, in an isolated container
docker compose -f compose.prod.yaml run --rm php \
bin/console doctrine:migrations:migrate --no-interaction --allow-no-migration
# Start replicas and wait for the health check to pass
docker compose -f compose.prod.yaml up -d --waitIl flag --wait blocca l'esecuzione finché ogni servizio non si dichiara integro, dando allo script di deploy un vero segnale di successo invece di un avvio fire-and-forget. Abbinare questo a un aggiornamento rolling in un orchestratore come Kubernetes o Docker Swarm produce rilasci senza downtime: i vecchi container continuano a servire finché i nuovi non superano il loro health check. Per una panoramica più ampia delle versioni del framework a cui questo workflow si rivolge, la panoramica della piattaforma Symfony e la guida alle novità di Symfony 8 e PHP 8.4 illustrano cosa è cambiato nella linea di release attuale.
Conclusione
- Usa un Dockerfile multi-stage così che le immagini di sviluppo e di produzione condividano una sola base mentre il target di produzione viene spedito senza le dipendenze dev di Composer
- Copia
composer.jsone i file di lock prima del sorgente per mantenere il layer di installazione delle dipendenze in cache attraverso le modifiche al codice - Esegui FrankenPHP in worker mode per riutilizzare il kernel già avviato attraverso le richieste, e azzera i servizi stateful con
ResetInterfaceper prevenire le fughe di stato - Scalda la cache di Symfony e compila
.envcondump-env prodin fase di build così che il container in esecuzione si avvii completamente inizializzato - Abilita il preloading di OPcache e imposta
validate_timestamps=0in produzione, cosa sicura proprio perché l'immagine è immutabile - Tieni
APP_SECRET, le credenziali del database e la chiave di decifratura del vault fuori dai layer dell'immagine; iniettali come variabili d'ambiente all'avvio del container - Applica le migrazioni Doctrine in un container usa e getta prima che le nuove repliche ricevano traffico, e vincola il rollout all'health check con
up -d --wait
Inizia a praticare!
Metti alla prova le tue conoscenze con i nostri simulatori di colloquio e test tecnici.
Tag
Condividi
Articoli correlati

Doctrine ORM: Padroneggiare le relazioni in Symfony
Guida completa alle relazioni Doctrine ORM in Symfony. OneToMany, ManyToMany, strategie di caricamento e ottimizzazione delle prestazioni con esempi pratici.

Domande di colloquio Symfony: Top 25 nel 2026
Le 25 domande di colloquio Symfony più frequenti. Architettura, Doctrine ORM, servizi, sicurezza, form e test con risposte dettagliate ed esempi di codice.

Symfony 7: API Platform e Best Practices
Guida completa ad API Platform 4 con Symfony 7: State Processors, State Providers, gruppi di serializzazione, filtri, sicurezza e test automatizzati per API REST professionali.