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:
# 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_actionsWorkflow completo de deploy
Arquivo .github/workflows/deploy.yml que faz build, testes e deploy na VPS:
# .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:
- 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:
# 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:
- 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 }}"$ runstack deploy --plan starter
Não quer configurar manualmente?
Não quer configurar manualmente? Implante o VPS em menos de 3 minutos com a Runstack. Infraestrutura da OPEN DATACENTER, com servidores no Brasil.