CI/CD com GitHub Actions: Pipeline de Deploy do Zero à Produção
Crie um pipeline CI/CD completo com GitHub Actions: testes automatizados, build Docker, deploy em Kubernetes ou VPS, rollback e notificações.
Por que CI/CD é inegociável em 2025
Deployar manualmente via FTP, SSH ou painel de hospedagem em 2025 é um pesadelo operacional. Cada deploy é um risco de erro humano, cada hotfix é uma corrida contra o relógio manual. CI/CD automatiza tudo: testes rodam em cada pull request, builds são imutáveis, deploys são repetiveis e o rollback é um comando.
GitHub Actions é a ferramenta padrão para a maioria dos projetos — integrado ao GitHub, gratuito para projetos open source, tem mercado massivo de actions prontas.
Anatomia de um pipeline completo
Developer push → PR aberto
↓
[CI Pipeline]
├── Lint (ESLint/Biome)
├── Type check (tsc)
├── Testes unitários
├── Testes de integração
└── Build (verificação)
↓ (PR aprovado + merge na main)
[CD Pipeline]
├── Build imagem Docker
├── Push para registry
├── Deploy em staging
├── Smoke tests
└── Deploy em produção (com aprovação manual)
Pipeline básico: CI para Node.js
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main, develop]
push:
branches: [main]
jobs:
test:
name: Test & Lint
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: test
POSTGRES_PASSWORD: test
POSTGRES_DB: testdb
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7
ports: ["6379:6379"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- name: Install dependencies
run: npm ci
- name: Type check
run: npm run type-check
- name: Lint
run: npm run lint
- name: Tests
run: npm test -- --coverage --reporter=verbose
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
REDIS_URL: redis://localhost:6379
JWT_SECRET: test-secret-key-for-ci
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
Pipeline CD: Build Docker e deploy
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
tags: ["v*"]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
name: Build & Push Docker Image
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image-tag: ${{ steps.meta.outputs.version }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=sha,prefix={{branch}}-
type=semver,pattern={{version}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
name: Deploy to Staging
needs: build-and-push
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v4
- name: Deploy to Kubernetes
uses: azure/k8s-deploy@v5
with:
manifests: k8s/staging/
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image-tag }}
kubeconfig: ${{ secrets.KUBE_CONFIG_STAGING }}
- name: Run smoke tests
run: |
sleep 30 # aguardar pods subirem
curl -f https://staging.minha-empresa.com/health || exit 1
deploy-production:
name: Deploy to Production
needs: [build-and-push, deploy-staging]
runs-on: ubuntu-latest
environment:
name: production
url: https://app.minha-empresa.com
steps:
- uses: actions/checkout@v4
- name: Deploy to production
uses: azure/k8s-deploy@v5
with:
manifests: k8s/production/
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ needs.build-and-push.outputs.image-tag }}
kubeconfig: ${{ secrets.KUBE_CONFIG_PRODUCTION }}
strategy: rolling
Protegendo Secrets
# Nunca commite secrets — sempre use GitHub Secrets
# gh CLI para adicionar secrets:
gh secret set DATABASE_URL --body "postgresql://..."
gh secret set JWT_SECRET --body "super-secret"
# Environments para isolar por ambiente
# Settings > Environments > production
# Configure: required reviewers antes de deploy em produção
Deploy em VPS (alternativa ao K8s)
# .github/workflows/deploy-vps.yml (para VPS com Docker Compose)
deploy-vps:
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: deploy
key: ${{ secrets.VPS_SSH_KEY }}
script: |
cd /opt/app
# Atualizar imagem
export IMAGE_TAG=${{ needs.build-and-push.outputs.image-tag }}
docker pull ghcr.io/empresa/api:$IMAGE_TAG
# Rolling update zero-downtime
docker compose up -d --no-deps api
# Health check
sleep 10
docker compose ps api | grep -q "Up" || (docker compose logs api && exit 1)
echo "Deploy concluído: $IMAGE_TAG"
Notificações de deploy
- name: Notify Slack on success
if: success()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "✅ Deploy em produção: ${{ github.event.head_commit.message }} por ${{ github.actor }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Notify Slack on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
payload: |
{
"text": "🚨 FALHA no deploy: ${{ github.run_url }}"
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
