Deploy zero-downtime com Nginx e PM2
Zero-downtime deploy garante que nenhuma requisição recebe erro 502 durante a atualização da aplicação. Com PM2 em modo cluster e Nginx como proxy, é possível fazer rolling restarts que trocam os workers um a um — sem janela de indisponibilidade, mesmo durante deploys frequentes.
PM2 cluster mode e reload gracioso
Configurar PM2 para reiniciar sem downtime:
javascript
# ecosystem.config.js — PM2 com cluster mode:
module.exports = {
apps: [{
name: 'minha-api',
script: 'dist/index.js',
// Cluster mode: múltiplos workers (1 por CPU):
instances: 'max',
exec_mode: 'cluster',
// Graceful shutdown — aguardar requisições em andamento:
kill_timeout: 10000, // matar worker após 10s se não fechar sozinho
wait_ready: true, // aguardar processo emitir 'ready' antes de considerar iniciado
listen_timeout: 10000, // timeout para o worker emitir 'ready'
// Ambiente de produção:
env_production: {
NODE_ENV: 'production',
PORT: 3000,
},
}]
}
# Iniciar em modo produção:
pm2 start ecosystem.config.js --env production
# Deploy zero-downtime (reload um worker por vez):
pm2 reload minha-api --update-env
# Ver workers em tempo real:
pm2 monitSIGTERM handler na aplicação Node.js
Fechar conexões abertas antes de encerrar o processo:
typescript
// src/index.ts — graceful shutdown obrigatório para zero-downtime:
import express from 'express'
import { Pool } from 'pg'
const app = express()
const db = new Pool()
const server = app.listen(3000, () => {
console.log('Servidor iniciado na porta 3000')
// Sinalizar para o PM2 que o processo está pronto:
process.send?.('ready')
})
// Handler de shutdown gracioso:
async function shutdown(signal: string) {
console.log(`Recebido ${signal}, encerrando servidor...`)
// 1. Parar de aceitar novas conexões:
server.close(async () => {
console.log('Servidor HTTP fechado')
// 2. Aguardar conexões existentes terminarem e fechar o pool:
await db.end()
console.log('Pool de banco fechado')
process.exit(0)
})
// 3. Forçar encerramento se demorar mais de 8s:
setTimeout(() => {
console.error('Timeout no shutdown — forçando saída')
process.exit(1)
}, 8000)
}
process.on('SIGTERM', () => shutdown('SIGTERM')) // PM2 reload usa SIGTERM
process.on('SIGINT', () => shutdown('SIGINT')) // Ctrl+C em devBlue-green deploy com Nginx
Trocar entre dois ambientes (blue/green) no Nginx sem downtime:
bash
# Dois processos rodando em portas diferentes:
# Blue: porta 3000 (versão atual)
# Green: porta 3001 (nova versão)
# /etc/nginx/conf.d/minha-api.conf:
upstream api_blue {
server 127.0.0.1:3000;
}
upstream api_green {
server 127.0.0.1:3001;
}
# Incluir o arquivo que define qual upstream está ativo:
include /etc/nginx/snippets/api-active.conf;
server {
listen 443 ssl http2;
server_name api.seudominio.com.br;
location /api/ {
proxy_pass http://api_active;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
# /etc/nginx/snippets/api-active.conf (começa apontando para blue):
# upstream api_active { server 127.0.0.1:3000; }
# Script de deploy blue-green:
#!/bin/bash
# deploy-bluegreen.sh
CURRENT=$(cat /opt/current-env) # "blue" ou "green"
if [ "$CURRENT" = "blue" ]; then
NEXT="green"; NEXT_PORT=3001
else
NEXT="blue"; NEXT_PORT=3000
fi
# Subir a nova versão na porta inativa:
cd /opt/minha-api-$NEXT
git pull && npm ci --only=production && npm run build
pm2 start ecosystem.config.js --name "api-$NEXT" -- --port $NEXT_PORT
# Aguardar healthcheck passar:
for i in {1..10}; do
curl -sf http://localhost:$NEXT_PORT/health && break
sleep 3
done
# Trocar o Nginx para a nova versão:
echo "upstream api_active { server 127.0.0.1:$NEXT_PORT; }" > /etc/nginx/snippets/api-active.conf
nginx -s reload
# Parar a versão antiga após 30s:
sleep 30
pm2 delete "api-$CURRENT"
echo $NEXT > /opt/current-envZero-downtime com Docker e docker-compose
Rolling update usando Docker Compose v2:
yaml
# docker-compose.yml — com múltiplas réplicas:
services:
api:
image: ghcr.io/org/minha-api:${IMAGE_TAG:-latest}
restart: always
deploy:
replicas: 2
update_config:
parallelism: 1 # atualizar 1 réplica por vez
delay: 10s # aguardar 10s entre réplicas
order: start-first # subir nova antes de desligar a antiga
failure_action: rollback
rollback_config:
parallelism: 1
delay: 5s
healthcheck:
test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
interval: 10s
timeout: 5s
retries: 3
start_period: 15s
nginx:
image: nginx:alpine
restart: always
ports:
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- api
# Deploy zero-downtime (atualiza 1 réplica por vez):
IMAGE_TAG=sha-abc1234 docker compose up -d apiMonitorar o deploy e detectar regressões
Verificar métricas após o deploy e fazer rollback automático:
bash
#!/bin/bash
# post-deploy-check.sh — executar após o deploy
API_URL="https://api.seudominio.com.br"
TELEGRAM_TOKEN="..."
CHAT_ID="..."
alerta() {
curl -s -X POST "https://api.telegram.org/bot$TELEGRAM_TOKEN/sendMessage" \
-d "chat_id=$CHAT_ID" --data-urlencode "text=$1"
}
# Verificar taxa de erros via Prometheus (janela de 5 min pós-deploy):
sleep 300 # aguardar 5 minutos
ERROR_RATE=$(curl -s "http://localhost:9090/api/v1/query" \
--data-urlencode 'query=rate(api_http_requests_total{status=~"5.."}[5m]) / rate(api_http_requests_total[5m])' \
| jq -r '.data.result[0].value[1]' 2>/dev/null || echo "0")
# Se taxa de erros > 5%, fazer rollback:
if (( $(echo "$ERROR_RATE > 0.05" | bc -l) )); then
alerta "🔴 Taxa de erros alta pós-deploy ($ERROR_RATE) — iniciando rollback!"
cd /opt/minha-api
git reset --hard HEAD~1
npm ci --only=production
pm2 reload minha-api --update-env
alerta "✅ Rollback concluído para $(git rev-parse --short HEAD)"
else
alerta "✅ Deploy verificado — taxa de erros normal ($ERROR_RATE)"
fi$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS para APIs em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.