Rollback automático de deploy

Um deploy com bug em produção precisa ser revertido em segundos, não em minutos. Rollback automático detecta falhas no health check logo após o deploy e reverte para a versão anterior sem intervenção humana. Combinando tags de imagem Docker versionadas com um script de rollback, você tem uma rede de segurança que cobre os cenários mais comuns.

Estratégia de versionamento para rollback

O rollback funciona apenas quando você mantém a imagem anterior disponível:

bash
# docker-compose.yml na VPS — usar tag de versão, não :latest
services:
  app:
    image: ghcr.io/usuario/minha-api:main   # sempre a última do branch
    # OU com versão semântica (mais controlado):
    # image: ghcr.io/usuario/minha-api:1.2.3

# Estratégia de tags no GitHub Actions:
# ghcr.io/usuario/app:main         — última build do main
# ghcr.io/usuario/app:sha-abc1234  — build específica (por commit)
# ghcr.io/usuario/app:1.2.3        — release semântico

# Para rollback: saber qual era a imagem anterior
# Na VPS — ver imagens disponíveis localmente:
docker images ghcr.io/usuario/minha-api

# Ver qual imagem está rodando atualmente:
docker inspect minha-api_app_1 | python3 -c "
import sys,json
info = json.load(sys.stdin)
print(info[0]['Config']['Image'])
"

Script de deploy com rollback automático

Script que salva a versão atual, faz o deploy e reverte em caso de falha:

bash
#!/bin/bash
# /opt/scripts/deploy-with-rollback.sh
set -euo pipefail

APP_DIR="/opt/minha-api"
HEALTH_URL="http://localhost:3000/health"
LOG="/var/log/deploy.log"
exec >> "$LOG" 2>&1

echo "=== Deploy iniciado: $(date) ==="

cd "$APP_DIR"

# 1. Salvar versão atual para rollback
CURRENT_IMAGE=$(docker compose images app -q 2>/dev/null || echo "none")
echo "Versão atual: $CURRENT_IMAGE"

# Função de rollback
rollback() {
  echo "ROLLBACK: revertendo para $CURRENT_IMAGE"
  if [ "$CURRENT_IMAGE" != "none" ]; then
    sed -i "s|image:.*|image: $CURRENT_IMAGE|" docker-compose.yml
    docker compose up -d --no-deps app
    echo "Rollback concluído"
  else
    echo "Sem versão anterior para rollback"
  fi
  exit 1
}

# 2. Puxar nova imagem
echo "Puxando nova imagem..."
docker compose pull app || rollback

# 3. Fazer deploy
echo "Iniciando novo container..."
docker compose up -d --no-deps app || rollback

# 4. Health check com retry
echo "Verificando saúde..."
MAX_RETRIES=12
RETRY=0
until curl -sf "$HEALTH_URL" > /dev/null 2>&1; do
  RETRY=$((RETRY + 1))
  if [ $RETRY -ge $MAX_RETRIES ]; then
    echo "Health check falhou após ${MAX_RETRIES} tentativas"
    rollback
  fi
  echo "Tentativa $RETRY/$MAX_RETRIES..."
  sleep 5
done

echo "=== Deploy concluído com sucesso: $(date) ==="

Rollback no GitHub Actions workflow

Integre o rollback diretamente no workflow do GitHub Actions:

yaml
# .github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Build e push imagem
        id: build
        uses: docker/build-push-action@v6
        with:
          push: true
          tags: |
            ghcr.io/usuario/app:main
            ghcr.io/usuario/app:sha-${{ github.sha }}

      - name: Configurar SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}

      - name: Salvar versão atual na VPS
        id: save-current
        run: |
          CURRENT=$(ssh deploy@${{ secrets.SSH_HOST }} \
            "docker inspect minha-api_app_1 --format='{{.Config.Image}}' 2>/dev/null || echo 'none'")
          echo "current_image=$CURRENT" >> $GITHUB_OUTPUT

      - name: Deploy
        id: deploy
        run: |
          ssh deploy@${{ secrets.SSH_HOST }} "/opt/scripts/deploy-with-rollback.sh"

      - name: Rollback em caso de falha
        if: failure() && steps.deploy.outcome == 'failure'
        run: |
          ssh deploy@${{ secrets.SSH_HOST }} "
            docker pull ${{ steps.save-current.outputs.current_image }}
            cd /opt/minha-api
            IMAGE=${{ steps.save-current.outputs.current_image }} docker compose up -d --no-deps app
          "

