Deploy automático na VPS com GitHub Actions

Deploys manuais via SSH são lentos, propensos a erro e não escalam. Com GitHub Actions, cada push no branch main dispara automaticamente o build, os testes e o deploy na VPS, tudo em minutos, sem intervenção humana. O workflow usa SSH com chave dedicada, rsync para sincronizar arquivos e docker compose para subir a nova versão.

Configurar chave SSH dedicada para o CI/CD

Nunca use sua chave pessoal no CI/CD, crie uma chave dedicada com acesso mínimo:

bash
# 1. Gerar chave SSH dedicada para o CI/CD (sem passphrase)
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/id_ed25519_github_actions -N ""

# 2. Autorizar a chave na VPS
ssh-copy-id -i ~/.ssh/id_ed25519_github_actions.pub deploy@IP_DA_VPS

# Ou manualmente na VPS:
cat ~/.ssh/id_ed25519_github_actions.pub >> ~/.ssh/authorized_keys

# 3. Adicionar ao GitHub Secrets (Settings → Secrets and variables → Actions):
# SSH_PRIVATE_KEY   → conteúdo de ~/.ssh/id_ed25519_github_actions
# SSH_HOST          → IP ou domínio da VPS
# SSH_USER          → deploy
# SSH_PORT          → 22 (ou porta customizada)

# Verificar o conteúdo da chave privada:
cat ~/.ssh/id_ed25519_github_actions

Workflow completo de deploy

Arquivo .github/workflows/deploy.yml que faz build, testes e deploy na VPS:

yaml
# .github/workflows/deploy.yml
name: Deploy para VPS

on:
  push:
    branches: [main]
  workflow_dispatch:          # permitir disparo manual

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production   # requer aprovação manual (opcional)

    steps:
      - name: Checkout código
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '22'
          cache: 'npm'

      - name: Instalar dependências e build
        run: |
          npm ci
          npm run build

      - name: Rodar testes
        run: npm test

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

      - name: Adicionar VPS ao known_hosts
        run: |
          ssh-keyscan -H ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts

      - name: Sincronizar arquivos com rsync
        run: |
          rsync -avz --delete \
            --exclude='.git' \
            --exclude='node_modules' \
            --exclude='.env' \
            ./ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:/opt/minha-api/

      - name: Deploy na VPS
        run: |
          ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "
            cd /opt/minha-api
            docker compose pull
            docker compose build app
            docker compose up -d --no-deps app
            docker compose ps
          "

Health check pós-deploy

Verifique que a nova versão está saudável antes de considerar o deploy bem-sucedido:

yaml
      - name: Health check pós-deploy
        run: |
          echo "Aguardando aplicação iniciar..."
          sleep 10

          MAX_RETRIES=12
          RETRY=0
          until curl -sf https://api.seudominio.com.br/health; do
            RETRY=$((RETRY + 1))
            if [ $RETRY -ge $MAX_RETRIES ]; then
              echo "Health check falhou após $MAX_RETRIES tentativas"
              exit 1
            fi
            echo "Tentativa $RETRY/$MAX_RETRIES — aguardando..."
            sleep 5
          done

          echo "Deploy concluído com sucesso!"

      - name: Rollback em caso de falha
        if: failure()
        run: |
          ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} "
            cd /opt/minha-api
            docker compose up -d --no-deps app --scale app=0
            docker compose up -d --no-deps app
          "
          echo "Rollback executado"

Deploy sem downtime com docker compose

Use --no-deps para reiniciar apenas o container da aplicação sem derrubar banco e nginx:

bash
# Na VPS — script de deploy chamado pelo GitHub Actions:
# /opt/scripts/deploy.sh

#!/bin/bash
set -euo pipefail

APP_DIR="/opt/minha-api"
cd "$APP_DIR"

echo "Iniciando deploy: $(date)"

# Pull da nova imagem (se usar imagem do registry)
docker compose pull app 2>/dev/null || true

# Rebuild da imagem local
docker compose build app

# Restart apenas do container app (banco e nginx ficam up)
docker compose up -d --no-deps --remove-orphans app

# Aguardar container ficar healthy
echo "Aguardando health check..."
TIMEOUT=60
ELAPSED=0
until docker compose ps app | grep -q "healthy"; do
  sleep 2
  ELAPSED=$((ELAPSED + 2))
  if [ $ELAPSED -ge $TIMEOUT ]; then
    echo "Container não ficou healthy em ${TIMEOUT}s"
    exit 1
  fi
done

echo "Deploy concluído: $(date)"

Notificar resultado do deploy no Telegram

Envie notificação de sucesso ou falha ao final de cada deploy:

yaml
      - name: Notificar sucesso no Telegram
        if: success()
        run: |
          curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
            -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
            -d text="✅ Deploy concluído
Repo: ${{ github.repository }}
Branch: ${{ github.ref_name }}
Commit: ${{ github.sha }}
Por: ${{ github.actor }}"

      - name: Notificar falha no Telegram
        if: failure()
        run: |
          curl -s -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \
            -d chat_id="${{ secrets.TELEGRAM_CHAT_ID }}" \
            -d text="❌ Deploy FALHOU
Repo: ${{ github.repository }}
Branch: ${{ github.ref_name }}
Ver: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"

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

Sim, ilimitado para repositórios públicos. Para repositórios privados: 2000 minutos/mês no plano Free. Um deploy típico (build Node.js + rsync + restart Docker) leva 2-3 minutos. Com 2000 min/mês, você tem ~666-1000 deploys/mês no plano gratuito.

rsync: envia apenas os arquivos que mudaram, não requer git na VPS, exclui facilmente node_modules e .env. git pull: mais simples mas requer git na VPS e não exclui facilmente arquivos sensíveis. Para a maioria dos casos: rsync é mais robusto e explícito sobre o que vai para a VPS.

No workflow: o job de deploy deve depender do job de testes: needs: [test]. O GitHub Actions só executa deploy se test tiver succeeded. Exemplo: jobs: { test: {...}, deploy: { needs: [test], runs-on: ubuntu-latest, ... } }.

Sim. Use environment para cada destino: jobs: { deploy-staging: { environment: staging }, deploy-prod: { environment: production, needs: [deploy-staging] } }. No GitHub Environments (Settings), configure aprovação manual obrigatória para production e secrets diferentes por ambiente.

Habilite debug logs adicionando secrets: ACTIONS_STEP_DEBUG = true e ACTIONS_RUNNER_DEBUG = true. Use act (open source) para rodar workflows localmente. Para o passo SSH específico: adicione echo "SSH_HOST=${{ secrets.SSH_HOST }}" para verificar se o secret está chegando (cuidado com logs públicos em repos públicos).