Montar um ci/cd pipeline confiável é o que separa os times que fazem deploy com medo dos times que entregam com rotina. Neste tutorial, você vai criar um fluxo completo com GitHub Actions e Docker: testes, build da imagem, publicação em registry e deploy em um servidor Linux com verificação de saúde e rollback. É um desenho enxuto, adequado para SaaS e produtos internos em estágio inicial ou intermediário.
O que um ci/cd pipeline precisa entregar
Antes do YAML, defina o contrato. Um bom ci/cd pipeline responde a quatro perguntas: o código passa nos testes? A imagem gerada é reproduzível? Quem publicou e quando? Como voltar à versão anterior? Para isso, o fluxo terá estes estágios:
- Integração contínua (CI): instala dependências e roda lint e testes a cada push e pull request.
- Build: gera uma imagem Docker etiquetada com o SHA do commit, nunca apenas com latest.
- Publicação: envia a imagem a um registry, aqui o GitHub Container Registry (GHCR).
- Deploy (CD): o servidor baixa a imagem exata, sobe os containers e valida o endpoint de health check.
- Rollback: se a validação falhar, volta para a tag anterior.
Pré-requisitos
- Repositório no GitHub com um Dockerfile funcional e testes automatizados (o exemplo usa Node.js, mas a lógica vale para qualquer stack).
- Um servidor Linux com Docker Engine e o plugin Docker Compose instalados.
- Um usuário
deployno servidor, com permissão para executar Docker e acesso por chave SSH. - Um endpoint
/healthna aplicação que retorne status HTTP 200 quando ela estiver pronta.
Passo 1: workflow de integração contínua
Crie o arquivo .github/workflows/pipeline.yml. O primeiro job roda em todo push e pull request, e é ele que protege a branch principal.
name: pipeline
on:
push:
branches: [main]
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm testUse npm ci (ou o equivalente do seu gerenciador) em vez de instalação livre: ele respeita o lockfile e torna o ci/cd pipeline determinístico. Ative também branch protection exigindo que o job test passe antes do merge.
Passo 2: build e publicação da imagem
O job de build só executa na branch main e depende do sucesso dos testes. A permissão packages: write permite publicar no GHCR com o token efêmero do próprio workflow, sem criar credenciais permanentes.
build:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- run: echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
- run: docker build -t ghcr.io/${{ github.repository }}:${{ github.sha }} .
- run: docker push ghcr.io/${{ github.repository }}:${{ github.sha }}Atenção: o GHCR exige nome de imagem em minúsculas. Se o nome da organização ou do repositório tiver letras maiúsculas, converta o valor antes do build.
Passo 3: deploy via SSH com health check e rollback
Para times pequenos, SSH mais Docker Compose é um caminho simples e auditável, sem a complexidade de um cluster. No servidor, o docker-compose.yml referencia a imagem por variável: image: ghcr.io/sua-org/app:${IMAGE_TAG}. Em seguida, crie o script /opt/app/deploy.sh:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/app
PREV=$(cat current_tag 2>/dev/null || echo none)
export IMAGE_TAG
docker compose pull
docker compose up -d
if curl -fs --retry 10 --retry-delay 3 --retry-connrefused http://localhost:8080/health; then
echo $IMAGE_TAG > current_tag
else
if [ "$PREV" != none ]; then IMAGE_TAG=$PREV docker compose up -d; fi
exit 1
fiO script guarda a última tag saudável em current_tag. Se o health check falhar após as tentativas, ele sobe a versão anterior e retorna erro, o que marca o job como falho no GitHub. Lembre que o servidor também precisa estar autenticado no GHCR (docker login com um token de leitura de pacotes) para executar o pull.
Agora adicione o job de deploy ao workflow:
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: |
mkdir -p ~/.ssh
echo '${{ secrets.DEPLOY_KEY }}' > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts
ssh deploy@${{ secrets.DEPLOY_HOST }} 'IMAGE_TAG=${{ github.sha }} /opt/app/deploy.sh'Cadastre DEPLOY_KEY e DEPLOY_HOST em Settings > Secrets and variables > Actions. Gere uma chave exclusiva para o deploy, com acesso restrito a esse usuário.
Trade-offs e boas práticas
Secrets e permissões
Aplique o princípio do menor privilégio: o GITHUB_TOKEN com escopo mínimo por job, chave SSH dedicada e, quando possível, ambientes protegidos (environments) com aprovação manual antes do deploy em produção. Nunca imprima secrets em logs.
Runners hospedados ou self-hosted
Runners hospedados pelo GitHub reduzem manutenção, mas têm custo por minuto em repositórios privados e não acessam redes internas. Runners self-hosted dão controle e acesso à rede privada, porém exigem patching, isolamento e monitoramento. Para um ci/cd pipeline que faz deploy em infraestrutura própria, avalie o runner dentro da sua VPC.
Limites do modelo com SSH
O deploy por SSH é simples, mas não oferece deploy sem downtime nativo, escalonamento ou balanceamento. Quando o produto crescer, o mesmo ci/cd pipeline pode ser adaptado para publicar em um orquestrador como Kubernetes, trocando apenas o último estágio. Mantenha o build e a tag por SHA: essa parte permanece igual.
Observabilidade do deploy
Registre o SHA em execução (por exemplo, em um endpoint /version ou em labels do container) e correlacione com métricas e logs. Assim, quando uma regressão aparecer, você identifica rapidamente qual entrega a causou. Um ci/cd pipeline sem rastreabilidade só automatiza o problema.
Conclusão
Com testes, build imutável por SHA, registry, deploy via SSH e rollback automático, você tem um ci/cd pipeline funcional em poucas dezenas de linhas, fácil de evoluir. O próximo passo é medir a frequência de deploys e o tempo de recuperação, e comunicar as entregas ao mercado com consistência: veja como organizar isso em calendário editorial para SaaS e transformar cada release em conteúdo.