Docker para PHP em produção: um guia prático de configuração para aplicativos de negócios
Docker faz um aplicativo PHP rodar da mesma maneira em um laptop, em testes e em produção, e torna os lançamentos e reversões rotineiros em vez de estressantes. Ele também adiciona peças móveis e uma configuração descuidada é pior do que um servidor simples. Este guia mostra uma linha de base pronta para produção com configuração real e quando você não deve se preocupar.
Em resumo
- Docker vale a pena quando você deseja ambientes idênticos, reversões rápidas e integração fácil. Para um único site de folheto pequeno sobre hospedagem compartilhada, geralmente é desnecessário.
- Construa uma imagem imutável no CI: construção em vários estágios, apenas dependências de produção, usuário não root, OPcache ativado, erros registrados e nunca exibidos.
- Liberar por tag de imagem (o hash de commit do Git), verificar a integridade após o início e reverter para a tag anterior automaticamente se a verificação falhar.
- Os contêineres são descartáveis, os dados não: mantenha o banco de dados e os uploads em volumes ou serviços gerenciados e faça backup deles fora Docker.
- Os erros de produção mais comuns: segredos na imagem, portas de banco de dados publicadas, logs ilimitados e limites de php-fpm que não correspondem à memória que você possui.
Para muitas aplicações empresariais, o custo real da hospedagem não é o servidor, mas as pessoas. A configuração que apenas um desenvolvedor entende, uma implantação feita manualmente por FTP e um ambiente de teste que se comporta de maneira diferente da produção são caros no longo prazo. Os contêineres resolvem isso empacotando o aplicativo com sua versão e extensões exatas PHP. Executamos nossos próprios sites PHP e sistemas clientes em uma pilha baseada em Docker, e este guia descreve a linha de base a partir da qual partimos.
Quando Docker vale a pena e quando não vale
- Vale a pena: vários serviços (web, PHP, banco de dados, fila), mais de um desenvolvedor, necessidade de lançamentos repetíveis e reversões rápidas ou um plano para migrar entre provedores.
- Não é necessário: um site de conteúdo simples que raramente muda na hospedagem gerenciada. A complexidade adicional precisa de uma recompensa.
- Não substitui boas operações: você ainda precisa de patches, backups e monitoramento. Consulte nosso guia de backup e monitoramento.
A arquitetura alvo
Uma configuração simples e sólida para um aplicativo PHP em um servidor:
- Proxy reverso e TLS na borda ( Nginx ou Caddy no host, ou um CDN como Cloudflare, consulte nosso guia de configuração Cloudflare).
- Contêiner da Web ( Nginx) que atende arquivos estáticos e transmite solicitações PHP.
- Contêiner PHP-FPM com seu código, embutido na imagem.
- Banco de dados em seu próprio contêiner com volume persistente, ou melhor, um serviço de banco de dados gerenciado para dados críticos.
- Volumes apenas para dados que devem sobreviver: arquivos de banco de dados e uploads de usuários.
Um Dockerfile de vários estágios
Uma compilação de vários estágios instala dependências em um estágio e copia apenas o resultado em uma imagem de tempo de execução reduzida. A imagem final não possui Composer, compiladores ou ferramentas de teste, o que a torna menor e dá menos uso aos invasores:
# ---- stage 1: PHP dependencies ----
FROM composer:2 AS vendor
WORKDIR /app
COPY composer.json composer.lock ./
RUN composer install --no-dev --no-scripts --prefer-dist --no-interaction --optimize-autoloader
# ---- stage 2: runtime ----
FROM php:8.3-fpm-alpine
RUN apk add --no-cache icu-libs libzip \
&& apk add --no-cache --virtual .build icu-dev libzip-dev $PHPIZE_DEPS \
&& docker-php-ext-install -j"$(nproc)" pdo_mysql intl zip opcache \
&& apk del .build
COPY docker/php.ini /usr/local/etc/php/conf.d/zz-app.ini
COPY docker/www.conf /usr/local/etc/php-fpm.d/zz-www.conf
WORKDIR /var/www/app
COPY --from=vendor /app/vendor ./vendor
COPY --chown=www-data:www-data . .
USER www-data
EXPOSE 9000
Escolha a versão PHP deliberadamente e verifique se ela ainda é compatível. Nosso guia de atualização PHP 8 explica como planejar isso. Fixe versões de imagens base e atualize-as de acordo com um cronograma, pois uma imagem que você nunca reconstrói acumula vulnerabilidades conhecidas. Mantenha um .dockerignore que exclua .git, .env, testes e dados locais, para que nada sensível acabe na imagem.
Configuração PHP de produção e php-fpm
# docker/php.ini
expose_php = 0
display_errors = 0
log_errors = 1
memory_limit = 256M
upload_max_filesize = 10M
post_max_size = 12M
opcache.enable = 1
opcache.memory_consumption = 128
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.validate_timestamps = 0 ; the code in an image never changes while it runs
# docker/www.conf
[www]
pm = dynamic
pm.max_children = 12
pm.start_servers = 3
pm.min_spare_servers = 2
pm.max_spare_servers = 5
pm.max_requests = 500
request_terminate_timeout = 30s
catch_workers_output = yes
decorate_workers_output = no
Definir opcache.validate_timestamps = 0 oferece o melhor desempenho para uma imagem imutável, mas só funciona se você nunca editar arquivos dentro de um contêiner em execução: em vez disso, você libera uma nova imagem. Dimensione o pool a partir de números reais: meça a memória média de um trabalhador PHP sob carga, decida quanta memória PHP pode usar no servidor e divida. Se um trabalhador usa 60 MB e você pode fornecer 1 GB PHP, pm.max_children tem cerca de 16. Definir um valor muito mais alto apenas faz a troca do servidor ou os processos de eliminação do kernel durante um pico de tráfego. Muito baixo e fila de solicitações. Verifique o log do php-fpm para avisos de "max_children alcançado".
Um arquivo Compose com os padrões corretos
x-logging: &default-logging
driver: json-file
options: { max-size: "10m", max-file: "5" } # logs cannot fill the disk
services:
web:
image: ghcr.io/your-org/app-web:${TAG}
ports: ["127.0.0.1:8080:80"] # only the local reverse proxy can reach it
depends_on: [php]
restart: unless-stopped
logging: *default-logging
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1/health.php"]
interval: 15s
timeout: 3s
retries: 3
php:
image: ghcr.io/your-org/app:${TAG}
env_file: .env # secrets stay on the server, not in the image
restart: unless-stopped
read_only: true
tmpfs: [/tmp]
mem_limit: 512m
volumes: [uploads:/var/www/app/storage/uploads]
logging: *default-logging
db:
image: mysql:8.4
env_file: .env.db
restart: unless-stopped
volumes: [db_data:/var/lib/mysql] # no "ports:" - the database is not public
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"]
interval: 15s
retries: 5
logging: *default-logging
volumes:
uploads:
db_data:
A imagem web é uma pequena imagem Nginx com sua pasta public/ e configuração do site copiadas, construída no mesmo pipeline da imagem PHP. Pontos principais: vincular portas publicadas a 127.0.0.1, nunca publicar o banco de dados, definir políticas de reinicialização, limitar a memória para que um contêiner não possa derrubar o host e limitar o tamanho do log. Lembre-se de que as portas publicadas pelo Docker podem contornar as regras do firewall do host, conforme explicamos no guia Cloudflare. Nunca monte o soquete Docker em um contêiner de aplicativo, pois ele dá a esse contêiner controle sobre o host.
Lançamentos e reversões
A melhor característica de uma configuração baseada em imagem é que cada versão é uma tag e uma reversão é "iniciar a tag anterior". Marque imagens com o hash de commit do Git, nunca confie em latest e faça com que o script de lançamento verifique a integridade:
#!/usr/bin/env bash
# /srv/app/release.sh <git-sha>
set -euo pipefail
TAG="${1:?usage: release.sh <git-sha>}"
cd /srv/app
PREV="$(cat .current_tag 2>/dev/null || true)"
export TAG
docker compose pull php web
docker compose up -d --no-deps php web
for i in $(seq 1 30); do
if curl -fsS http://127.0.0.1:8080/health.php > /dev/null; then
echo "$TAG" > .current_tag
echo "healthy: $TAG"; exit 0
fi
sleep 2
done
echo "unhealthy after release, rolling back to ${PREV:-none}" >&2
if [ -n "$PREV" ]; then TAG="$PREV" docker compose up -d --no-deps php web; fi
exit 1
As alterações no banco de dados precisam de cuidados: escreva migrações para que a versão anterior do código ainda funcione com o novo esquema (adicione colunas primeiro, remova-as em uma versão posterior). É isso que torna uma reversão rápida segura. Faça um backup do banco de dados antes de qualquer versão que altere o esquema.
Construção e envio da CI
Crie a imagem uma vez no CI e implante exatamente esse artefato. Um fluxo de trabalho mínimo GitHub Actions:
name: build-and-release
on:
push:
branches: [main]
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/your-org/app:${{ github.sha }}
- name: Release over SSH
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
run: |
install -m 600 /dev/null key && echo "$DEPLOY_KEY" > key
ssh -i key -o StrictHostKeyChecking=accept-new deploy@your-server "/srv/app/release.sh ${{ github.sha }}"
Execute seus testes e análises estáticas no mesmo pipeline antes da criação da imagem e use uma chave de implantação que só pode executar o script de lançamento. Adicione uma verificação de vulnerabilidade de imagem se seu perfil de risco exigir.
Logs, dados e segurança
- Os logs vão para stdout e stderr, são rotacionados pelo Docker e, para qualquer coisa importante, enviados para um local central para que sobrevivam ao contêiner.
- Os dados residem fora do contêiner. Um volume não é um backup. Use a abordagem em nosso guia de backup: criptografado, externo e testado para restauração.
- Os segredos nunca são incorporados às imagens. Passe-os em tempo de execução por meio de arquivos env ou de um gerenciador de segredos e mantenha-os fora do Git.
- Execute como um usuário não root, use um sistema de arquivos somente leitura sempre que possível e descarte o que não for necessário.
- Mantenha as imagens atualizadas. Reconstrua regularmente para obter correções de segurança na imagem base e PHP.
- Aplique os controles no nível do aplicativo da nossa lista de verificação de segurança PHP, pois os contêineres não corrigem códigos inseguros.
Uma lista de verificação de prontidão para produção
- Imagem de vários estágios apenas com dependências de produção, imagem base fixada, usuário não root.
- OPcache ativado, erros registrados não exibidos, php-fpm dimensionado a partir da memória medida.
- Componha um arquivo com políticas de reinicialização, verificações de integridade, limites de memória, rotação de log e nenhuma porta de banco de dados pública.
- Lançamentos por commit-hash tag com verificação automática de integridade e reversão.
- Backups e uma restauração testada para o banco de dados e uploads.
- Monitoramento e alertas sobre saúde, disco, erros e certificados.
Como podemos ajudar
Colocamos em contêineres aplicativos PHP, configuramos pipelines e liberações de CI e os conectamos a pagamentos, CRM e mensagens. Consulte nosso serviço de integrações ou agende uma ligação e analisaremos seu processo atual de hospedagem e liberação.
Perguntas frequentes
Docker é bom para executar PHP em produção?
Sim, quando bem utilizado. Oferece ambientes idênticos, lançamentos repetíveis e reversões rápidas. Precisa de configuração adequada: imagens imutáveis, verificações de integridade, limites de recursos, rotação de logs, gerenciamento de segredos e backups de dados fora do contêiner.
O banco de dados deve ser executado no Docker em produção?
Pode ser usado em sistemas pequenos e médios se usar um volume persistente, nunca for exposto publicamente e tiver backups testados. Para dados críticos, muitas equipes preferem um serviço de banco de dados gerenciado, que lide com failover, atualizações e backups para você.
Como faço para reverter uma versão Docker ?
Marque cada imagem com o hash de commit do Git e mantenha a tag anterior. Para reverter, inicie os contêineres com a tag anterior. Escreva migrações de banco de dados para que o código antigo ainda funcione com o esquema mais recente.
Quantos trabalhadores php-fpm devo configurar?
Meça a memória média de um trabalhador sob carga, decida quanta memória PHP pode usar e divida. Definir um valor muito alto causa troca, enquanto um valor muito baixo causa solicitações na fila. Observe o log do php-fpm para avisos de max_children.
Qual é o maior erro de segurança Docker com aplicativos PHP ?
Colocar segredos na imagem ou Git e publicar portas de banco de dados na internet. Evite também executar como root, montar o soquete Docker em contêineres e nunca atualizar a imagem base.
Chamada de descoberta gratuita
Traga uma stack desordenada ou uma integração parada - a Integrono mapeia o primeiro passo mais inteligente.
Recebe uma leitura senior sobre armadilhas de orçamento, dependências e se convém comprar, colar ou construir - antes de começar o trabalho pago. O CEO permanece no loop.