Um deploy automatizado bem desenhado é o que separa times que publicam com segurança de times que dependem de acesso SSH manual e de torcida a cada release. Neste tutorial, mostro um fluxo enxuto e reproduzível: build da imagem, push para um registry, atualização no servidor via Docker Compose, verificação de saúde e rollback. É um padrão que atende bem SaaS pequenos e médios, sem exigir Kubernetes desde o primeiro dia.
Pré-requisitos e arquitetura do fluxo
O fluxo tem quatro peças: repositório Git, runner de CI, registry de imagens e um servidor (VM em qualquer cloud) com Docker e Docker Compose. A lógica é simples: um push na branch main dispara o pipeline, que gera uma imagem tagueada com o SHA do commit e aciona um script remoto que troca a versão em execução.
- Repositório com Dockerfile funcional e endpoint
/healthna aplicação. - Registry: GitHub Container Registry (ghcr.io), Docker Hub ou um registry privado.
- Servidor Linux com Docker Engine e plugin Compose instalados.
- Usuário de deploy dedicado, com chave SSH própria e permissão apenas para o necessário.
Se você ainda não padronizou a execução dos containers em produção, vale ler antes o Docker Compose em Produção: Tutorial Passo a Passo para Times de TI, que cobre volumes, secrets e backup.
Passo 1: tagueie imagens de forma imutável
O erro mais comum em um deploy automatizado é usar apenas a tag latest. Ela é mutável, dificulta auditoria e torna o rollback impreciso. Use o SHA do commit como tag: cada imagem aponta para exatamente um estado do código, e voltar versão passa a ser trocar uma variável.
No docker-compose.yml do servidor, referencie a tag por variável de ambiente:
services:
app:
image: ghcr.io/sua-org/app:${APP_TAG}
restart: unless-stopped
ports:
- '127.0.0.1:8080:8080'
env_file: .env
Passo 2: configure o pipeline no GitHub Actions
Crie o arquivo .github/workflows/deploy.yml. O job faz checkout, autentica no registry, constrói e publica a imagem e, por fim, executa o script remoto por SSH. Confira sempre a documentação oficial de cada action e fixe versões para evitar mudanças inesperadas no pipeline.
name: deploy
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ghcr.io/sua-org/app:${{ github.sha }}
- uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: /opt/app/deploy.sh ${{ github.sha }}
Cadastre SSH_HOST, SSH_USER e SSH_KEY em Settings > Secrets and variables > Actions. Nunca versione chaves ou arquivos .env no repositório.
Passo 3: escreva o script de deploy com healthcheck e rollback
A parte que dá confiabilidade ao deploy automatizado fica no servidor. O script registra a tag em execução, sobe a nova versão, testa o endpoint de saúde e, se falhar, restaura a tag anterior.
#!/usr/bin/env bash
set -euo pipefail
cd /opt/app
NEW=$1
OLD=$(cat .current_tag 2>/dev/null || echo none)
export APP_TAG=$NEW
docker compose pull app
docker compose up -d app
for i in $(seq 1 20); do
if curl -fsS http://127.0.0.1:8080/health >/dev/null; then
echo $NEW > .current_tag
exit 0
fi
sleep 3
done
echo 'Healthcheck falhou, executando rollback' >&2
if [ $OLD != none ]; then
export APP_TAG=$OLD
docker compose up -d app
fi
exit 1
Dê permissão com chmod +x /opt/app/deploy.sh. Como o script sai com código 1 em caso de falha, o job do GitHub Actions fica vermelho e o time é notificado, enquanto o serviço continua na versão anterior.
Autenticação do servidor no registry
Para o docker compose pull funcionar com imagens privadas, faça docker login ghcr.io uma vez no servidor, usando um token com escopo apenas de leitura de pacotes. Não reaproveite o token de escrita do pipeline.
Trade-offs e limites desse modelo
Esse desenho é simples e barato, mas tem limites reais que você precisa conhecer antes de adotá-lo:
- Downtime curto: o
up -drecria o container, e pode haver uma janela de indisponibilidade. Para zero downtime, avalie um proxy reverso com dois containers (blue/green) ou um orquestrador. - Migrações de banco: rollback de imagem não desfaz alterações de schema. Adote migrações compatíveis com a versão anterior (expand/contract).
- Servidor único: sem alta disponibilidade. Se o host cair, o serviço cai.
- Acesso SSH pelo CI: exige proteção da chave, usuário restrito e, idealmente, firewall limitando origens.
Para quem precisa de mais escala, o mesmo pipeline pode evoluir para Kubernetes ou outro orquestrador, mantendo a imagem imutável como contrato entre build e execução.
Boas práticas para evoluir o pipeline
- Adicione testes antes do build e bloqueie o deploy se falharem.
- Use environments do GitHub com aprovação manual para produção.
- Escaneie imagens com uma ferramenta de vulnerabilidades no próprio pipeline.
- Centralize logs e alertas para correlacionar cada release a variações de erro e latência.
- Revise custos da infraestrutura que hospeda o fluxo; o post Custo de Cloud Estrangeiro: o que eu mudaria hoje para cortar até 70% mostra por onde começar.
Teste o rollback de propósito, publicando uma versão com /health quebrado em ambiente de homologação. Um deploy automatizado só é confiável quando o caminho de falha foi exercitado, não apenas o caminho feliz.
Conclusão
Com imagens imutáveis, pipeline em GitHub Actions e um script com healthcheck e rollback, você ganha um deploy automatizado auditável, repetível e barato de operar. Comece pequeno, meça o tempo de cada release e evolua para blue/green ou orquestração quando o produto exigir. O ganho real está em tornar publicar algo rotineiro e seguro, liberando o time para entregar valor em vez de apagar incêndio.