Destino Dev

Blog

GitHub Actions para iniciantes: build Docker, push e deploy via SSH

Por John Pires · 08 de março de 2026

Introducao ao problema

O que vamos automatizar

Você tem uma aplicação (por exemplo, uma API Node.js ou um serviço em Go) que precisa ser empacotada em uma imagem Docker, enviada para um registry e atualizada automaticamente em um servidor de produção via SSH. Fazer isso manualmente é sujeito a esquecimento, erros de versão e downtime desnecessário. O objetivo é implementar uma pipeline simples e robusta usando GitHub Actions para automatizar build, push e deploy via SSH.

Este fluxo cobre três operações críticas: build da imagem, push para um registry (Docker Hub ou GitHub Container Registry) e deploy remoto executando comandos via SSH no servidor alvo. Cada etapa deve ser reproduzível e segura (uso de secrets), além de fornecer logs para depuração.

Vou explicar conceitos do zero com analogias diretas: pense no processo como uma linha de montagem industrial — o código fonte entra, as máquinas constroem o produto (Docker build), o caminhão leva o produto ao depósito (push para registry) e um gerente remoto instrui o depósito a colocar o novo produto na prateleira (SSH executa docker pull + restart).

Por que usar Actions + Docker + SSH

GitHub Actions fornece integração estreita com o repositório — cada push ou merge pode disparar a pipeline automaticamente. Docker garante que o ambiente de execução seja consistente. SSH ainda é o método mais direto para controlar servidores que você já administra (sem necessidade de serviços de deploy terceirizados).

Vantagens práticas: reprodutibilidade, versionamento da imagem (tags), rollback controlado (mantendo imagens anteriores no registry) e logs centralizados do GitHub Actions. Desvantagens a ter em mente: é preciso proteger chaves SSH e tokens do registry, além de configurar o servidor para permitir atualizações via Docker.

Regra prática: não copie chaves nem tokens no repositório. Sempre use GitHub Secrets e restringa permissões no servidor.

Pre-requisitos e setup

Ambiente local

Pré-requisitos no seu PC de desenvolvimento:

  • Git configurado e repositório no GitHub
  • Conta em um registry (Docker Hub ou GitHub Container Registry)
  • Docker instalado para testar localmente

Você precisa validar localmente o Dockerfile e garantir que a aplicação roda em container antes de configurar a pipeline. Isso reduz ciclos de tentativa/erro no CI.

Servidor remoto e chaves SSH

No servidor de destino você deve ter Docker instalado e um usuário com permissão para executar containers (ou docker-compose). Vamos usar SSH para conectar e executar os comandos de atualização.

Gere uma chave SSH sem passphrase e adicione a public key ao servidor (authorized_keys). Exemplo de geração e cópia:

O código abaixo cria a chave e copia a chave pública para o servidor remoto. Antes de rodar, substitua user@host pela sua conexão.

# Gera uma chave SSH sem passphrase (execute localmente)
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/id_ed25519_github_actions -N ''
# Copia a chave pública para o servidor remoto (substitua user@host)
ssh-copy-id -i ~/.ssh/id_ed25519_github_actions.pub user@host

Depois de executar, você terá a chave privada que será adicionada como GitHub Secret (por exemplo, SSH_PRIVATE_KEY). Adicione também o user@host ao host known_hosts ou configure o workflow para aceitar a fingerprint de forma segura.

Crie os seguintes Secrets no repositório GitHub (Settings > Secrets):

  • DOCKER_USERNAME e DOCKER_PASSWORD (ou GITHUB_TOKEN/CR_PAT para registries)
  • SSH_PRIVATE_KEY (conteúdo do arquivo id_ed25519_github_actions)
  • SERVER_USER e SERVER_HOST (opcionais, para não hardcodar)

Tutorial guiado passo a passo

Dockerfile e build

Exemplo real: API Node.js. Este Dockerfile produz uma imagem minimalista com multi-stage build para reduzir tamanho final.

# Dockerfile
# Stage de build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --production=false
COPY . .
RUN npm run build

# Stage de runtime
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/package*.json ./
RUN npm ci --production
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["node", "dist/index.js"]

O que este Dockerfile faz: no primeiro estágio instala dependências de desenvolvimento e roda o build (por exemplo, transpilar TypeScript). No segundo estágio instala somente dependências de produção e copia o build final, resultando em uma imagem menor. Depois de executar docker build localmente, você terá uma imagem pronta para rodar em produção.

Teste local do build e run:

# build localmente com tag de teste
docker build -t myapp:local .
# rodar localmente
docker run -p 3000:3000 --rm myapp:local

Se o container inicia e responde, o Dockerfile está pronto para o CI.

GitHub Actions workflow

A seguir, um workflow completo que:

  1. Dispara em push na branch main
  2. Faz check-out do código
  3. Login no Docker Hub
  4. Builda e push da imagem
  5. Conecta via SSH e executa comandos de deploy (docker pull + restart)

Coloque o arquivo em .github/workflows/ci-cd.yml

# .github/workflows/ci-cd.yml
name: CI CD Build and Deploy

on:
  push:
    branches: [ main ]

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Set up QEMU (if needed for cross-build)
        uses: docker/setup-qemu-action@v2

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }}

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v0.1.8
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          port: 22
          script: |
            docker pull ${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }}
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 3000:3000 ${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }}

Explicação: o job usa o docker/build-push-action para construir e empurrar a imagem com tag igual ao SHA do commit. Em seguida, appleboy/ssh-action conecta ao servidor usando a chave privada (guardada em secrets) e executa comandos para atualizar o container.

