Blog

Infraestrutura 10 min de leitura

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.

Imagem de capa: Docker para PHP em produção: um guia prático de configuração para aplicativos de negócios

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

  1. Imagem de vários estágios apenas com dependências de produção, imagem base fixada, usuário não root.
  2. OPcache ativado, erros registrados não exibidos, php-fpm dimensionado a partir da memória medida.
  3. 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.
  4. Lançamentos por commit-hash tag com verificação automática de integridade e reversão.
  5. Backups e uma restauração testada para o banco de dados e uploads.
  6. 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.