Todo o trabalho construído nos artigos anteriores — testes, build de imagens, gerenciamento de secrets — converge em um único objetivo: colocar código novo em produção de forma confiável, rápida e repetível. O deploy é o último metro dessa jornada, e é onde a maioria dos times sente mais ansiedade.
Essa ansiedade tem origem histórica. Quando deploys eram eventos raros e manuais, cada um carregava o peso acumulado de semanas de mudanças. O risco era proporcional ao tamanho do lote. Com CI/CD bem implementado, deploys são frequentes, pequenos e automatizados — e a ansiedade desaparece porque o risco por deploy torna-se mínimo.
Este artigo cobre as estratégias e implementações concretas de deploy automático usando GitHub Actions, do caso mais simples ao mais sofisticado.
Os Três Modelos de Deploy
Antes de escrever qualquer pipeline, é necessário entender que existem três modelos fundamentais de deploy, cada um adequado a contextos diferentes.
Push-based deployment — o pipeline de CI/CD conecta ativamente ao servidor de destino e executa os comandos de atualização. É o modelo mais direto e mais comum em infraestruturas tradicionais com servidores fixos. O pipeline "empurra" a mudança para o servidor.
Pull-based deployment — o servidor de destino monitora um registro de imagens ou um repositório e aplica as mudanças por conta própria quando detecta uma nova versão. É o modelo usado pelo ArgoCD e pelo FluxCD em ambientes Kubernetes. O servidor "puxa" a mudança do registro.
Artifact-based deployment — o pipeline gera um artefato imutável — uma imagem Docker, um pacote ZIP, um binário compilado — que é armazenado em um registro. O deploy consiste em instruir o ambiente de destino a usar aquele artefato específico. É o modelo usado pelo AWS ECS, pelo Elastic Beanstalk e pelo Heroku.
Os exemplos deste artigo cobrem principalmente o modelo push-based para servidores próprios e o artifact-based para serviços gerenciados, que são os mais relevantes para quem está começando.
Deploy Simples via SSH
O caso mais básico: um servidor Linux com Docker instalado, acessível via SSH. O pipeline conecta, baixa a nova imagem e reinicia o container.
Configuração inicial no servidor:
# No servidor de produção
# Cria o usuário de deploy sem privilégios desnecessários
sudo useradd -m -s /bin/bash deploy
sudo usermod -aG docker deploy
# Cria diretório para a aplicação
sudo mkdir -p /opt/minha-api
sudo chown deploy:deploy /opt/minha-api
# Cria o arquivo de variáveis de ambiente da aplicação
sudo tee /opt/minha-api/.env > /dev/null << 'EOF'
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://app:senha@localhost:5432/producao
REDIS_URL=redis://localhost:6379
EOF
sudo chmod 600 /opt/minha-api/.env
sudo chown deploy:deploy /opt/minha-api/.env
Gerando e configurando a chave SSH para o pipeline:
# Na máquina local — gera um par de chaves dedicado para o pipeline
ssh-keygen -t ed25519 -C "github-actions-deploy" \
-f ~/.ssh/github_actions_deploy \
-N ""
# Copia a chave pública para o servidor
ssh-copy-id -i ~/.ssh/github_actions_deploy.pub deploy@seu-servidor.com
# Adiciona a chave privada como secret no repositório
gh secret set DEPLOY_SSH_KEY < ~/.ssh/github_actions_deploy
gh secret set DEPLOY_HOST --body "seu-servidor.com"
gh secret set DEPLOY_USER --body "deploy"
O workflow de deploy:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
env:
IMAGE: ghcr.io/${{ github.repository }}:${{ github.sha }}
jobs:
# ── Etapa 1: CI (reutiliza o workflow existente) ──
ci:
uses: ./.github/workflows/ci.yml
# ── Etapa 2: Build e publicação da imagem ─────────
build:
needs: ci
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image: ${{ env.IMAGE }}
steps:
- uses: actions/checkout@v4
- name: Login no GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Configura Buildx
uses: docker/setup-buildx-action@v3
- name: Constrói e publica imagem
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ env.IMAGE }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ── Etapa 3: Deploy em staging ────────────────────
deploy-staging:
needs: build
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.minha-api.com
steps:
- name: Configura SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.STAGING_SSH_KEY }}
- name: Registra host no known_hosts
run: |
ssh-keyscan -H ${{ secrets.STAGING_HOST }} >> ~/.ssh/known_hosts
- name: Executa deploy em staging
env:
IMAGE: ${{ needs.build.outputs.image }}
run: |
ssh deploy@${{ secrets.STAGING_HOST }} bash -s << ENDSSH
set -e
# Autentica no registro
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io -u ${{ github.actor }} --password-stdin
# Baixa a nova imagem
docker pull $IMAGE
# Para o container atual graciosamente
docker stop minha-api-staging 2>/dev/null || true
docker rm minha-api-staging 2>/dev/null || true
# Sobe o novo container
docker run -d \
--name minha-api-staging \
--env-file /opt/minha-api/.env \
--network host \
--restart unless-stopped \
--log-driver json-file \
--log-opt max-size=100m \
--log-opt max-file=3 \
$IMAGE
# Aguarda o healthcheck
echo "Aguardando aplicação ficar saudável..."
for i in \$(seq 1 30); do
STATUS=\$(curl -s -o /dev/null -w "%{http_code}" \
http://localhost:3000/health 2>/dev/null || echo "000")
if [ "\$STATUS" = "200" ]; then
echo "Aplicação saudável após \${i}s"
exit 0
fi
sleep 1
done
echo "Aplicação não ficou saudável em 30s"
docker logs minha-api-staging --tail 50
exit 1
ENDSSH
# ── Etapa 4: Smoke tests em staging ───────────────
smoke-tests:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Executa smoke tests
env:
BASE_URL: https://staging.minha-api.com
run: |
# Verifica endpoints críticos
check_endpoint() {
local endpoint=$1
local expected=$2
local status=$(curl -s -o /dev/null -w "%{http_code}" "$BASE_URL$endpoint")
if [ "$status" != "$expected" ]; then
echo "FALHA: $endpoint retornou $status, esperado $expected"
return 1
fi
echo "OK: $endpoint retornou $status"
}
check_endpoint "/health" "200"
check_endpoint "/api/v1/status" "200"
check_endpoint "/api/v1/nao-existe" "404"
check_endpoint "/api/v1/protegido" "401"
# ── Etapa 5: Deploy em produção ───────────────────
deploy-producao:
needs: smoke-tests
runs-on: ubuntu-latest
environment:
name: production
url: https://api.minha-api.com
steps:
- name: Configura SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.PROD_SSH_KEY }}
- name: Registra host no known_hosts
run: |
ssh-keyscan -H ${{ secrets.PROD_HOST }} >> ~/.ssh/known_hosts
- name: Deploy com zero downtime em produção
env:
IMAGE: ${{ needs.build.outputs.image }}
HOST: ${{ secrets.PROD_HOST }}
run: |
ssh deploy@$HOST bash -s << 'ENDSSH'
set -euo pipefail
IMAGE="${{ env.IMAGE }}"
CONTAINER_ATUAL="minha-api"
CONTAINER_NOVO="minha-api-new"
PORTA_PROD=3000
PORTA_CANARY=3001
echo "=== Iniciando deploy zero-downtime ==="
echo "Imagem: $IMAGE"
# Autentica e baixa a nova imagem ANTES de derrubar a atual
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker pull $IMAGE
# Sobe o novo container em porta alternativa
docker run -d \
--name $CONTAINER_NOVO \
--env-file /opt/minha-api/.env \
--restart unless-stopped \
-p $PORTA_CANARY:3000 \
$IMAGE
# Aguarda o novo container ficar saudável
echo "Verificando saúde do novo container..."
SAUDAVEL=false
for i in $(seq 1 60); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
http://localhost:$PORTA_CANARY/health 2>/dev/null || echo "000")
if [ "$STATUS" = "200" ]; then
echo "Novo container saudável após ${i}s"
SAUDAVEL=true
break
fi
sleep 1
done
if [ "$SAUDAVEL" != "true" ]; then
echo "ERRO: Novo container não ficou saudável"
echo "Logs do novo container:"
docker logs $CONTAINER_NOVO --tail 100
docker rm -f $CONTAINER_NOVO
exit 1
fi
# Transfere o tráfego — atualiza o nginx para apontar para a nova porta
# (em um cenário real, aqui seria uma atualização de upstream do nginx)
echo "Transferindo tráfego para o novo container..."
# Para o container antigo graciosamente
docker stop $CONTAINER_ATUAL 2>/dev/null || true
# Sobe o novo container na porta de produção
docker rm $CONTAINER_ATUAL 2>/dev/null || true
docker rm $CONTAINER_NOVO
docker run -d \
--name $CONTAINER_ATUAL \
--env-file /opt/minha-api/.env \
--restart unless-stopped \
-p $PORTA_PROD:3000 \
$IMAGE
# Remove imagens antigas para liberar espaço
docker image prune -f
echo "=== Deploy concluído com sucesso ==="
ENDSSH
- name: Verifica deploy em produção
run: |
sleep 5
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
https://api.minha-api.com/health)
if [ "$STATUS" != "200" ]; then
echo "ERRO: Produção não está saudável — status $STATUS"
exit 1
fi
echo "Produção saudável — status $STATUS"
# ── Etapa 6: Notificação ──────────────────────────
notificar:
needs: [deploy-producao]
runs-on: ubuntu-latest
if: always()
steps:
- name: Notifica resultado no Slack
uses: slackapi/slack-github-action@v1.26.0
with:
payload: |
{
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "${{ needs.deploy-producao.result == 'success' && '✅ *Deploy bem-sucedido*' || '❌ *Deploy falhou*' }}\n*Repositório:* ${{ github.repository }}\n*Branch:* ${{ github.ref_name }}\n*Commit:* `${{ github.sha }}`\n*Autor:* ${{ github.actor }}"
}
}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Rollback Automatizado
Um deploy automatizado sem rollback automatizado está incompleto. Quando o deploy de produção falha, cada segundo conta. O rollback deve ser tão simples quanto o deploy:
# .github/workflows/rollback.yml
name: Rollback de Emergência
on:
workflow_dispatch:
inputs:
versao:
description: 'SHA do commit para o qual fazer rollback'
required: true
type: string
ambiente:
description: 'Ambiente de destino'
required: true
type: choice
options:
- staging
- production
jobs:
rollback:
runs-on: ubuntu-latest
environment: ${{ inputs.ambiente }}
steps:
- name: Valida que a imagem existe no registro
run: |
IMAGE="ghcr.io/${{ github.repository }}:${{ inputs.versao }}"
docker manifest inspect $IMAGE > /dev/null 2>&1 || {
echo "ERRO: Imagem $IMAGE não encontrada no registro"
exit 1
}
echo "Imagem validada: $IMAGE"
- name: Configura SSH
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets[format('{0}_SSH_KEY', inputs.ambiente == 'production' && 'PROD' || 'STAGING')] }}
- name: Executa rollback
env:
IMAGE: ghcr.io/${{ github.repository }}:${{ inputs.versao }}
HOST: ${{ inputs.ambiente == 'production' && secrets.PROD_HOST || secrets.STAGING_HOST }}
run: |
ssh-keyscan -H $HOST >> ~/.ssh/known_hosts
ssh deploy@$HOST bash -s << ENDSSH
set -e
echo "Iniciando rollback para ${{ inputs.versao }}..."
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker pull ${{ env.IMAGE }}
docker stop minha-api 2>/dev/null || true
docker rm minha-api 2>/dev/null || true
docker run -d \
--name minha-api \
--env-file /opt/minha-api/.env \
--restart unless-stopped \
-p 3000:3000 \
${{ env.IMAGE }}
sleep 5
STATUS=\$(curl -s -o /dev/null -w "%{http_code}" \
http://localhost:3000/health)
if [ "\$STATUS" != "200" ]; then
echo "ERRO CRÍTICO: Rollback falhou — aplicação não está saudável"
exit 1
fi
echo "Rollback concluído — aplicação rodando versão ${{ inputs.versao }}"
ENDSSH
- name: Registra rollback no audit log
run: |
echo "ROLLBACK executado por ${{ github.actor }}" >> audit.log
echo "Versão: ${{ inputs.versao }}" >> audit.log
echo "Ambiente: ${{ inputs.ambiente }}" >> audit.log
echo "Data: $(date -u)" >> audit.log
Deploy para AWS ECS com GitHub Actions
Para quem usa containers gerenciados na AWS, o fluxo de deploy muda: em vez de conectar via SSH a servidores, instrui-se o ECS a usar a nova imagem:
deploy-ecs:
needs: build
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Configura credenciais AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_DEPLOY_ROLE_ARN }}
aws-region: us-east-1
- name: Login no ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
- name: Faz tag e push da imagem para o ECR
env:
ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
ECR_REPOSITORY: minha-api
IMAGE_TAG: ${{ github.sha }}
run: |
docker pull ghcr.io/${{ github.repository }}:${{ github.sha }}
docker tag ghcr.io/${{ github.repository }}:${{ github.sha }} \
$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
- name: Atualiza task definition do ECS
id: task-def
uses: aws-actions/amazon-ecs-render-task-definition@v1
with:
task-definition: infrastructure/ecs-task-definition.json
container-name: minha-api
image: ${{ steps.login-ecr.outputs.registry }}/minha-api:${{ github.sha }}
- name: Deploy no ECS
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
task-definition: ${{ steps.task-def.outputs.task-definition }}
service: minha-api-service
cluster: producao
wait-for-service-stability: true
wait-for-minutes: 10
Estratégia de Promoção entre Ambientes
Em projetos maduros, o código não vai direto do repositório para produção. Ele percorre uma cadeia de ambientes, sendo promovido apenas quando aprovado em cada etapa:
Commit → CI → Build → Staging → QA → Pre-prod → Produção
O GitHub Actions modela isso com workflows encadeados e aprovações:
# A promoção para produção só acontece após aprovação explícita
# configurada em Settings → Environments → production → Required reviewers
jobs:
deploy-staging:
# Automático — acontece a cada merge na main
environment: staging
deploy-preprod:
needs: deploy-staging
# Automático após staging — mas requer que os testes passem
environment: pre-production
if: needs.deploy-staging.result == 'success'
deploy-producao:
needs: deploy-preprod
# Requer aprovação manual configurada no GitHub Environment
environment: production
if: needs.deploy-preprod.result == 'success'
Essa estrutura garante que nenhum código chega a produção sem ter passado por todos os ambientes intermediários e sem aprovação explícita de alguém com autoridade para isso.
O Que Vem a Seguir
O próximo artigo fecha o Módulo 4 abordando notificações, logs e monitoramento do pipeline — como configurar alertas para falhas, como interpretar logs de pipeline para diagnóstico rápido e como integrar o status do pipeline com ferramentas de comunicação da equipe.
Referências para Aprofundamento
Documentação oficial - GitHub Actions — Deploying with GitHub Actions — Guia oficial com exemplos de deploy para múltiplas plataformas incluindo AWS, Azure e GCP. - AWS ECS Deploy Task Definition — GitHub — Action oficial da AWS para deploy no ECS com documentação detalhada de todos os parâmetros.
Estratégias de deploy - Deployment Strategies — Martin Fowler — Artigo clássico de Martin Fowler sobre Blue-Green Deployment, uma das estratégias de zero-downtime mais usadas. - Canary Releases — Martin Fowler — Explicação detalhada de Canary Releases com casos de uso e considerações práticas.
Ferramentas - webfactory/ssh-agent — GitHub — Action para gerenciamento de chaves SSH em runners do GitHub Actions. - slackapi/slack-github-action — GitHub — Action oficial do Slack para notificações de workflows com suporte a Block Kit para mensagens ricas.