Acelerando builds Docker em CI usando cache BuildKit (export/import) no GitHub Actions
Por John Pires · 08 de março de 2026

Acelerando builds Docker em CI usando cache BuildKit (export/import) no GitHub Actions
1) Problema real que resolve
Contexto e impacto
Em pipelines de CI que constroem imagens Docker com frequência, o tempo gasto no passo de build pode dominar a duração total do pipeline. Isso afeta feedback loop, custo de runners e produtividade da equipe. A raiz do problema é que a maioria dos workflows de CI roda em runners efêmeros sem cache de camadas Docker entre execuções, forçando rebuilds completos mesmo para mudanças triviais.
O que o cache de BuildKit resolve
O BuildKit permite exportar e importar caches de camadas (layer cache) para fontes externas — por exemplo, um registry (ghcr/dockrhub), um diretório local ou o GitHub Actions cache — e reutilizar camadas em builds subsequentes. Exportar/importar cache reduz drasticamente passos repetitivos como apt/yum installs, compilação de dependências e installs de pacotes, entregando builds de minutos para segundos.
Problemas típicos que veremos neste artigo:
- Builds que ignoram cache e refazem tudo
- Uso incorreto de cache-from sem push do cache
- Corridas concorrentes que sobrescrevem cache
2) Abordagem ingênua vs abordagem otimizada
Jeito errado (code-review): pipeline sem cache correto
O que vejo com frequência em projetos: um workflow simples que roda docker build (ou buildx) e faz push da imagem, mas sem exportar o cache. Às vezes usam --cache-from apontando para uma imagem que nunca é atualizada ou esquecem de habilitar buildx. Resultado: cache não é reaproveitado entre execuções.
# Exemplo de workflow ingênuo (trecho relevante)
- name: Build image
run: |
docker build -t myorg/myapp:${{ github.sha }} .
docker push myorg/myapp:${{ github.sha }}
Contexto: esse bloco constrói e empurra a imagem, mas não usa BuildKit cache export/import. O que acontece: cada runner começa do zero, fazendo download e instalação de dependências novamente.
Falhas comuns nessa abordagem
Erros que costumo apontar em code review:
- Uso de docker build legacy sem buildx — sem suporte nativo a cache-to/registry.
- Uso de --cache-from apontando para imagens sem layers de cache ou imagem não existente.
- Esquecer de autenticar no registry antes de tentar usar cache baseado em registry.
Jeito certo (otimizado): export/import de cache com BuildKit
O padrão robusto é usar docker buildx junto com --cache-to e --cache-from apontando para um registro (por exemplo ghcr.io ou docker.io). Workflow estabelece um builder (setup-buildx), autentica no registry, usa build-push-action para exportar/importar cache em cada build e faz push da imagem.
Principais vantagens:
- Reaproveitamento de camadas entre runs e mesmo entre branches
- Redução substancial do tempo de build em etapas custosas
- Compatibilidade com multi-arch quando necessário
3) Implementacao com codigo real
Requisitos práticos
Para o exemplo prático precisamos de:
- GitHub Actions runner
- Permissão de push para o registry usado (GITHUB_TOKEN com packages: write para ghcr)
- docker/build-push-action (v4+) e docker/setup-buildx-action
Dockerfile real (boa prática para cache)
Contexto: um Dockerfile Node.js que separa dependências do código para maximizar cache de camadas. Isso reduz rebuilds quando apenas o código muda. O bloco abaixo é um exemplo realista (não um hello world).
# Dockerfile
# Etapa de dependências — raramente muda
FROM node:18-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN apk add --no-cache python3 make g++ \
&& npm ci --production
# Etapa build — para apps que precisam compilar assets
FROM node:18-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npm run build
# Etapa final
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
O que acontece: a camada de dependências é isolada. Se package.json não mudar, a etapa deps será cacheada e restaurada via BuildKit cache. Isso reduz tempo de build quando só o código muda.
Workflow GitHub Actions com cache export/import
Contexto: workflow funcional que autentica no GitHub Container Registry (ghcr), configura buildx e usa cache-to/cache-from com ref baseado no image tag. Esse pattern é seguro para pipelines sequenciais e pode ser extendido para concorrência com chaves únicas.
# .github/workflows/docker-build.yml
name: ci-build
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Log in to GHCR
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Build and push with cache
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/myorg/myapp:${{ github.sha }}
cache-from: type=registry,ref=ghcr.io/myorg/myapp:buildcache
cache-to: type=registry,ref=ghcr.io/myorg/myapp:buildcache,mode=max
O que acontece: buildx tenta importar cache de ghcr.io/myorg/myapp:buildcache e, ao final do build, atualiza/buildcache no registry. Em runs seguintes, esse cache é reutilizado para acelerar.
Comandos locais para reproduzir
Contexto: testar o mesmo comportamento localmente usando buildx para entender o que o CI faz. Assuma que você já está autenticado no registry.
# Teste local: importar cache e depois exportar
docker buildx create --name mybuilder --use
docker buildx build --cache-from=type=registry,ref=ghcr.io/myorg/myapp:buildcache \
--cache-to=type=registry,ref=ghcr.io/myorg/myapp:buildcache,mode=max \
-t ghcr.io/myorg/myapp:test . --push
O que acontece: a primeira vez fará upload do cache. Execuções subsequentes reaproveitam camadas do cache remoto e são muito mais rápidas.
4) Testes e validacao
Como medir ganhos
Valide tempo de build com e sem cache. Ferramentas simples servem: cronometre execução do step build na Actions UI ou registre logs de tempo dentro do job. Métricas esperadas: redução de 60-95% no tempo para builds que reutilizam cache de dependências.
Testes que recomendo
- Executar push com cache vazio (primeiro build) e registrar tempo.
- Executar outro push sem alterar package.json e medir o tempo — deve cair substancialmente.
- Alterar package.json e confirmar que as camadas certas são invalidada e recontruídas.
- Executar builds concorrentes (pull requests) e observar comportamento do cache, testando estratégias para evitar race conditions.
Use logs detalhados em build-push-action adicionando build-args ou ativando progress=plain para depurar o que foi cacheado.
Exemplo de validação via logs
Contexto: como forçar logs mais verbosos para ver uso de cache.
# No step build, adicione build-args ou extra options
- name: Build and push with verbose
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: ghcr.io/myorg/myapp:${{ github.sha }}
cache-from: type=registry,ref=ghcr.io/myorg/myapp:buildcache
cache-to: type=registry,ref=ghcr.io/myorg/myapp:buildcache,mode=max
outputs: type=log
O que acontece: dependendo da versão da action, você verá detalhes no log sobre import/export de cache e quais etapas reaproveitaram cache.
5) Consideracoes de producao
Erros comuns, armadilhas e dicas práticas
Abaixo resumo armadilhas que vejo em code review e como corrigi-las:
- Cache-from sem cache-to: apontar para um cache remoto que nunca foi populado. Resultado: cache não encontrado e build sem aceleração. Solução: sempre garantir que algum pipeline (ou tag) popula a referência cache-to antes de depender dela.
- Permissões insuficientes: GITHUB_TOKEN precisa de permissions: packages: write para ghcr. Revisar secrets se usar docker hub.
- Race conditions entre builds concorrentes: múltiplos jobs podem escrever no mesmo cache ref; considerar usar chaves por branch ou por commit, ou estratégias de mode=max que mesclam caches. Outra alternativa é usar um cache per-branch e um job que faça merge periódico.
- Tamanho do cache: caches grandes podem incrementar custo e latência ao puxar. Limite o que entra no cache (ex.: não cachear diretórios enormes ou dados temporários).
- Inline cache vs registry cache: inline (type=inline) injeta metadados no manifest da imagem — útil para registries que expõem o cache via image, mas requer push da imagem. Registry cache (type=registry) separa cache em referência explicita e costuma ser mais flexível.
Tip: prefira cache-to=type=registry,mode=max para ambientes CI onde vários runners devem compartilhar cache; use branch-specific refs para evitar sobrescrita entre branches.
Escalabilidade e segurança
Em grandes organizações, considere políticas para retenção de cache, limites de tamanho e controle de acesso a registries. Logs e métricas são cruciais: monitore hit-rate do cache (percentual de etapas que reaproveitam cache) e latência de builds. Isso ajuda priorizar otimizações no Dockerfile (por exemplo, priorizar instalar dependências antes de copiar código).
Privacidade: não armazene chaves ou secrets no cache. BuildKit suporta secret mounting (--secret) que não persiste em camadas, use isso para credenciais temporárias durante build.
Conclusao prática
Resumo direto
Use buildx com cache-to/cache-from apontando para um registry para reduzir dramaticamente tempos de build em CI. Corrija Dockerfiles para maximizar reutilização de camadas (separe dependências do código), configure autenticação correta e valide ganhos com testes controlados.
Checklist de adoção
- Ativar docker/setup-buildx-action
- Autenticar no registry (ex.: ghcr) com permissões de escrita
- Configurar cache-from e cache-to com ref consistente (ex.: myapp:buildcache)
- Ajustar Dockerfile para separar etapas mutáveis de imutáveis
- Monitorar hit-rate e ajustar estratégia de ref/branch para evitar race conditions
Aplicando esse padrão você transforma builds que antes consumiam grande parte do tempo de CI em passos rápidos e previsíveis. Em code review, foque em: Dockerfile bomb-proof para cache, workflow com export/import do cache e validação de permissões e concorrência.
Este artigo mostrou o jeito errado (ignorar cache) e o jeito certo (BuildKit export/import), com exemplos práticos para GitHub Actions, Dockerfile e comandos locais para reproduzir. Adapte as refs e políticas de cache para o seu fluxo de trabalho e monitore o comportamento em produção.
Assuntos: Docker, github-actions, buildkit, ci, cache, DevOps
Leia também
Ambientes de preview em PRs com GitHub Actions e Docker Compose — passo a passo
Aprenda a criar ambientes de preview para PRs usando GitHub Actions e Docker Compose. Mostro o jeito errado e o certo, com exemplos reais, scripts de saúde e…
Canary deploy com GitHub Actions e Docker: métricas, rollout e rollback automático
Pipeline prático de canary deploy com GitHub Actions, Docker e Kubernetes: exemplos funcionais de Dockerfile, manifests Nginx-canary, workflow e script de…
Canary deploy com GitHub Actions em Kubernetes usando Helm e Flagger
Guia prático para implementar canary deploy com GitHub Actions, Helm e Flagger: padrões de pipeline, exemplos de Canary CRD e AnalysisTemplate, integrações…
Testes em CI com GitHub Actions e Docker Compose para Postgres e Redis
Aprenda a rodar testes de integração no CI usando GitHub Actions e Docker Compose com Postgres e Redis. Guia passo a passo para iniciantes: arquivos…
Escanear imagens Docker no CI com Trivy e GitHub Actions — falhe por CVSS
Aprenda a escanear imagens Docker no CI com Trivy e GitHub Actions e a fazer o pipeline falhar quando uma vulnerabilidade atingir um limiar CVSS. Passo a…
GitHub Actions: build, testar em container e publicar imagem no GHCR
Pipeline prática: build de imagem Docker, executar testes no container carregado e publicar no GHCR usando GitHub Actions. Código e workflow para iniciar e…