Webhook de deploy: atualizar a VPS via GitHub webhook
Para quem quer deploy automático sem configurar GitHub Actions, um servidor de webhook simples na própria VPS resolve: o GitHub envia uma requisição POST a cada push, o servidor valida a assinatura e executa o script de deploy. Simples, sem dependência de runners externos.
Servidor de webhook em Node.js
API Express que recebe webhooks do GitHub e executa o deploy:
typescript
// webhook-server.ts — servidor dedicado para deploy
import express from 'express'
import crypto from 'crypto'
import { exec } from 'child_process'
import { promisify } from 'util'
const execAsync = promisify(exec)
const app = express()
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!
const DEPLOY_SCRIPT = process.env.DEPLOY_SCRIPT ?? '/opt/scripts/deploy.sh'
const DEPLOY_BRANCH = process.env.DEPLOY_BRANCH ?? 'refs/heads/main'
// Verificar assinatura HMAC do GitHub:
function verificarAssinatura(payload: Buffer, signature: string): boolean {
const esperado = 'sha256=' + crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(Buffer.from(esperado), Buffer.from(signature))
}
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.headers['x-hub-signature-256'] as string
if (!signature || !verificarAssinatura(req.body, signature)) {
return res.status(401).json({ error: 'Assinatura inválida' })
}
const payload = JSON.parse(req.body.toString())
// Só executar para push na branch correta:
if (payload.ref !== DEPLOY_BRANCH) {
return res.json({ message: 'Branch ignorada' })
}
res.json({ message: 'Deploy iniciado', commit: payload.after })
// Executar deploy assincronamente (não bloquear o webhook):
try {
const { stdout, stderr } = await execAsync(`bash ${DEPLOY_SCRIPT}`, {
timeout: 300_000, // 5 minutos de timeout
env: { ...process.env, GIT_COMMIT: payload.after },
})
console.log('Deploy concluído:', stdout)
} catch (err) {
console.error('Deploy falhou:', err)
}
})
app.listen(9000, '127.0.0.1', () => console.log('Webhook server rodando na porta 9000'))Script de deploy chamado pelo webhook
Script shell que executa o deploy e envia notificação:
bash
#!/bin/bash
# /opt/scripts/deploy.sh
set -e
APP_DIR="/opt/minha-api"
LOG_FILE="/var/log/deploy.log"
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $1" | tee -a "$LOG_FILE"; }
alerta() {
curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \
-d "chat_id=${TELEGRAM_CHAT_ID}" --data-urlencode "text=$1" > /dev/null
}
log "=== Iniciando deploy ==="
log "Commit: ${GIT_COMMIT:-desconhecido}"
cd "$APP_DIR"
# Salvar versão atual para rollback:
PREVIOUS=$(git rev-parse --short HEAD)
log "Versão anterior: $PREVIOUS"
# Baixar código mais recente:
git fetch origin main
git reset --hard origin/main
# Instalar dependências:
npm ci --only=production
# Build (se aplicável):
if [ -f tsconfig.json ]; then
npm run build
fi
# Migrations:
if npm run --if-present db:migrate; then
log "Migrations executadas"
fi
# Reiniciar aplicação:
pm2 reload minha-api --update-env
# Verificar que o serviço está respondendo:
sleep 5
if curl -sf http://localhost:3000/health > /dev/null; then
CURRENT=$(git rev-parse --short HEAD)
log "Deploy concluído: $PREVIOUS → $CURRENT"
alerta "✅ Deploy concluído: $PREVIOUS → $CURRENT"
else
log "ERRO: healthcheck falhou, revertendo para $PREVIOUS"
alerta "🔴 Deploy falhou! Revertendo para $PREVIOUS"
git reset --hard "$PREVIOUS"
npm ci --only=production
pm2 reload minha-api --update-env
exit 1
fiConfigurar webhook no GitHub
Registrar o endpoint no repositório do GitHub:
bash
# 1. Expor o webhook server via Nginx:
# /etc/nginx/conf.d/webhook.conf:
server {
listen 443 ssl http2;
server_name deploy.seudominio.com.br;
ssl_certificate /etc/letsencrypt/live/deploy.seudominio.com.br/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/deploy.seudominio.com.br/privkey.pem;
location /webhook {
proxy_pass http://127.0.0.1:9000;
proxy_set_header X-Real-IP $remote_addr;
}
}
# 2. Configurar no GitHub:
# Repo → Settings → Webhooks → Add webhook
# Payload URL: https://deploy.seudominio.com.br/webhook
# Content type: application/json
# Secret: mesmo valor do WEBHOOK_SECRET no .env
# Events: ✓ Just the push event
# 3. Rodar o webhook server com PM2:
pm2 start dist/webhook-server.js --name webhook-deploy
pm2 save
# 4. Testar manualmente:
curl -X POST https://deploy.seudominio.com.br/webhook \
-H "Content-Type: application/json" \
-H "X-Hub-Signature-256: sha256=ASSINATURA" \
-d '{"ref":"refs/heads/main","after":"abc123"}'Evitar deploys concorrentes
Usar lock file para não rodar dois deploys ao mesmo tempo:
typescript
// webhook-server.ts — adicionar controle de concorrência:
let deploying = false
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
// ... verificar assinatura ...
if (deploying) {
return res.status(429).json({ message: 'Deploy já em andamento, aguardando...' })
}
const payload = JSON.parse(req.body.toString())
if (payload.ref !== DEPLOY_BRANCH) {
return res.json({ message: 'Branch ignorada' })
}
res.json({ message: 'Deploy enfileirado' })
deploying = true
try {
await execAsync(`bash ${DEPLOY_SCRIPT}`, {
timeout: 300_000,
env: { ...process.env, GIT_COMMIT: payload.after },
})
} finally {
deploying = false
}
})
# Alternativa via lock file no script shell:
LOCK_FILE="/tmp/deploy.lock"
if [ -f "$LOCK_FILE" ]; then
# Verificar se o processo que criou o lock ainda existe:
LOCK_PID=$(cat "$LOCK_FILE")
if kill -0 "$LOCK_PID" 2>/dev/null; then
echo "Deploy já em andamento (PID $LOCK_PID)"
exit 0
fi
fi
echo $ > "$LOCK_FILE"
trap "rm -f $LOCK_FILE" EXITWebhook vs GitHub Actions: quando usar cada um
Comparativo para decidir a melhor abordagem de CI/CD:
bash
# GitHub Actions — use quando precisar de:
# ✓ Build no ambiente CI (Node.js, Python, Go)
# ✓ Testes automatizados com serviços (PostgreSQL, Redis)
# ✓ Push para registry Docker (GHCR, Docker Hub)
# ✓ Deploy em múltiplos ambientes (staging, prod)
# ✓ Aprovação manual antes do deploy
# ✓ Matrix de testes (múltiplas versões/OS)
# ✓ Cache de dependências gerenciado
# Webhook self-hosted — use quando:
# ✓ A VPS já tem o Node.js e todas as dependências
# ✓ O deploy é simples: git pull + pm2 reload
# ✓ Você quer evitar os 2.000 min/mês do GitHub gratuito
# ✓ Quer menor latência (sem cold start do runner)
# ✓ VPS sem acesso à internet (intranet)
# ✗ Evite para builds pesados (TypeScript, Docker)
# ✗ Evite para múltiplos ambientes com aprovação
# Híbrido (melhor dos dois):
# GitHub Actions: testes + build + push Docker
# Webhook ou SSH Action: apenas o deploy final na VPS
# → CI no GitHub, deploy via SSH/webhook$ 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.