Blue/Green deployment para zero downtime

Deploy blue/green mantém duas versões rodando e troca o tráfego após validação:

bash
# Conceito blue/green com Nginx + dois containers:

# 1. Container "blue" rodando na porta 3000 (versão atual)
# 2. Deploy "green" na porta 3001 (nova versão)
# 3. Testar a versão green
# 4. Atualizar Nginx para apontar para green (3001)
# 5. Se OK: parar blue. Se falhar: manter blue, parar green.

# docker-compose.yml:
services:
  app-blue:
    image: ghcr.io/usuario/app:current
    ports: ["127.0.0.1:3000:3000"]

  app-green:
    image: ghcr.io/usuario/app:new
    ports: ["127.0.0.1:3001:3000"]
    profiles: ["green"]    # sobe apenas quando solicitado

# Script de swap blue/green:
#!/bin/bash
# 1. Subir green
docker compose --profile green up -d

# 2. Health check no green
sleep 5
curl -sf http://localhost:3001/health || (docker compose --profile green down && exit 1)

# 3. Trocar Nginx para green
sed -i 's/proxy_pass http://localhost:3000/proxy_pass http://localhost:3001/' /etc/nginx/sites-available/app
nginx -s reload

# 4. Parar blue
docker compose stop app-blue

Histórico de deploys e auditoria

Mantenha um registro dos deploys para facilitar investigação de incidentes:

bash
#!/bin/bash
# Adicionar ao script de deploy:
DEPLOY_LOG="/var/log/deploys.jsonl"

log_deploy() {
  local status="$1"
  local image="$2"
  local git_sha="$3"

  echo "{
    \"timestamp\": \"$(date -Iseconds)\",
    \"status\": \"$status\",
    \"image\": \"$image\",
    \"git_sha\": \"$git_sha\",
    \"user\": \"github-actions\",
    \"host\": \"$(hostname)\"
  }" >> "$DEPLOY_LOG"
}

# Chamar no script:
log_deploy "success" "ghcr.io/usuario/app:sha-abc1234" "abc1234"
log_deploy "rollback" "ghcr.io/usuario/app:sha-def5678" "def5678"

# Consultar histórico:
cat /var/log/deploys.jsonl | python3 -m json.tool | grep -A 5 "rollback"

# Últimos 10 deploys:
tail -10 /var/log/deploys.jsonl | python3 -c "
import sys,json
for line in sys.stdin:
    d = json.loads(line)
    print(f"{d['timestamp']} [{d['status'].upper()}] {d['image']}")
"

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

Depende do tempo de startup da aplicação. Para Node.js simples: 5-10 segundos. Para Spring Boot: 30-60 segundos. Para FastAPI: 5-15 segundos. Regra: medir o tempo de startup em staging e multiplicar por 1.5 para margem de segurança. Use retry loop (12 tentativas × 5 segundos = 60 segundos de janela) em vez de sleep fixo.

Não. --no-deps apenas reinicia o container especificado sem afetar os outros. Se a nova imagem falhar no startup, o container fica em estado Restarting ou Exited, não há rollback automático para a versão anterior. O rollback deve ser implementado explicitamente no script de deploy.

Valide o health check da versão de rollback antes de considerar resolvido. Se a versão anterior também estiver com problemas (ex: falha de infraestrutura, não de código), o rollback não vai resolver. Nesse caso: investigate o health check endpoint primeiro, certifique-se que retorna 200 apenas quando a aplicação está realmente funcional.

Rollback: operação de infraestrutura, reverter para imagem/versão anterior em produção, imediatamente. Revert: operação de git, criar um commit que desfaz as mudanças do commit problemático, que depois dispara o CI/CD normal. Rollback é mais rápido (segundos), revert é mais correto (cria histórico rastreável).

Canary direciona um percentual do tráfego para a nova versão antes de migrar 100%. Na VPS com Nginx: use upstream com weight: upstream backend { server app-stable:3000 weight=9; server app-canary:3000 weight=1; }, 10% vai para canary. Se as métricas forem boas, aumente o peso gradualmente. Mais complexo que blue/green mas menos impacto em caso de problema.