# 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. - Published: 2026-06-25 - Updated: 2026-07-06 - Author: SharpSkill - Tags: symfony, docker, frankenphp, php, deployment, devops - Reading time: 10 min --- 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. > **Lo stack Symfony Docker del 2026** > > Il template ufficiale di Symfony Docker adotta ora [FrankenPHP](https://frankenphp.dev/) 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. ```yaml # 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`. > **Eseguire i comandi della console nel container** > > 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](https://docs.docker.com/build/building/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 # 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 --from=composer/composer:2-bin /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:warmup ``` Copiare `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`. ```php // 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](/blog/symfony/symfony-messenger-queues-workers-async-architecture) 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. > **Fughe di stato nel worker mode** > > 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. ## 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. ```bash # 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](https://symfony.com/doc/current/deployment.html) 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. ```ini ; 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=600 ``` Symfony genera il file `preload.php` indicato sopra durante `cache:warmup`, quindi il file esiste già nell'immagine dopo la build. Il [preloading di OPcache](https://www.php.net/manual/en/opcache.preloading.php) 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. ```gitignore # .dockerignore /.git/ /vendor/ /node_modules/ /var/ /.env.local /.env.*.local /tests/ /docker/ compose*.yaml Dockerfile ``` Ignorare `/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. ```yaml # 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-stopped ``` L'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. ```php // src/Controller/HealthController.php 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. ```bash # 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 --wait ``` Il 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](/technologies/symfony) e la [guida alle novità di Symfony 8 e PHP 8.4](/blog/symfony/symfony-8-new-features-php-84-lazy-objects) 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.json` e 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 `ResetInterface` per prevenire le fughe di stato - Scalda la cache di Symfony e compila `.env` con `dump-env prod` in fase di build così che il container in esecuzione si avvii completamente inizializzato - Abilita il preloading di OPcache e imposta `validate_timestamps=0` in 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` --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/it/blog/symfony/symfony-docker-development-production