Publicando Imagens no Docker Hub e GitHub Container Registry

[286] Publicando Imagens no Docker Hub e GitHub Container Registry

Publicando imagens em registros de container: convenção de nomes no Docker Hub e no GitHub Container Registry, login por token, automação completa com GitHub Actions e metadata-action, builds multi-plataforma para ARM e AMD64, cache de camadas e estratégia de tags para rastreabilidade e rollback.
DevOps

10 min de leitura

Uma imagem que existe apenas na máquina do desenvolvedor não serve a nenhum pipeline de CI/CD, a nenhum servidor de produção, a nenhum colega de equipe. Para que uma imagem seja utilizável em qualquer ambiente, ela precisa ser publicada em um registro de container — um serviço que armazena, versiona e distribui imagens.

O fluxo padrão em qualquer pipeline de entrega é construir a imagem, enviá-la para um registro e, em seguida, instruir o servidor de destino a baixá-la e executá-la. O registro é o elo entre a construção e a execução.

Docker Hub: O Registro Público Padrão

O Docker Hub é o registro padrão do Docker — quando se executa docker pull nginx, é do Docker Hub que a imagem vem. Qualquer pessoa pode criar uma conta gratuita e publicar imagens públicas sem limite.

Criando uma conta e fazendo login:

# Login interativo
docker login

# Login com credenciais diretas (útil em scripts)
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin

Convenção de nomenclatura no Docker Hub:

usuario/nome-da-imagem:tag

# Exemplos
ricardomatosdocker/minha-api:1.0.0
ricardomatosdocker/minha-api:latest

Publicando uma imagem:

# Constrói com a tag no formato correto
docker build -t ricardomatosdocker/minha-api:1.0.0 .

# Adiciona a tag latest apontando para a mesma imagem
docker tag ricardomatosdocker/minha-api:1.0.0 ricardomatosdocker/minha-api:latest

# Publica ambas as tags
docker push ricardomatosdocker/minha-api:1.0.0
docker push ricardomatosdocker/minha-api:latest

GitHub Container Registry: Imagens Integradas ao Repositório

O GitHub Container Registry (GHCR) armazena imagens diretamente associadas a um repositório GitHub. A integração com GitHub Actions é nativa e as permissões seguem as mesmas regras do repositório — quem tem acesso ao repositório tem acesso às imagens.

A URL do GHCR segue o formato:

ghcr.io/usuario-ou-org/nome-da-imagem:tag

# Exemplos
ghcr.io/ricardomatosdocker/minha-api:1.0.0
ghcr.io/minha-empresa/minha-api:main

Login no GHCR:

# Usando um Personal Access Token com permissão write:packages
echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_USERNAME" --password-stdin

Automatizando a Publicação com GitHub Actions

O fluxo mais comum em projetos modernos: a cada push na branch main, o pipeline constrói a imagem e a publica automaticamente. A cada tag de release, publica com o número de versão.

# .github/workflows/docker-publish.yml
name: Build e Publicação de Imagem

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

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  build-and-push:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write

    steps:
      - name: Clona o repositório
        uses: actions/checkout@v4

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

      - name: Login no GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extrai metadados da imagem
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
          tags: |
            type=ref,event=branch
            type=ref,event=pr
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=sha,prefix=sha-

      - name: Constrói e publica a imagem
        uses: docker/build-push-action@v5
        with:
          context: .
          push: ${{ github.event_name != 'pull_request' }}
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

O que esse workflow produz em cada cenário:

Um push na main gera a tag main e uma tag com o SHA do commit como sha-abc1234. Um Pull Request gera uma imagem de teste mas não a publica. A criação da tag v1.2.0 gera as tags 1.2.0, 1.2 e v1.2.0 — seguindo as convenções do SemVer.

Multi-platform Builds: Imagens para ARM e AMD64

Com a popularização de máquinas Apple Silicon e instâncias ARM na AWS (Graviton), imagens que rodam apenas em AMD64 se tornam um problema. O Docker Buildx permite construir imagens para múltiplas arquiteturas em um único comando:

