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 monit

    SIGTERM 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 dev

    Blue-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-env

    Zero-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 api

    Monitorar 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.

    Perguntas frequentes