Pular para o conteúdo

Acelerar o build de imagem no CI

Intermediário12 min de leituracontainers

Na sua máquina, o segundo docker build leva segundos. No CI, leva os mesmos oito minutos de sempre — porque cada execução começa em um runner limpo, sem nenhuma camada anterior. Este guia faz o cache sobreviver entre execuções.

O cache de camadas fica no armazenamento local do daemon Docker. Runner efêmero significa daemon novo a cada execução: nada para reaproveitar. A solução é exportar o cache para um lugar que persiste — o registry, ou o cache do próprio provedor de CI.

Janela do terminal
docker buildx create --name ci --driver docker-container --use
docker buildx build \
--cache-from type=registry,ref=registry.exemplo.com/loja:buildcache \
--cache-to type=registry,ref=registry.exemplo.com/loja:buildcache,mode=max \
-t registry.exemplo.com/loja:${GIT_SHA} \
--push .

mode=max exporta as camadas de todos os estágios, inclusive os intermediários do multi-stage — é o que realmente acelera. O padrão (mode=min) só guarda o estágio final e rende bem menos.

No GitHub Actions, há também o cache nativo do runner:

- uses: docker/build-push-action@v6
with:
push: true
tags: registry.exemplo.com/loja:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

Compare os dois na sua realidade: type=gha tem cota por repositório e é rápido para projetos pequenos; o registry não tem essa cota, é compartilhado entre workflows e entre a máquina de quem desenvolve, e custa armazenamento.

Cache não adianta se toda mudança de código invalida a instalação de dependências. A regra é do que muda menos para o que muda mais.

FROM node:22-slim AS build
WORKDIR /app
# 1. só o manifesto: esta camada muda quando as dependências mudam
COPY package.json package-lock.json ./
RUN npm ci
# 2. o código vem depois: mudar código não reinstala nada
COPY . .
RUN npm run build

O erro clássico é COPY . . antes do npm ci / pip install / go mod download: o cache quebra em todo commit.

Ganho extra com cache de montagem, que preserva o cache do gerenciador de pacotes entre builds sem inchar a imagem:

RUN --mount=type=cache,target=/root/.npm npm ci
RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
RUN --mount=type=cache,target=/go/pkg/mod go mod download

Some a isso um .dockerignore que exclua .git, node_modules e artefatos: contexto grande atrasa o build antes mesmo de a primeira camada rodar.

Janela do terminal
# compare execuções: com cache frio e com cache quente
time docker buildx build --no-cache -t loja:teste . > /dev/null
time docker buildx build -t loja:teste . > /dev/null

Na saída do buildx, procure por CACHED nas etapas: se as etapas de dependência não aparecem como cacheadas na segunda execução, a chave ou a ordem estão erradas.

O que observar depois de ligar o cache:

  • Etapas de dependência marcadas como CACHED em builds seguidos.
  • Tempo total do job de imagem, antes e depois.
  • Tempo gasto exportando o cache — com mode=max em imagens grandes, o push do cache pode comer parte do ganho. Se acontecer, teste mode=min ou limite o cache aos estágios pesados.
Sintoma Causa provável
Nada CACHED na segunda execução Driver padrão em vez de docker-container
Cache acerta na base e para no meio COPY . . cedo demais no Dockerfile
Acerta em main e nunca em PR Escopo do cache por branch — use um cache compartilhado
Build multiplataforma sem cache Cada plataforma tem sua chave; exporte para as duas
Cache cresce sem parar no registry Sem política de retenção na tag buildcache
Cache “acerta” com dependência antiga Lockfile não copiado antes da instalação

Um cuidado de segurança: o cache é um artefato compartilhado. Não use o cache de builds de pull requests de terceiros para produzir a imagem de produção, e não deixe segredo entrar em camada cacheada (use --mount=type=secret, nunca ARG).

  • Builder docker-container no CI.
  • cache-from e cache-to configurados, com mode=max.
  • Dockerfile ordenado do estável para o volátil.
  • .dockerignore reduzindo o contexto.
  • Etapas de dependência aparecendo como CACHED.
  • Tempo medido antes e depois, incluindo o custo de exportar o cache.
  • Retenção definida para a tag de cache no registry.