- name: Configura QEMU para emulação de ARM
  uses: docker/setup-qemu-action@v3

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

- name: Constrói para múltiplas plataformas
  uses: docker/build-push-action@v5
  with:
    context: .
    platforms: linux/amd64,linux/arm64
    push: true
    tags: ${{ steps.meta.outputs.tags }}

O registro armazena um manifesto multi-plataforma — quando docker pull é executado, o Docker automaticamente baixa a variante correta para a arquitetura do host.

Gerenciando Tags de Forma Consistente

Uma estratégia de tagging bem definida evita confusão sobre qual versão está rodando em cada ambiente:

# SHA do commit — imutável, rastreável, ideal para rollbacks
ghcr.io/empresa/api:sha-abc1234

# Branch — sempre aponta para o último build daquela branch
ghcr.io/empresa/api:main
ghcr.io/empresa/api:develop

# Versão semântica — para releases estáveis
ghcr.io/empresa/api:1.2.0
ghcr.io/empresa/api:1.2      # última patch da minor 1.2
ghcr.io/empresa/api:1        # última minor do major 1

# Ambiente — prático mas menos rastreável
ghcr.io/empresa/api:staging
ghcr.io/empresa/api:production

# latest — conveniente mas perigoso em produção
ghcr.io/empresa/api:latest

A recomendação para ambientes de produção é usar sempre o SHA do commit ou a versão semântica exata — nunca latest ou tags de ambiente mutáveis. Isso garante que o deploy é determinístico e que um rollback pode ser feito apontando para um SHA específico.

Inspecionando Imagens Publicadas

# Inspeciona uma imagem remota sem baixá-la
docker manifest inspect ghcr.io/empresa/api:1.2.0

# Verifica as plataformas suportadas
docker manifest inspect ghcr.io/empresa/api:1.2.0 | \
  grep -A2 '"platform"'

# Histórico de camadas de uma imagem publicada
docker history ghcr.io/empresa/api:1.2.0

Encerrando o Módulo 3

Com este artigo conclui-se o Módulo 3. Foram cobertos os conceitos fundamentais de containers, a instalação e uso do Docker, a escrita de Dockerfiles eficientes, volumes e redes, orquestração local com Docker Compose, boas práticas de produção e publicação em registros.

O Módulo 4 entra no tema que une tudo isso em um fluxo contínuo: CI/CD na Prática. Os containers construídos neste módulo serão testados, publicados e implantados automaticamente por pipelines que rodam sem intervenção humana.

Referências para Aprofundamento

Documentação oficial

GitHub Actions

Leitura complementar

Exercícios

Exercício 1

Por que o step de publicação usa push: ${{ github.event_name != 'pull_request' }}? O que se perderia ao publicar também a partir de PRs?

Ver resposta

✓ Resposta: Porque em um Pull Request o objetivo é validar que a imagem constrói, não distribuí-la. A expressão mantém o build acontecendo — que é justamente o valor do check — e desliga apenas a publicação. Publicar a partir de PRs traria dois problemas. O primeiro é poluição: o registro acumularia tags de branches que talvez nunca sejam mergeadas. O segundo é de segurança: qualquer pessoa capaz de abrir um PR passaria a gravar no registro da organização, o que abre caminho para publicar conteúdo arbitrário sob um nome confiável — e imagens são baixadas e executadas justamente por confiança na origem.

Exercício 2

Por que o workflow declara permissions: contents: read e packages: write? O que acontece se packages: write for esquecido?

Ver resposta

✓ Resposta: Porque o GITHUB_TOKEN é gerado automaticamente a cada execução e suas permissões são escopadas. Declará-las explicitamente aplica o mínimo privilégio ao workflow: ele lê o repositório e escreve pacotes, e nada além disso — se o token vazar em um log, o alcance é limitado a essas duas capacidades. Esquecer o packages: write produz uma falha particularmente confusa: o docker login no GHCR conclui com sucesso, dando a impressão de que a autenticação está correta, e só o push falha por permissão. É um dos erros mais comuns na primeira configuração, justamente porque o login não denuncia o problema.

