# Symfony e Docker em 2026: ambiente de desenvolvimento e deploy em produção > Um fluxo completo de Symfony com Docker para 2026: ambiente de desenvolvimento com FrankenPHP, imagem de produção multi-stage, modo worker, segredos criptografados, otimização de imagem e deploys sem downtime com migrações. - Published: 2026-06-25 - Updated: 2026-07-06 - Author: SharpSkill - Tags: symfony, docker, frankenphp, php, deployment, devops - Reading time: 10 min --- Este tutorial de Symfony com Docker monta um fluxo de trabalho completo em contêineres: um ambiente de desenvolvimento local reproduzível e uma imagem de produção endurecida, ambos centrados no FrankenPHP e no Symfony 7.4 LTS. Rodar o Symfony em contêineres elimina a clássica divergência de ambiente entre notebooks e servidores, e transforma cada implantação no envio de um único artefato imutável, em vez de executar uma sequência frágil de comandos manuais no servidor. > **A stack Symfony com Docker em 2026** > > O template oficial Symfony Docker agora entrega o [FrankenPHP](https://frankenphp.dev/) como runtime, substituindo a dupla tradicional Nginx mais PHP-FPM. Um único binário serve HTTP/2 e HTTP/3 e executa o Symfony em modo worker persistente, mantendo o kernel inicializado entre as requisições em vez de reconstruí-lo a cada chamada. ## Ambiente de desenvolvimento com Docker Compose para Symfony Uma boa configuração de desenvolvimento garante a cada colaborador a mesma versão do PHP, as mesmas extensões e o mesmo banco de dados sem tocar na máquina hospedeira. O Docker Compose descreve essa stack de forma declarativa. O exemplo abaixo combina um contêiner FrankenPHP construído a partir de um `Dockerfile` local com um serviço PostgreSQL 17, conectados na rede padrão do Compose. ```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: ``` O bind mount no serviço `php` mapeia a raiz do projeto para dentro do contêiner, então as alterações de arquivos entram em vigor sem reconstruir a imagem. O FrankenPHP gera um certificado TLS local na primeira inicialização, e é por isso que a porta 443 fica exposta e `caddy_data` é um volume nomeado: o certificado sobrevive ao `docker compose down`. A dica `serverVersion=17` no DSN permite ao Doctrine pular uma consulta de detecção de versão a cada conexão. Com a stack no ar, todo comando do Symfony é executado dentro do contêiner `php`, e não no host, o que garante a versão e as extensões corretas do PHP. Criar um banco, rodar migrações ou abrir um shell seguem o mesmo padrão através do `docker compose exec`. > **Executando comandos do console no contêiner** > > Prefixe as chamadas ao Symfony e ao Composer com `docker compose exec php` para executá-las contra o runtime PHP do contêiner, por exemplo `docker compose exec php bin/console make:entity` ou `docker compose exec php composer require symfony/uid`. O host nunca precisa ter o PHP instalado. ## Dockerfile multi-stage para produção com Symfony Um [Dockerfile multi-stage](https://docs.docker.com/build/building/multi-stage/) compartilha uma base comum e então se divide em um alvo de desenvolvimento e um alvo de produção. O estágio de produção instala apenas as dependências de runtime, descarta os pacotes de desenvolvimento do Composer e aquece o cache em tempo de build para que o contêiner em execução inicie instantaneamente. ```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 ``` Copiar o `composer.json` e os arquivos de lock antes do restante do código-fonte é a otimização central: o Docker mantém em cache a camada de instalação de dependências e só a reexecuta quando os manifestos mudam, não a cada edição de código. A flag `--classmap-authoritative` informa ao Composer que o classmap está completo, de modo que o autoloader nunca recorre a buscas no sistema de arquivos em runtime. Aquecer o cache durante o build faz com que a primeira requisição de produção chegue a um contêiner totalmente compilado. ## Modo worker do FrankenPHP e o Runtime do Symfony O PHP-FPM tradicional inicializa o kernel do Symfony, trata uma requisição e descarta tudo. O modo worker mantém o kernel e o contêiner de injeção de dependências vivos entre as requisições, e é daí que vem a maior parte da economia de latência. O pacote `runtime/frankenphp-symfony` viabiliza isso através do componente Symfony Runtime e do bootstrap padrão `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']); }; ``` A linha `FRANKENPHP_CONFIG="worker ./public/index.php"` definida no Dockerfile ativa esse loop. Como o contêiner é reutilizado, qualquer serviço que guarde estado específico da requisição precisa se redefinir entre as chamadas. O Symfony trata os serviços do framework automaticamente, mas serviços customizados com estado devem implementar `Symfony\Contracts\Service\ResetInterface` e receber a tag `kernel.reset`, para que o estado acumulado não vaze para a próxima requisição. Essa é a mesma disciplina exigida pelos [workers do Messenger](/blog/symfony/symfony-messenger-queues-workers-async-architecture) de longa duração, e o ganho é uma vazão de requisições várias vezes maior que a do PHP-FPM no mesmo hardware. Para o desenvolvimento local, rodar o FrankenPHP com a flag `--watch` reinicia o worker automaticamente quando os arquivos mudam, de modo que o modo worker não atrapalha o ciclo de edição e recarga. ## FrankenPHP versus PHP-FPM no Symfony A escolha entre o FrankenPHP e a stack clássica Nginx mais PHP-FPM molda tanto o Dockerfile quanto o comportamento em runtime. A tabela abaixo resume as diferenças práticas para uma configuração de produção com Symfony. | Aspecto | FrankenPHP em modo worker | Nginx + PHP-FPM | |--------|------------------------|-----------------| | Contêineres | Um | Dois (servidor web + PHP) | | Boot do kernel | Uma vez por worker | Uma vez por requisição | | HTTP/3 | Nativo | Exige configuração extra | | TLS | Automático (Caddy) | Configuração manual de certificado | | Gestão de estado | Precisa redefinir entre requisições | Isolado por requisição | | Custo de cold-start | Pago uma vez no boot | Pago a cada requisição | O modo worker vence em vazão porque amortiza a cara etapa de boot do kernel e de compilação do contêiner ao longo de milhares de requisições. O PHP-FPM continua relevante quando uma aplicação depende de globais ou de bibliotecas de terceiros que assumem um processo novo por requisição, já que essas quebram sob um worker reutilizado. > **Vazamento de estado no modo worker** > > Um serviço que armazena dados de requisição em uma propriedade privada servirá os dados de uma requisição para a seguinte a menos que se redefina. Audite singletons, event subscribers e qualquer coisa que segure um token de segurança ou a requisição atual antes de migrar para o modo worker em produção. ## Gerenciando variáveis de ambiente e segredos em produção O Symfony lê a configuração a partir de variáveis de ambiente, e há duas maneiras sólidas de fornecê-las em produção. A primeira é injetar variáveis simples através do orquestrador ou do arquivo Compose. A segunda é o cofre de segredos criptografados do Symfony, que armazena valores sensíveis no repositório como texto cifrado e os descriptografa em runtime com uma única chave privada. ```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)" ``` A etapa `composer dump-env prod` no Dockerfile compila os arquivos `.env` em um único `.env.local.php` otimizado, o que remove o custo de runtime de fazer o parse dos arquivos dotenv no boot. Qualquer variável definida no ambiente real do contêiner ainda sobrepõe os padrões compilados, então os segredos fornecidos pelo orquestrador sempre prevalecem. O [guia de implantação do Symfony](https://symfony.com/doc/current/deployment.html) documenta a ordem completa de precedência, mas a regra prática é simples: nunca embuta `APP_SECRET` ou credenciais de banco na imagem, e injete-os no início do contêiner. ## Otimizando a imagem de produção do Symfony Duas configurações do OPcache respondem pela maior parte da diferença de desempenho em produção: desativar a validação de timestamps e habilitar o preloading. Como uma imagem de contêiner é imutável, o código-fonte nunca muda em runtime, então o OPcache nunca deveria consultar arquivos para checar edições. O preloading vai além, carregando as classes do Symfony e da aplicação na memória compartilhada uma única vez na inicialização do servidor. ```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 ``` O Symfony gera o arquivo `preload.php` listado acima durante o `cache:warmup`, então o arquivo já existe na imagem após o build. O [preloading do OPcache](https://www.php.net/manual/en/opcache.preloading.php) vincula essas classes na inicialização e pula a etapa de compilação na primeira requisição que precisar delas. Definir `validate_timestamps=0` só é seguro para implantações imutáveis, em que uma nova release significa uma nova imagem; em um host mutável isso serviria código desatualizado. Dimensionar `max_accelerated_files` acima da contagem real de arquivos mantém todo o framework em cache sem despejo. ## Reduzindo o tamanho da imagem Symfony com .dockerignore A ausência de um arquivo `.dockerignore` incha o build silenciosamente. Sem ele, a etapa `COPY . .` envia o diretório `vendor` local, o `var/cache` do desenvolvimento, a saída de build do node e o histórico do `.git` direto para dentro da imagem e do contexto de build. Excluí-los encolhe a imagem, acelera o upload do contexto de build para o daemon e evita que artefatos de desenvolvimento sobrescrevam as dependências de produção recém-instaladas. ```gitignore # .dockerignore /.git/ /vendor/ /node_modules/ /var/ /.env.local /.env.*.local /tests/ /docker/ compose*.yaml Dockerfile ``` Ignorar `/vendor/` é o que mais importa: o estágio de produção roda `composer install --no-dev`, então enviar o diretório vendor de desenvolvimento do host anularia completamente essa etapa. Excluir `/var/` mantém o cache e os logs de desenvolvimento fora da imagem, permitindo que o `cache:warmup` em tempo de build produza um cache de produção limpo. Combinada com o build multi-stage e as extensões baseadas em Alpine, uma imagem Symfony enxuta costuma ficar bem abaixo de 150 MB. ## Implantando contêineres Symfony e executando migrações O arquivo Compose de produção referencia uma imagem pré-construída de um registry, em vez de construir localmente, e define um health check para que o orquestrador só direcione tráfego a um contêiner que responde. Os segredos chegam como variáveis de ambiente, nunca como camadas da imagem. ```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 ``` O health check faz um curl em uma rota `/health`, então a aplicação precisa expor uma. Um controller mínimo que retorna 200 sem tocar no banco de dados mantém a verificação rápida e evita marcar o contêiner como não saudável durante uma instabilidade transitória do banco. Quando a prontidão do banco importa, uma segunda sonda mais profunda pode rodar uma consulta leve, mas o endpoint de liveness continua trivial. ```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']); } } ``` As migrações de banco de dados devem rodar como uma etapa distinta antes de os novos contêineres receberem tráfego, e não dentro do boot da aplicação. Executá-las em um contêiner efêmero garante que o esquema fique atualizado exatamente uma vez por release, mesmo quando várias réplicas da aplicação iniciam em paralelo. ```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 ``` A flag `--wait` bloqueia até que todo serviço reporte estado saudável, dando ao script de deploy um sinal de sucesso real em vez de um início do tipo "dispare e esqueça". Combinar isso com uma atualização gradual em um orquestrador como Kubernetes ou Docker Swarm resulta em releases sem downtime: os contêineres antigos continuam servindo até os novos passarem no health check. Para uma visão mais ampla das versões do framework que esse fluxo mira, o [panorama da plataforma Symfony](/technologies/symfony) e o [guia de recursos do Symfony 8 e PHP 8.4](/blog/symfony/symfony-8-new-features-php-84-lazy-objects) cobrem o que mudou na linha de release atual. ## Conclusão - Use um Dockerfile multi-stage para que as imagens de desenvolvimento e de produção compartilhem uma base enquanto o alvo de produção é entregue sem as dependências de desenvolvimento do Composer - Copie o `composer.json` e os arquivos de lock antes do código-fonte para manter a camada de instalação de dependências em cache através das mudanças de código - Rode o FrankenPHP em modo worker para reutilizar o kernel inicializado entre as requisições, e redefina serviços com estado usando `ResetInterface` para evitar vazamentos de estado - Aqueça o cache do Symfony e compile o `.env` com `dump-env prod` em tempo de build para que o contêiner em execução inicie totalmente inicializado - Habilite o preloading do OPcache e defina `validate_timestamps=0` em produção, o que é seguro justamente porque a imagem é imutável - Mantenha `APP_SECRET`, as credenciais de banco e a chave de descriptografia do cofre fora das camadas da imagem; injete-os como variáveis de ambiente no início do contêiner - Aplique as migrações do Doctrine em um contêiner efêmero antes de as novas réplicas receberem tráfego, e condicione o rollout ao health check com `up -d --wait` --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/pt/blog/symfony/symfony-docker-development-production