Build e push de imagem Docker com GitHub Actions

Construir imagens Docker no CI e publicar em um registry é o padrão para deploys reproduzíveis, a VPS sempre executa exatamente a imagem que passou pelos testes. GitHub Container Registry (GHCR) é gratuito para repositórios públicos e privados com GitHub Free, eliminando a necessidade de Docker Hub ou registries pagos.

Workflow completo de build e push

Build da imagem Docker e push para GitHub Container Registry com cache:

yaml
# .github/workflows/docker-build.yml
name: Build e Push Docker

on:
  push:
    branches: [main, develop]
    tags: ['v*.*.*']
  pull_request:
    branches: [main]

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}   # ex: usuario/minha-api

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write              # necessário para push no GHCR

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

      - name: Login no GHCR
        if: github.event_name != 'pull_request'
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}   # automático, sem config

      - name: Extrair metadata (tags e labels)
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=sha-

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

      - name: Build e Push
        uses: docker/build-push-action@v6
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha        # cache do GitHub Actions
          cache-to: type=gha,mode=max

Estratégia de tags automáticas

O metadata-action gera tags automaticamente baseadas em branch, tag e commit:

bash
# Tags geradas automaticamente pelo metadata-action:

# Push no branch main:
# ghcr.io/usuario/app:main
# ghcr.io/usuario/app:sha-abc1234

# Push de tag v1.2.3:
# ghcr.io/usuario/app:1.2.3
# ghcr.io/usuario/app:1.2
# ghcr.io/usuario/app:latest (tag semver com latest=true)

# Pull Request:
# Imagem buildada mas não publicada (push: false)
# Útil para verificar que o build não quebra

# Usar a imagem na VPS:
# docker compose pull
# docker compose up -d

# docker-compose.yml na VPS:
# services:
#   app:
#     image: ghcr.io/usuario/minha-api:main
#     # ou: image: ghcr.io/usuario/minha-api:1.2.3

Pull de imagem privada na VPS

Para fazer pull de imagem privada do GHCR na VPS, autentique o Docker:

bash
# Na VPS — autenticar no GHCR:
# 1. Criar Personal Access Token no GitHub:
#    Settings → Developer Settings → Personal access tokens (classic)
#    Scopes: read:packages

# 2. Login na VPS:
echo "SEU_GITHUB_TOKEN" | docker login ghcr.io -u SEU_USUARIO --password-stdin

# 3. Verificar autenticação:
cat ~/.docker/config.json | grep ghcr.io

# 4. Docker Compose puxa automaticamente no deploy:
docker compose pull app
docker compose up -d --no-deps app

# Alternativa via GitHub Actions secret (mais seguro):
# No workflow, injete o token via SSH:
# ssh deploy@VPS "echo '${{ secrets.GITHUB_TOKEN }}' | docker login ghcr.io -u ${{ github.actor }} --password-stdin"

Multi-platform: build para ARM e AMD64

Para VPS ARM (Ampere, Graviton) ou Raspberry Pi, faça build multi-platform:

yaml
      # Habilitar QEMU para emulação
      - name: Configurar QEMU
        uses: docker/setup-qemu-action@v3

      - name: Configurar Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build multi-platform
        uses: docker/build-push-action@v6
        with:
          context: .
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

# O Docker seleciona automaticamente a imagem correta
# para a arquitetura da máquina que faz o pull:
# VPS AMD64 → pega a camada linux/amd64
# VPS ARM64 → pega a camada linux/arm64

Verificar vulnerabilidades na imagem com Trivy

Escaneie a imagem por CVEs no próprio CI antes de fazer push:

yaml
      - name: Scan de vulnerabilidades com Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
          format: 'table'
          exit-code: '1'            # falhar o CI se encontrar CVE crítica
          ignore-unfixed: true
          severity: 'CRITICAL,HIGH'

      # Alternativa: gerar SBOM (Software Bill of Materials)
      - name: Gerar SBOM
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
          format: 'cyclonedx'
          output: 'sbom.json'

      - name: Upload SBOM como artefato
        uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.json

Não quer configurar manualmente?

Implante o VPS em menos de 5 minutos com a Runstack. Infraestrutura da OPEN Datacenter, com servidores em São Paulo.

Perguntas frequentes

GHCR: integrado ao GitHub, autenticação com GITHUB_TOKEN (sem configuração), gratuito para repos públicos e para orgs com GitHub Free. Docker Hub: o registry padrão, mas tem rate limit para pulls (100/6h anônimo, 200/6h autenticado free). Para projetos no GitHub: GHCR é a escolha natural. Para imagens públicas amplamente usadas: Docker Hub ainda tem melhor alcance.

Sem cache: build completo em cada push (~2-5 min para Node.js). Com cache: apenas as camadas modificadas são reconstruídas (~30s-1min para mudanças só de código). O cache é invalidado quando Dockerfile ou arquivos do COPY mudam. GitHub Actions oferece 10 GB de cache por repositório.

Use paths filter no on.push para disparar apenas quando o diretório relevante mudou: paths: ["apps/api/**", "packages/**"]. Combine com dorny/paths-filter action para condicionar jobs específicos: uses: dorny/paths-filter@v3 e if: steps.filter.outputs.api == "true".

Por padrão, imagens no GHCR herdam a visibilidade do repositório: repo público = imagem pública, repo privado = imagem privada. Você pode mudar individualmente em github.com/packages. Imagens privadas exigem docker login ghcr.io para pull.

Use a action actions/delete-package-versions para limpar versões antigas automaticamente: ao final do workflow, delete imagens com mais de 30 dias ou mantenha apenas as últimas N versões. O GitHub também tem retention policy configurável em Package settings.