Exercício 3

O que o docker/metadata-action gera em cada cenário — push na main, Pull Request e criação da tag v1.2.0? Por que produzir 1.2 além de 1.2.0?

Ver resposta

✓ Resposta: Um push na main gera a tag main e uma tag com o SHA do commit, como sha-abc1234. Um Pull Request gera a tag correspondente ao PR, mas sem publicação. A tag v1.2.0 gera 1.2.0, 1.2 e v1.2.0. A tag 1.2 existe para quem deseja receber correções de patch sem alterar a referência: quem aponta para ela passa a rodar a 1.2.1 e a 1.2.2 assim que saírem, mas nunca salta sozinho para a 1.3. É a mesma lógica das faixas de versão dos gerenciadores de pacote, aplicada a imagens — segurança de atualização automática apenas onde a compatibilidade está garantida.

Exercício 4

Por que a tag sha-abc1234 é a referência ideal para rollback, e por que apontar produção para latest é problemático?

Ver resposta

✓ Resposta: Porque o SHA é imutável e rastreável: aquela tag corresponde a exatamente um commit e a exatamente uma imagem, para sempre. O rollback se resume a apontar para o SHA anterior, com certeza absoluta do que está subindo e de qual código o produziu. O latest é o oposto — um ponteiro móvel. Duas máquinas que fizeram pull em momentos diferentes podem estar executando imagens distintas sob o mesmo nome, sem nenhum sinal disso. E o custo se manifesta no pior momento: durante um incidente, a pergunta "qual versão está no ar?" deixa de ter resposta confiável, e o rollback não tem alvo definido.

Exercício 5

Por que builds multi-plataforma se tornaram necessários? O que o registro efetivamente armazena quando se usa platforms: linux/amd64,linux/arm64?

Ver resposta

✓ Resposta: Porque a arquitetura deixou de ser homogênea: máquinas Apple Silicon no desenvolvimento e instâncias ARM em produção — como as Graviton da AWS — passaram a conviver com o AMD64 tradicional. Uma imagem construída apenas para AMD64 falha ao subir nessas máquinas ou roda sob emulação, com perda expressiva de desempenho. Com o build multi-plataforma, o registro armazena um manifesto multi-plataforma: uma lista que aponta para a imagem específica de cada arquitetura. O docker pull consulta esse manifesto e baixa automaticamente a variante correta para o host, sem que quem executa precise saber que existem duas.

Exercício 6

Que problema cache-from: type=gha e cache-to: type=gha,mode=max resolvem? E o que o mode=max acrescenta?

Ver resposta

✓ Resposta: Resolvem o fato de o runner ser descartado ao fim de cada execução, levando junto todo o cache de camadas do Docker. Sem um cache externo, cada build recomeça do zero — reinstala dependências, recompila tudo — mesmo quando apenas uma linha mudou, o que anula na prática toda a estratégia de ordenação de camadas do Dockerfile. O type=gha grava essas camadas no cache do GitHub Actions e as recupera na execução seguinte. O mode=max acrescenta a exportação das camadas intermediárias, incluindo as dos estágios de build de um multi-stage — que, por não fazerem parte da imagem final, não seriam preservadas de outro modo, apesar de serem justamente as mais caras de reconstruir.

Comentários

Mais em DevOps

O Que é um Container e Por Que Isso Mudou Tudo
O Que é um Container e Por Que Isso Mudou Tudo

O que é um container e por que ele mudou a entrega de software: o problema…

Módulos no Terraform: Reusabilidade e Organização
Módulos no Terraform: Reusabilidade e Organização

A saída para a duplicação de configuração entre ambientes: a anatomia de um…

GitLab Self-Hosted: Soberania Total sobre Código e Pipelines
GitLab Self-Hosted: Soberania Total sobre Código e Pipelines

Quando o SaaS não é opção — setores regulados, residência de dados, custo por…