Tutorial: implementar deploy canário com GitHub Actions e Nginx para aplicações PHP monolito
Deploy & DevOps · Por Kevin Marcondes · Publicado em · 11 min de leitura
Este tutorial: implementar deploy canário com GitHub Actions e Nginx para aplicações PHP monolito (smoke tests e rollback automático) é um passo a passo prático. Você vai dividir tráfego entre versões, forçar canário por header/cookie, promover gradualmente e reverter com segurança.
Pré-requisitos e visão da arquitetura canário
A estratégia canário reduz risco. Uma pequena parcela do tráfego vai para a nova versão. Se tudo ficar estável, você promove. Caso contrário, reverte.
Usaremos Nginx para roteamento ponderado e GitHub Actions para build, deploy, smoke tests, promoção e rollback. Dois pools PHP‑FPM isolam execuções.
Componentes: Nginx na frente, dois pools PHP‑FPM (stable e canary), releases versionados e workflow de CI acionando SSH no servidor.
Você precisa de acesso root ou sudo, chaves SSH e um endpoint de saúde, como /health. Tenha logs e métricas mínimas para observar erros.
Para evitar problemas com estáticos, use fingerprinting de arquivos. Exemplo: app.abc123.css. Isso evita servir CSS/JS da versão errada.
Monte um plano de rollback simples e testado. Um checklist ajuda a não esquecer etapas críticas. Veja o checklist de pré‑deploy e rollback.
- Nginx como reverse proxy e roteador canário
- Dois pools PHP‑FPM: stable e canary
- Releases em /var/www/app/releases/<sha> + symlinks stable/canary
- Workflow no GitHub Actions com SSH e scripts idempotentes
- Endpoint /health para smoke tests e monitoramento básico
- Assets com fingerprint para evitar inconsistências
- Plano de rollback claro e testado
Configurar dois pools PHP‑FPM (stable e canary)
Crie dois pools PHP‑FPM para isolar opcache e parâmetros entre stable e canary. Eles usam sockets e diretórios distintos. Um reload do PHP‑FPM limpa o opcache e aplica mudanças com interrupção mínima.
Ajuste pm.max_children conforme a capacidade. O canário recebe menos tráfego. Garanta permissões corretas no diretório da aplicação e nos sockets.
; /etc/php/8.2/fpm/pool.d/app.conf
[app_stable]
user = www-data
group = www-data
listen = /run/php-fpm-stable.sock
pm = dynamic
pm.max_children = 20
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
chdir = /var/www/app/releases/stable
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.validate_timestamps] = 1
[app_canary]
user = www-data
group = www-data
listen = /run/php-fpm-canary.sock
pm = dynamic
pm.max_children = 8
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3
chdir = /var/www/app/releases/canary
php_admin_value[opcache.enable] = 1
php_admin_value[opcache.validate_timestamps] = 1
Nginx: roteamento canário por porcentagem, header e cookie
Configure o Nginx para separar tráfego entre stable e canary por porcentagem com split_clients. Permita forçar canário com header X‑Canary: 1 ou cookie canary=1. A seleção também define o docroot para o PHP.
Sobre assets: sirva estáticos do stable por simplicidade. Use fingerprinting para evitar inconsistências visuais. Alternativas: CDN ou regras duplicadas com caminhos versionados.
Se preferir outra troca de tráfego, considere um fluxo blue‑green em containers. Há um guia prático no final desta página.
| Sinal | Como definir | Quando usar |
|---|---|---|
| Porcentagem | split_clients 5% canary | Gradual padrão sem intervenção do usuário |
| Header | X-Canary: 1 | Forçar canário em smoke tests e QA |
| Cookie | canary=1 | Fixar usuário/grupo no canário temporariamente |
# Contexto http (ex.: nginx.conf)
map $http_x_canary $force_hdr_canary {
default 0;
"1" 1;
}
map $cookie_canary $force_ck_canary {
default 0;
"1" 1;
}
# Percentual no split_clients (editado via CI)
split_clients "$request_id" $bucket {
5% canary;
* stable;
}
# Prioriza header/cookie; senão usa bucket
map "$force_hdr_canary$force_ck_canary$bucket" $pool {
~1 canary;
~.*canary canary;
default stable;
}
map $pool $fpm_socket {
stable unix:/run/php-fpm-stable.sock;
canary unix:/run/php-fpm-canary.sock;
}
map $pool $docroot {
stable /var/www/app/releases/stable/public;
canary /var/www/app/releases/canary/public;
}
server {
listen 80;
server_name example.com;
# Fallback de estáticos aponta para stable
root /var/www/app/releases/stable/public;
location /health {
add_header Content-Type text/plain;
return 200 "ok";
}
location / {
try_files $uri /index.php?$query_string;
}
location ~ \.php$ {
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $docroot$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $docroot;
fastcgi_pass $fpm_socket;
}
# Estáticos: use fingerprinting (ex.: app.abc123.css)
location ~* \.(?:css|js|png|jpg|jpeg|gif|svg|ico|woff2?)$ {
expires 7d;
try_files $uri @php_fallback;
}
location @php_fallback { rewrite ^ /index.php last; }
}
Pipeline CI: build e empacotamento no GitHub Actions
O workflow compila dependências, empacota a aplicação e prepara variáveis para o deploy. Use Composer com flags de produção e artefato único tar.gz.
A etapa de build é separada do deploy. É possível acionar por workflow_dispatch e informar o percentual inicial do canário.
Para organizar o pipeline com boas práticas, confira o guia de pipeline de deploy automático.
- Composer em modo produção
- Artefato único versionado por SHA
- Inputs do workflow para parametrizar rollout
- Separação clara de build e deploy
name: canary-deploy-php
on:
workflow_dispatch:
inputs:
percent_start:
description: 'Percentual inicial do canário'
default: '5'
host:
description: 'Host de produção (SSH)'
required: true
app_url:
description: 'URL pública para smoke tests'
required: true
jobs:
build:
runs-on: ubuntu-latest
outputs:
rel: ${{ steps.pkg.outputs.rel }}
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- name: Install Composer deps
run: |
composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader
- name: Build assets (opcional)
run: |
echo 'adicione seu build de front aqui, se houver'
- name: Package artifact
id: pkg
run: |
REL=${GITHUB_SHA}
tar -czf release-$REL.tar.gz --exclude-vcs --exclude=.github .
echo rel=$REL >> $GITHUB_OUTPUT
- uses: actions/upload-artifact@v4
with:
name: php-release-${{ steps.pkg.outputs.rel }}
path: release-${{ steps.pkg.outputs.rel }}.tar.gz
Deploy do canário via SSH e reloads sem downtime
Nesta etapa, enviamos o artefato, extraímos em releases/<sha> e atualizamos o symlink canary. Definimos o percentual inicial no split_clients. Teste a configuração (nginx -t) e faça reloads.
Grave o release estável atual para rollback rápido. O reload do PHP‑FPM limpa o opcache. Isso evita servir bytecode antigo.
deploy:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: php-release-${{ needs.build.outputs.rel }}
- name: Prep SSH
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
run: |
mkdir -p ~/.ssh
echo "$SSH_KEY" > ~/.ssh/id_rsa
chmod 600 ~/.ssh/id_rsa
- name: Ship and activate canary
env:
SSH_USER: ${{ secrets.SSH_USER }}
SSH_HOST: ${{ inputs.host }}
REL: ${{ needs.build.outputs.rel }}
PSTART: ${{ inputs.percent_start }}
run: |
scp -o StrictHostKeyChecking=no release-$REL.tar.gz $SSH_USER@$SSH_HOST:/tmp/
ssh -o StrictHostKeyChecking=no $SSH_USER@$SSH_HOST bash -s <<EOF
set -euo pipefail
REL=${REL}
APP=/var/www/app
sudo mkdir -p $APP/releases/$REL
sudo tar -xzf /tmp/release-$REL.tar.gz -C $APP/releases/$REL
# Registra release estável atual
PREV=$(readlink -f $APP/releases/stable || true)
echo "$PREV" | sudo tee $APP/prev_stable.txt >/dev/null
# Ativa canary
sudo ln -sfn $APP/releases/$REL $APP/releases/canary
sudo systemctl reload php8.2-fpm
# Ajusta percentual no split_clients (arquivo snippet)
# Ex.: /etc/nginx/snippets/app_split.conf contém a linha "5% canary;"
sudo sed -i "s/^[[:space:]]*[0-9]\+% canary;/ ${PSTART}% canary;/" /etc/nginx/snippets/app_split.conf || true
sudo nginx -t && sudo systemctl reload nginx
EOF
Smoke tests e promoção gradual automática
Com o canário ativo, execute smoke tests forçando o header X‑Canary: 1. Valide endpoints críticos. Se passarem, aumente o percentual em etapas, como 5→25→50→100.
Entre etapas, aguarde e monitore erros. Valide HTTP 200 em sequência e aborte no primeiro erro. Integre alertas para agir rápido. Veja também como implementar health checks e readiness.
- Forçar canário com header em todas as chamadas de teste
- Testar rotas de login, checkout e APIs críticas
- Aguardar e monitorar 5xx entre promoções
- Abortar e acionar rollback no primeiro erro
#!/usr/bin/env bash
# run on GitHub Actions or jump host
set -euo pipefail
APP_URL="$1" # ex.: https://example.com
PERCENTS=(5 25 50 100)
SMOKE_ENDPOINTS=("/health" "/" "/api/status")
smoke() {
local url="$1"; local fail=0
for i in {1..10}; do
for ep in "${SMOKE_ENDPOINTS[@]}"; do
code=$(curl -fsS -o /dev/null -w "%{http_code}" -H "X-Canary: 1" "$url$ep" || true)
if [[ "$code" != "200" ]]; then
echo "Smoke FAIL $ep -> $code"; fail=1
else
echo "Smoke OK $ep -> $code"
fi
done
sleep 1
done
return $fail
}
for p in "${PERCENTS[@]}"; do
echo "Promovendo canário para ${p}%..."
ssh $SSH_OPTS $SSH_USER@$SSH_HOST "sudo sed -i 's/^[[:space:]]*[0-9]\+% canary;/ ${p}% canary;/' /etc/nginx/snippets/app_split.conf && sudo nginx -t && sudo systemctl reload nginx"
echo "Aguardando estabilizar..."; sleep 60
if ! smoke "$APP_URL"; then
echo "Falha nos smoke tests em ${p}%. Iniciando rollback..."; exit 42
fi
echo "OK em ${p}%"
done
echo "100% canário aprovado. Promovendo a estável..."
ssh $SSH_OPTS $SSH_USER@$SSH_HOST 'APP=/var/www/app; sudo ln -sfn "$APP/releases/canary" "$APP/releases/stable" && sudo systemctl reload php8.2-fpm && sudo sed -i "s/^[[:space:]]*[0-9]\+% canary;/ 0% canary;/" /etc/nginx/snippets/app_split.conf && sudo nginx -t && sudo systemctl reload nginx'
Rollback automático e limpeza segura
Se algo falhar, reverta de imediato. Zere o canário, restaure o symlink stable para o release anterior e recarregue Nginx e PHP‑FPM. Não exclua o release falho até todo o tráfego voltar ao estável.
Armazene o caminho do release estável antes do deploy. Use esse valor no rollback. Depois, investigue com calma, fora do horário de pico.
#!/usr/bin/env bash
# rollback.sh (executar no runner)
set -euo pipefail
ssh $SSH_OPTS $SSH_USER@$SSH_HOST '
set -euo pipefail
APP=/var/www/app
PREV=$(cat $APP/prev_stable.txt || true)
echo "Restaurando stable -> $PREV"
# 0% tráfego no canário
sudo sed -i "s/^[[:space:]]*[0-9]\+% canary;/ 0% canary;/" /etc/nginx/snippets/app_split.conf || true
sudo nginx -t && sudo systemctl reload nginx
# Reaponta o estável
if [ -n "$PREV" ] && [ -d "$PREV" ]; then
sudo ln -sfn "$PREV" "$APP/releases/stable"
fi
sudo systemctl reload php8.2-fpm
'
Como confirmar que funcionou e próximos passos
Confirme que usuários anônimos e autenticados acessam a versão correta conforme o percentual. Force o canário com header e valide o fluxo completo: login, carrinho, checkout e APIs.
Monitore erros 5xx, tempos de resposta e logs de aplicação. Integre alertas do time. Ao final, limpe releases antigos e documente percentuais, tempos e métricas.
Quer alternativa em containers com headers e pesos? Veja o tutorial de canário com Traefik. Para trocas totais, use deploy blue‑green PHP. Você concluiu o tutorial: implementar deploy canário com GitHub Actions e Nginx para aplicações PHP monolito.
- Conferir percentuais com amostras controladas
- Acompanhar logs e métricas por 24–48 horas
- Remover releases antigos após validação
- Documentar rollout e aprendizados
Perguntas frequentes
Posso usar um único pool PHP‑FPM para stable e canary?
Pode, mas pools separados isolam opcache e parâmetros, reduzindo riscos de interferência entre versões. Com um único pool, invalide o opcache a cada release e confira se o limite de memória comporta o cache de ambas as versões.
Como lidar com assets (CSS/JS) diferentes entre versões?
Use fingerprinting nos nomes de arquivos para que navegador e Nginx sirvam o asset correto mesmo quando só o tráfego dinâmico entra no canário. Alternativamente, publique estáticos em uma CDN por versão e aponte URLs absolutas versionadas.
Este fluxo garante zero downtime?
Reloads do Nginx e do PHP‑FPM são graciosos e costumam manter conexões. A indisponibilidade tende a ser mínima. Mesmo assim, teste configuração com nginx -t e promova em passos curtos para reduzir riscos e evitar interrupções percebidas.
E as migrations de banco durante o canário?
Planeje migrations compatíveis com a versão antiga. Primeiro aplique mudanças que não quebrem o estável. Só depois promova o canário. Se a nova versão exigir schema novo, use feature flags e migre em etapas para permitir rollback sem perda.
Como monitorar erros durante a promoção?
Colete taxas de 5xx, latência p95/p99 e erros de aplicação. Integre alertas no seu canal de time. Se não há stack de observabilidade, comece com um endpoint /health e contadores simples. Evolua para métricas e alertas conforme a maturidade.
Consigo forçar um usuário específico no canário?
Sim. Defina um cookie canary=1 ao autenticar usuários que você deseja fixar no canário. O Nginx prioriza header e cookie sobre a amostragem por porcentagem, permitindo testes direcionados e suporte a QA sem depender do hash aleatório.
Precisa colocar canários em produção com segurança?
Implemente rollout gradual, smoke tests e rollback automático no seu monólito PHP com apoio especializado.
Falar sobre deploy canárioVeja também
- tutorial: configurar mTLS entre Traefik e serviços em Docker Compose usando smallstep CA e certificados automáticos — passo a passo
- Tutorial: integrar Sentry ao pipeline de deploy com GitHub Actions para upload automático de source maps e criação de issues (PHP e JavaScript) — passo a passo
- Configurar retenção e compactação de logs de containers Docker com journald + logrotate + rsyslog em VPS com pouco disco
- tutorial: configurar GitHub Actions para testes de integração com PostgreSQL em containers efêmeros e rollback automático de migrações — guia prático