Deployment: Envoyer, Forge e estratégias zero-downtime
1. Fundamentos do Deployment Zero-Downtime em PHP
1.1. O que é zero-downtime e por que é crítico para aplicações PHP
Zero-downtime deployment é a capacidade de atualizar uma aplicação em produção sem interromper o serviço aos usuários. Para aplicações PHP, isso é especialmente crítico porque:
- Usuários podem estar em meio a requisições HTTP
- Jobs de fila podem estar sendo processados
- Sessões ativas podem ser corrompidas durante a troca de versões
Uma aplicação PHP que fica indisponível por 30 segundos durante um deploy pode perder milhares de requisições e causar frustração nos usuários.
1.2. Desafios específicos do PHP: opcache, sessions e filas
O PHP apresenta desafios únicos para zero-downtime:
Opcache: O cache de bytecode do PHP armazena versões compiladas dos arquivos. Se você substituir arquivos PHP sem limpar o opcache, o servidor continuará executando código antigo.
// Script para limpar opcache programaticamente
if (function_exists('opcache_reset')) {
opcache_reset();
echo "Opcache limpo com sucesso.\n";
}
Sessions: Dados de sessão armazenados em arquivos podem ser dessincronizados se a estrutura mudar entre versões.
Filas: Workers processando jobs antigos podem falhar se classes ou métodos forem removidos na nova versão.
1.3. Estratégias clássicas: blue-green deployment vs. rolling updates
Blue-Green Deployment: Mantém dois ambientes completos (blue e green). Um recebe tráfego enquanto o outro é atualizado. A troca é feita via balanceador de carga.
Rolling Updates: Atualiza instâncias uma a uma, mantendo as demais ativas. Mais comum em clusters com múltiplos servidores web.
Para aplicações PHP simples, o mecanismo de symlinks (usado pelo Envoyer) é a abordagem mais prática.
2. Laravel Forge: Provisionamento e Deploy Simplificado
2.1. Configuração de servidores: Nginx, PHP-FPM e banco de dados
O Laravel Forge automatiza a configuração inicial do servidor. Você escolhe o provedor (DigitalOcean, AWS, Linode) e o Forge provisiona:
- Nginx com configuração otimizada para PHP
- PHP-FPM com pools configurados
- MySQL/PostgreSQL ou MariaDB
- Redis para cache e filas
# Exemplo de configuração Nginx gerada pelo Forge
server {
listen 80;
server_name exemplo.com;
root /home/forge/exemplo.com/current/public;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.2-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
}
2.2. Script de deploy automático: git pull, composer install e migrations
O script de deploy do Forge pode ser personalizado. Exemplo típico:
# Deploy script do Forge
cd /home/forge/exemplo.com
git pull origin main
composer install --no-dev --prefer-dist
php artisan migrate --force
php artisan config:cache
php artisan route:cache
php artisan view:cache
2.3. Integração com GitHub/GitLab e deploy contínuo via webhooks
Configure webhooks no repositório para disparar deploys automáticos. No GitHub, vá em Settings > Webhooks e aponte para a URL fornecida pelo Forge.
// Exemplo de verificação de webhook (para validação no servidor)
$signature = $_SERVER['HTTP_X_HUB_SIGNATURE_256'] ?? '';
$payload = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $payload, env('WEBHOOK_SECRET'));
if (hash_equals($expected, $signature)) {
// Executar deploy
}
3. Laravel Envoyer: Orquestração de Deploys com Zero-Downtime
3.1. Mecanismo de "deploy de ativação": criação de release tags e symlinks
O Envoyer implementa zero-downtime através do padrão de releases:
/home/forge/exemplo.com/
├── releases/
│ ├── 20240101-120000/ # Release atual
│ └── 20240102-100000/ # Nova release (preparada)
└── current -> releases/20240101-120000/ # Symlink ativo
Durante o deploy, uma nova release é criada em paralelo. Apenas quando tudo está pronto, o symlink current é atualizado atomicamente.
3.2. Configuração de hooks: preparação, ativação e rollback
Os hooks permitem executar scripts em momentos específicos:
// Hook de ativação (executado após symlink ser trocado)
php artisan migrate --force
php artisan queue:restart
// Hook de rollback (se algo der errado)
php artisan down --retry=10
// Reverter symlink manualmente
3.3. Gerenciamento de múltiplos servidores e balanceadores de carga
Para múltiplos servidores, o Envoyer coordena deploys sequenciais ou paralelos:
// Configuração de servidores no Envoyer (exemplo conceitual)
'servers' => [
'web1' => ['ip' => '10.0.0.1', 'roles' => ['web']],
'web2' => ['ip' => '10.0.0.2', 'roles' => ['web']],
'worker' => ['ip' => '10.0.0.3', 'roles' => ['queue']],
],
4. Estratégias de Migrations e Cache sem Interrupção
4.1. Migrations seguras: separação de mudanças destrutivas em múltiplos deploys
Nunca misture mudanças destrutivas (como remover colunas) com deploys. Exemplo de migração segura:
// Primeiro deploy: adicionar nova coluna
Schema::table('users', function (Blueprint $table) {
$table->string('email_new')->nullable();
});
// Segundo deploy (após código usar nova coluna): remover antiga
Schema::table('users', function (Blueprint $table) {
$table->dropColumn('email');
$table->renameColumn('email_new', 'email');
});
4.2. Recriação de cache (config, routes, views) sem downtime
O cache deve ser recriado após o symlink ser atualizado:
// No hook de ativação do Envoyer
Artisan::call('config:cache');
Artisan::call('route:cache');
Artisan::call('view:cache');
4.3. Limpeza do opcache via PHP-FPM reload ou comando dedicado
Forçar recarga do PHP-FPM garante que todo opcache seja limpo:
sudo service php8.2-fpm reload
Ou use uma rota protegida para limpeza programática:
Route::post('/__opcache/reset', function () {
if (opcache_reset()) {
return response()->json(['status' => 'ok']);
}
return response()->json(['status' => 'error'], 500);
})->middleware('auth:api');
5. Gerenciamento de Filas e Jobs durante o Deploy
5.1. Pausa e retomada segura de workers (Horizon e Supervisord)
Com Laravel Horizon, pause workers antes do deploy:
// No hook de preparação
Artisan::call('horizon:pause');
// No hook de ativação
Artisan::call('horizon:continue');
Artisan::call('horizon:terminate');
5.2. Evitando perda de jobs: uso de filas com retry e dead-letter queues
Configure jobs para serem resilientes:
class ProcessPayment implements ShouldQueue
{
public $tries = 5;
public $backoff = [2, 5, 10, 30, 60];
public function handle(): void
{
try {
// Lógica do job
} catch (\Exception $e) {
if ($this->attempts() >= $this->tries) {
// Enviar para dead-letter queue
dispatch(new HandleFailedPayment($this->paymentId));
}
throw $e;
}
}
}
5.3. Sincronização entre versões antiga e nova de workers
Use versionamento de jobs para evitar incompatibilidades:
// Na nova versão, mantenha a classe antiga como alias
class_alias(\App\Jobs\V2\ProcessPayment::class, \App\Jobs\ProcessPayment::class);
6. Monitoramento e Rollback em Produção
6.1. Health checks automatizados pós-deploy (Laravel Pulse + APM)
Configure endpoints de health check:
Route::get('/health', function () {
$checks = [
'database' => DB::connection()->getPdo() ? 'ok' : 'fail',
'cache' => Cache::get('health_check') ?? 'fail',
'queue' => Queue::size() < 1000 ? 'ok' : 'warning',
];
if (in_array('fail', $checks)) {
return response()->json($checks, 500);
}
return response()->json($checks);
});
6.2. Estratégias de rollback rápido: ativação de release anterior
No Envoyer, o rollback é simples:
# Comando de rollback no servidor
cd /home/forge/exemplo.com
ln -sfn releases/20240101-120000 current
sudo service php8.2-fpm reload
6.3. Alertas e logs: detectando falhas no deploy em tempo real
Configure notificações no Envoyer para Slack ou email:
// Exemplo de log estruturado para monitoramento
Log::channel('deploy')->info('Deploy iniciado', [
'release' => $releaseId,
'server' => gethostname(),
'timestamp' => now(),
]);
7. Segurança e Boas Práticas no Pipeline de Deploy
7.1. Variáveis de ambiente (.env) e secrets management no servidor
Nunca armazene .env no repositório. Use o gerenciador de secrets do Forge:
// No script de deploy, referencie variáveis do ambiente
DB_PASSWORD=$(forge env get DB_PASSWORD)
7.2. Permissões de diretórios e hardening do ambiente PHP
Configure permissões corretas:
# Após cada deploy
chmod -R 755 /home/forge/exemplo.com/current/storage
chmod -R 755 /home/forge/exemplo.com/current/bootstrap/cache
chown -R forge:forge /home/forge/exemplo.com
7.3. Testes automatizados antes do deploy: CI/CD integrado com Forge/Envoyer
Integre testes no pipeline:
# Exemplo de GitHub Actions
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run tests
run: |
composer install
php artisan test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Trigger Forge deploy
run: |
curl -X POST ${{ secrets.FORGE_DEPLOY_URL }}
Referências
- Laravel Forge Documentation — Documentação oficial do Laravel Forge, incluindo provisionamento, deploy e gerenciamento de servidores.
- Laravel Envoyer Documentation — Documentação oficial do Envoyer com detalhes sobre zero-downtime deployments, hooks e rollbacks.
- PHP Opcache Documentation — Manual oficial do PHP sobre opcache, incluindo funções de reset e configurações recomendadas.
- Laravel Horizon Documentation — Documentação oficial do Horizon para gerenciamento de filas e workers durante deploys.
- Zero-Downtime Deployment Strategies — Artigo de Martin Fowler sobre blue-green deployment e estratégias de zero-downtime.
- Laravel Deployment Best Practices — Guia de boas práticas de deploy para aplicações Laravel, incluindo migrations seguras e cache management.