O que acontece após o push: a imagem com tag específica é armazenada no registry. O SSH se conecta e força o servidor a baixar a nova imagem e executar o container. O uso de tags imutáveis (sha) evita problemas de cache e garante rollback fácil — basta redeploy de outra tag existente.

Servidor: docker-compose alternativa

Se no servidor você preferir docker-compose, crie um docker-compose.yml com imagem parametrizada e use docker-compose pull + up -d no script SSH. Exemplo:

# docker-compose.yml (exemplo server side)
version: '3.8'
services:
  web:
    image: yourdockeruser/myapp:latest
    ports:
      - "3000:3000"
    restart: always

Depois altere o script de deploy no workflow para:

docker-compose pull web
docker-compose up -d --no-deps web

Com docker-compose você centraliza configurações como variáveis de ambiente e volumes no server.

Erros comuns e armadilhas

A seguir problemas que aparecerão no caminho e como evitá-los.

Chaves SSH mal configuradas: se o host não aceitar a key, a ação falhará com erro de autenticação. Verifique authorized_keys, permissões (chmod 600 na private key local), e se o usuário tem permissão para executar Docker.

Secrets incorretos: variáveis com nomes errados levam a falhas de login no registry. Sempre teste as credenciais localmente (docker login).

Imagens sem tag imutável: usar 'latest' sem versão impede rollback e causa problemas de cache. Prefira tags por SHA ou semver.

Permissões do Docker no servidor: se o usuário remoto não estiver no grupo docker, será necessário sudo. Ajuste o script para usar sudo docker ... ou configure permissões adequadas.

  1. Problema: tempo limite de conexão SSH — cheque firewall e porta.
  2. Problema: ação appleboy/ssh-action não encontra known_hosts — valide fingerprint no script ou pré-popule.

Dica rápida:

Teste cada camada localmente: primeiro Docker build/run, depois docker pull em um servidor de teste, finalmente conecte via SSH e execute os comandos manualmente antes de automatizar.

Resultado final funcionando

Como verificar

Checklist para confirmar que o fluxo está operacional:

  • Ao dar push na branch main: workflow inicia no GitHub Actions
  • Job de build conclui sem erros e image é enviada ao registry
  • Etapa SSH retorna sucesso e os comandos de deploy são executados
  • O serviço no servidor responde na porta esperada

Valide também logs do container no servidor:

# comandos no servidor para checar
docker logs -f myapp
# ou, se usar docker-compose
docker-compose logs -f web

Se os logs mostrarem inicialização correta, o deploy foi bem-sucedido. Caso contrário, os logs ajudarão a identificar erros de runtime (variáveis de ambiente, portas, dependências externas).

Logs e troubleshooting

GitHub Actions fornece logs detalhados por etapa. Se a etapa de deploy via SSH falhar, verifique o output da action appleboy (ela imprime stdout/stderr do remoto). Para problemas de build/push, verifique se as credenciais do registry estão válidas e se o nome da imagem está correto.

Passos práticos para diagnosticar:

  1. Rodar docker build e push localmente usando as mesmas credenciais.
  2. SSH manual para o servidor e executar docker pull manual.
  3. Verificar permissões e versões do Docker no servidor.

Desafio para o leitor praticar

Tarefas obrigatórias

Implemente o pipeline no seu repositório e conclua as seguintes tarefas:

  • Automatizar build e push para uma imagem taggeada com SHA
  • Configurar deploy via SSH que reinicie o serviço com a nova imagem
  • Testar rollback: execute o deploy de uma tag anterior e valide que o serviço volta ao estado anterior

Documente no README do projeto os secrets que precisa configurar e os comandos manuais que você usou para validar cada passo.

Extra: monitoramento e rollback

Para avançar, adicione monitoramento simples que verifique health endpoint após deploy. Exemplo de script no servidor que faz um curl para /health e tenta rollback em caso de falha:

# exemplo de script de deploy com healthcheck (server-side)
#!/bin/bash
IMAGE=$1
docker pull $IMAGE
docker stop myapp || true
docker rm myapp || true
docker run -d --name myapp -p 3000:3000 $IMAGE
sleep 5
if ! curl -fsS http://localhost:3000/health; then
  echo "Healthcheck failed, rolling back"
  # aqui você poderia puxar a tag anterior salva em um arquivo
  docker pull yourdockeruser/myapp:previous
  docker stop myapp || true
  docker rm myapp || true
  docker run -d --name myapp -p 3000:3000 yourdockeruser/myapp:previous
fi

Guardar a tag anterior em um pequeno arquivo no servidor (por exemplo, LAST_DEPLOY_TAG) facilita o rollback manual ou automatizado.

Conclusao pratica

Automatizar build, push e deploy com GitHub Actions e SSH é uma solução direta e poderosa para equipes pequenas que controlam seus próprios servidores. O fluxo apresentado é minimalista, seguro quando se usa secrets, e permite rollback e rastreabilidade por tags imutáveis.

Checklist final prático:

  • Validar Dockerfile localmente
  • Adicionar secrets no GitHub (SSH_PRIVATE_KEY, DOCKER_USERNAME, DOCKER_PASSWORD, SERVER_HOST, SERVER_USER)
  • Implementar workflow conforme exemplo e testar com commits na branch main
  • Monitorar logs e implementar healthchecks e políticas de rollback

Resumo técnico: prefira tags imutáveis (sha), mantenha chaves fora do repositório, teste cada camada localmente e use logs do Actions + logs do container para solução de problemas. Com esses passos é possível ter deploys previsíveis e menos tempo gasto em tarefas operacionais repetitivas.

Próxima ação recomendada: implemente o pipeline no seu repositório de testes e compartilhe o primeiro log de sucesso com o time — evidência prática vale mais que teoria.

Assuntos: DevOps, Docker, github-actions, ci-cd, ssh

Leia também