Alternar tema

Como acelerar builds do Docker com cache: guia prático

Easton editorial illustration: registry transfer crane

Corrigi um erro de digitação, executei docker build de novo e então… o npm install recomeçou. Dez minutos depois, eu já tinha rolado a timeline várias vezes, tomado duas xícaras de café e a barra de progresso continuava girando.

Quem já trabalhou com aplicações em contêiner conhece bem essa frustração.

Depois de entender o cache do Docker, consegui reduzir o build de 10 minutos para 30 segundos.

30 segundos
Tempo de build
Redução de 10 minutos para 30 segundos, uma melhora de 20 vezes

Neste caso, três ajustes fizeram a maior diferença: configurar o .dockerignore, entender o cache de camadas e otimizar a ordem das instruções do Dockerfile. No fim, mostro também uma técnica mais avançada de montagem de cache do BuildKit. Se o seu build está lento, vale conferir cada ponto.

Por que o build do Docker demora tanto?

O contexto de build é grande demais

Vamos começar por uma armadilha bastante comum: o contexto de build (Build Context).

Ao executar docker build ., a primeira ação do Docker não é rodar o Dockerfile. Ele empacota todos os arquivos do diretório . e os envia ao Docker daemon. Todos mesmo: node_modules, a pasta .git e até centenas de megabytes de dados de teste baixados.

O caso mais extremo que encontrei foi o de um projeto frontend com um contexto de build de 800 MB. Só a transferência dos arquivos levava de 2 a 3 minutos. Na prática, a imagem precisava de menos de 10 MB de código-fonte.

É como querer enviar um livro e acabar empacotando a estante junto.

A reação em cadeia ao invalidar o cache de camadas

O segundo problema é não entender como funciona o cache de camadas do Docker.

Uma imagem Docker é composta por camadas. Cada instrução do Dockerfile — FROM, RUN ou COPY — cria uma delas. Durante o build, o Docker verifica se há cache disponível para cada camada. Se a instrução for idêntica e os arquivos relacionados não tiverem mudado, o cache é reutilizado e a instrução não precisa ser executada novamente.

Parece ótimo, certo?

O problema é que, quando o cache de uma camada é invalidado, todas as camadas seguintes precisam ser reconstruídas. É o efeito dominó: a primeira peça cai e derruba todas as outras.

Muitos Dockerfiles são escritos assim:

FROM node:18
COPY . /app
WORKDIR /app
RUN npm install

À primeira vista, não parece haver nada errado. Mas há um problema importante.

A linha COPY . /app copia o projeto inteiro. Se qualquer arquivo mudar — mesmo que seja apenas uma correção de digitação no README.md —, o cache dessa camada será invalidado. Com isso, o npm install que aparece depois também terá de ser executado novamente.

É por isso que mudar uma única linha de código pode obrigar você a reinstalar toda a árvore de dependências.

A ordem das instruções não ajuda o cache

A terceira armadilha é não saber como ordenar as instruções.

A estratégia de cache do Docker é simples: ele verifica as camadas de cima para baixo e, quando uma delas é invalidada, deixa de usar o cache nas seguintes. Portanto, você deve colocar as instruções que mudam menos no início e as que mudam mais no fim.

Na prática, muitos Dockerfiles fazem o contrário: primeiro copiam o código, que muda com frequência, e depois instalam as dependências, que mudam menos. Assim, qualquer alteração no código inutiliza o cache das dependências.

Em resumo, o problema é não separar o que muda com frequência do que permanece relativamente estável.

Técnica 1 — Configure o .dockerignore para reduzir o contexto de build

Com o problema identificado, a otimização mais simples e imediata é usar o .dockerignore.

O que é esse arquivo?

Se você já usou o .gitignore, a ideia é a mesma. O .dockerignore informa ao Docker quais arquivos não devem ser incluídos no contexto de build.

A criação é simples: adicione um arquivo .dockerignore na raiz do projeto, no mesmo nível do Dockerfile, e escreva as regras dentro dele.

Como configurar em um projeto Node.js?

Este é o arquivo que uso:

# Diretórios de dependências
**/node_modules/
**/npm-debug.log
**/.npm

# Arquivos do Git
.git/
.gitignore
.gitattributes

# Testes e documentação
**/test/
**/tests/
**/docs/
**/*.md
!README.md

# IDEs e editores
.vscode/
.idea/
*.swp
*.swo
.DS_Store

# Variáveis de ambiente e configuração
.env
.env.*
*.local

# Artefatos de build
dist/
build/
coverage/

Alguns pontos importantes:

  1. Sempre exclua node_modules. Essa pasta pode ter centenas de megabytes, e as dependências serão reinstaladas dentro da imagem. Não há motivo para copiá-la do ambiente local.
  2. Use o prefixo **/ para corresponder a todos os diretórios aninhados. Por exemplo, **/node_modules/ corresponde tanto a ./node_modules/ quanto a ./packages/lib/node_modules/, sem deixar diretórios para trás.
  3. Adicione a barra ao nome do diretório. node_modules/ representa um diretório, enquanto node_modules representa um arquivo. A diferença parece pequena, mas o Docker leva a regra ao pé da letra.

Qual é o impacto?

Fiz um teste em um projeto Next.js:

  • Antes: contexto de build de 520 MB, com 2 minutos e 15 segundos de transferência
  • Depois: contexto de build de 4,8 MB, com 3 segundos de transferência

Sim, apenas 3 segundos. Foram mais de 2 minutos economizados.

Isso sem contar a redução no tamanho da imagem. Como .git e node_modules deixaram de ser copiados, a imagem final caiu de 1,2 GB para 680 MB.

Armadilhas comuns

Armadilha 1: o .dockerignore só funciona na raiz do contexto de build. Se o comando for docker build -f subfolder/Dockerfile ., o .dockerignore deve ficar na raiz do projeto, e não dentro de subfolder.

Armadilha 2: escrever node_modules sem a barra pode não funcionar como esperado. Use node_modules/.

Armadilha 3: esquecer de excluir .git. Esse diretório pode facilmente chegar a centenas de megabytes e nunca será usado pela imagem.

Técnica 2 — Entenda e aproveite o cache de camadas do Docker

O .dockerignore resolve o problema da transferência, mas a otimização principal ainda depende de entender como o cache funciona.

Como funciona o cache de camadas?

Uma imagem Docker é como um bolo de várias camadas: cada camada é o resultado de uma instrução do Dockerfile.

Considere este Dockerfile:

FROM node:18          # Camada 1
RUN apt-get update    # Camada 2
COPY package.json .   # Camada 3
RUN npm install       # Camada 4
COPY . .              # Camada 5

Durante o build, o Docker verifica cada camada:

  1. Camada 1: para a instrução FROM, verifica se a imagem node:18 está disponível localmente. Se estiver, usa o cache.
  2. Camada 2: para a instrução RUN, verifica se o texto do comando é idêntico. Se for, usa o cache.
  3. Camada 3: para a instrução COPY, calcula o checksum de package.json. Se o arquivo não mudou, usa o cache.
  4. Camada 4: para a instrução RUN, continua a verificação.
  5. Camada 5: aplica a mesma lógica.

O ponto principal é: quando o cache de uma camada é invalidado, todas as camadas seguintes precisam ser reconstruídas.

Esse é o efeito dominó mencionado antes. Se você alterar package.json na camada 3, tanto o npm install da camada 4 quanto a cópia do código na camada 5 serão executados novamente.

Como saber se o cache foi usado?

Observe a saída do build:

Step 3/5 : COPY package.json .
 ---> Using cache
 ---> 3a8f29e7c5b1

Se aparecer Using cache, o cache foi usado. Caso contrário, a camada está sendo reconstruída.

Você também pode executar docker history <ID da imagem> para consultar o histórico de camadas da imagem, incluindo o SIZE e o horário de criação de cada uma.

Por que a instrução COPY é diferente?

Uma instrução RUN considera o texto do comando. Se o texto de RUN npm install não mudar, o Docker pode considerar o cache reutilizável.

COPY e ADD funcionam de outra forma. O Docker calcula o checksum do conteúdo copiado. Mesmo que o nome do arquivo permaneça igual, qualquer alteração no conteúdo invalida o cache.

Esse comportamento faz sentido: quando o conteúdo muda, as etapas seguintes do build podem ser afetadas, e reutilizar um cache antigo não seria seguro.

Ao mesmo tempo, é por isso que COPY . . exige cuidado. Qualquer arquivo alterado no projeto, até mesmo README.md, invalida essa camada.

Técnica 3 — Otimize a ordem das instruções no Dockerfile

Com o funcionamento do cache em mente, a próxima etapa é prática: como escrever o Dockerfile para aproveitar o máximo possível do cache?

Regra de ouro: do mais estável ao que muda mais

A ideia central cabe em uma frase: coloque primeiro as instruções que mudam menos e deixe por último as que mudam mais.

Isso funciona porque o Docker verifica o cache de cima para baixo. Se as camadas iniciais forem estáveis, as mudanças nas camadas finais não afetam o cache anterior.

Na prática, a ordem é esta:

  1. Imagem base — quase nunca muda
  2. Dependências do sistema — mudam de vez em quando
  3. Dependências do projeto — mudam ocasionalmente
  4. Código-fonte — muda todos os dias

Organizar as instruções dessa maneira maximiza o reaproveitamento do cache.

Exemplo incorreto: copiar o código antes de instalar as dependências

É comum começar com um Dockerfile assim:

FROM node:18
WORKDIR /app

# Incorreto: copia o projeto inteiro de uma vez
COPY . .

# Depois instala as dependências
RUN npm install

# Comando de inicialização
CMD ["npm", "start"]

Qual é o problema? Uma alteração no código-fonte invalida a camada de COPY . ., e o npm install posterior também precisa ser executado novamente.

O resultado é reinstalar centenas de pacotes npm por causa de uma única linha de JavaScript. Lá se vão mais 10 minutos.

Exemplo correto: instalar as dependências antes de copiar o código

Esta é a versão otimizada:

FROM node:18
WORKDIR /app

# Etapa 1: copia apenas os arquivos de dependências
COPY package.json package-lock.json ./

# Etapa 2: instala as dependências; esta camada será armazenada em cache
RUN npm ci --only=production

# Etapa 3: copia o código-fonte
COPY . .

# Comando de inicialização
CMD ["npm", "start"]

As vantagens são claras:

  1. Enquanto package.json não mudar, a camada do npm ci usará o cache.
  2. Uma alteração no código-fonte invalida apenas a camada de COPY . .; o cache da instalação das dependências continua disponível.
  3. O segundo build ignora a instalação do npm e termina muito mais rápido.

Nos meus testes, essa mudança reduziu os builds seguintes de 7 ou 8 minutos para cerca de 30 segundos.

O mesmo princípio vale para outras linguagens

Projeto Python:

FROM python:3.11
WORKDIR /app

# Primeiro, copia requirements.txt
COPY requirements.txt .

# Depois, executa pip install
RUN pip install --no-cache-dir -r requirements.txt

# Por último, copia o código
COPY . .

Projeto Go:

FROM golang:1.21
WORKDIR /app

# Primeiro, copia go.mod e go.sum
COPY go.mod go.sum ./

# Baixa as dependências
RUN go mod download

# Depois, copia o código
COPY . .

# Compila
RUN go build -o main .

A lógica é sempre a mesma: copie os arquivos de gerenciamento de dependências separadamente do código-fonte para que a etapa de instalação reutilize o cache sempre que possível.

Técnica avançada: COPY mais granular

Em projetos com uma estrutura complexa, você pode dividir ainda mais as cópias:

# Primeiro, copia os arquivos de configuração que mudam pouco
COPY .eslintrc.json .prettierrc ./

# Depois, copia os arquivos de dependências
COPY package*.json ./
RUN npm install

# Em seguida, copia a biblioteca compartilhada, se houver
COPY ./lib ./lib

# Por último, copia o código da aplicação
COPY ./src ./src

Essa abordagem é menos comum, mas pode ser útil em alguns cenários, como projetos monorepo.

Técnica avançada — Montagem de cache do BuildKit

As otimizações anteriores são fundamentais. Agora podemos avançar para a montagem de cache do BuildKit.

O que é o BuildKit?

O BuildKit é o mecanismo de build introduzido no Docker 18.09. Ele é mais rápido que o mecanismo antigo e oferece recursos de cache mais avançados.

Ativá-lo é simples:

# Ativação temporária
export DOCKER_BUILDKIT=1
docker build .

# Ou adicione a variável antes do comando
DOCKER_BUILDKIT=1 docker build .

Em versões recentes do Docker, a partir da 19.03, o BuildKit deve estar ativado por padrão. Se não tiver certeza, execute docker version para confirmar.

O que é uma montagem de cache?

O cache de camadas tem uma limitação: quando uma camada é invalidada, ela precisa ser executada por completo novamente.

Imagine que você alterou package.json para adicionar uma dependência. O cache da camada de npm install será invalidado e todos os pacotes, inclusive os já baixados, terão de ser transferidos outra vez.

A montagem de cache resolve esse problema. A lógica é: mesmo quando o cache da camada é invalidado, o cache de download do gerenciador de pacotes pode ser preservado.

Na prática, o gerenciador de pacotes recebe um diretório de cache persistente e compartilhado entre builds.

Como usar?

Exemplo para um projeto Node.js:

FROM node:18
WORKDIR /app

COPY package*.json ./

# O ponto principal: monta o diretório de cache do npm
RUN --mount=type=cache,target=/root/.npm \
    npm ci --only=production

COPY . .
CMD ["npm", "start"]

O trecho --mount=type=cache,target=/root/.npm é o mais importante:

  • type=cache indica que se trata de uma montagem de cache
  • target=/root/.npm é o diretório de cache do npm

Com essa configuração, mesmo que package.json mude e invalide o cache da camada, o npm não precisa baixar todos os pacotes do zero. Ele lê o conteúdo já armazenado em /root/.npm e baixa apenas pacotes novos ou atualizados.

Como configurar outros gerenciadores de pacotes?

Yarn:

RUN --mount=type=cache,target=/root/.yarn \
    yarn install --frozen-lockfile

pip (Python):

RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

apt (pacotes do sistema):

RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
    apt-get update && apt-get install -y gcc

Observe o parâmetro sharing=locked no exemplo do apt. Como o apt precisa de acesso exclusivo ao cache, esse parâmetro evita conflitos entre builds simultâneos.

Qual é o resultado?

Testei essa configuração em um projeto com mais de 200 dependências:

  • Cache da camada invalidado, mas montagem de cache disponível: a instalação das dependências caiu de 8 minutos para 1 minuto e 30 segundos
  • Inicialização completamente fria, sem nenhum cache: continuou levando 8 minutos

Em outras palavras, a montagem de cache funciona como uma segunda linha de defesa. Quando o cache da camada continua válido, o build é ainda mais rápido porque pula a instrução. Quando ele é invalidado, a montagem de cache evita baixar tudo novamente.

Observações importantes

  1. O tempo de retenção padrão não é longo: o BuildKit limpa por padrão caches com mais de 2 dias quando eles ultrapassam 512 MB. Em ambientes de CI/CD, talvez seja necessário ajustar essa política.

  2. Nem todo projeto precisa disso: se houver poucas dependências, como apenas uma dezena de pacotes, a montagem de cache fará pouca diferença.

  3. Use o caminho correto: cada gerenciador de pacotes usa um diretório de cache diferente. Consulte a documentação para confirmar.

Conclusão

Depois de todos esses detalhes, as ações principais continuam sendo três:

Primeiro, faça agora: crie um arquivo .dockerignore na raiz do projeto e exclua node_modules, .git, arquivos de teste e outros itens desnecessários. A tarefa leva menos de cinco minutos e pode reduzir o contexto de build em mais de 90%.

Segundo, ajuste hoje: reorganize as instruções do Dockerfile. Primeiro copie os arquivos de dependências, depois instale-as com RUN e, por último, copie o código-fonte. Essa mudança pode reduzir os builds seguintes de 10 minutos para 30 segundos.

Terceiro, explore quando fizer sentido: se o projeto tiver muitas dependências ou atualizações frequentes, experimente a montagem de cache do BuildKit. Ela ajuda quando o cache de uma camada é invalidado.

No meu projeto, essas três mudanças reduziram o build de 10 minutos para 30 segundos e a imagem de 1,2 GB para 680 MB. Foi uma das otimizações com melhor relação entre esforço e resultado que já apliquei.

Se o build do seu projeto está lento, teste esses ajustes e compare os tempos antes e depois. Se funcionar, conte nos comentários quanto o processo acelerou.

Fluxo completo para acelerar builds do Docker

Reduza o build de 10 minutos para 30 segundos usando o cache de camadas, o .dockerignore e uma ordem mais eficiente das instruções do Dockerfile

Estimated time: PT30M

  1. 1

    Step 1: Entenda por que o build é lento: contexto de build e cache de camadas

    Problemas no contexto de build:
  2. 2

    Step 2: Técnica 1: configure o .dockerignore para reduzir o contexto de build

    Faça agora: crie um arquivo .dockerignore na raiz do projeto e exclua node_modules, .git, arquivos de teste e outros itens desnecessários. Isso leva menos de cinco minutos e pode reduzir o contexto de build em mais de 90%.
  3. 3

    Step 3: Técnica 2: otimize a ordem das instruções do Dockerfile

    Ajuste hoje: primeiro use COPY nos arquivos de dependências, depois execute RUN para instalá-las e, por último, copie o código-fonte. Essa mudança pode reduzir os builds seguintes de 10 minutos para 30 segundos.
  4. 4

    Step 4: Técnica 3: use a montagem de cache do BuildKit

    Explore quando fizer sentido: se o projeto tiver muitas dependências ou atualizações frequentes, use a montagem de cache do BuildKit para evitar recomeçar do zero quando o cache de uma camada for invalidado.

FAQ

Por que o build do Docker demora tanto?
Problemas no contexto de build:
• Ao executar docker build ., a primeira ação do Docker é empacotar todos os arquivos do diretório . e enviá-los ao Docker daemon
• Isso inclui node_modules, a pasta .git, dados de teste e outros arquivos
• O contexto de build de um projeto frontend pode chegar a 800 MB, e apenas a transferência leva de 2 a 3 minutos
• Na prática, a imagem talvez precise de menos de 10 MB de código-fonte
• É como querer enviar um livro e acabar empacotando a estante junto

Reação em cadeia quando o cache de camadas é invalidado:
• As imagens Docker são compostas por camadas, e cada instrução do Dockerfile, como FROM, RUN e COPY, cria uma delas
• Durante o build, o Docker verifica se cada camada tem um cache disponível; se a instrução for idêntica e os arquivos relacionados não tiverem mudado, ele reutiliza o cache
• O problema é que, quando o cache de uma camada é invalidado, todas as camadas seguintes precisam ser reconstruídas, como uma sequência de dominós

Muitos Dockerfiles são escritos assim: FROM node:18, COPY . /app, WORKDIR /app, RUN npm install. Com essa ordem, qualquer alteração no código faz o npm install ser executado novamente.
Como configurar o .dockerignore para reduzir o contexto de build?
Faça agora: crie um arquivo .dockerignore na raiz do projeto e exclua node_modules, .git, arquivos de teste e outros itens desnecessários. Isso leva menos de cinco minutos e pode reduzir o contexto de build em mais de 90%.

Exemplo de configuração do .dockerignore:
• node_modules, para excluir dependências
• .git, para excluir o histórico do Git
• *.log, para excluir arquivos de log
• .env, para excluir variáveis de ambiente
• dist, para excluir artefatos de build
• test, para excluir arquivos de teste
• *.md, para excluir arquivos de documentação

Depois da configuração, o contexto de build pode cair de 800 MB para menos de 10 MB, e a transferência passa de 2 a 3 minutos para poucos segundos.
Como otimizar a ordem das instruções no Dockerfile?
Ajuste hoje: primeiro use COPY nos arquivos de dependências, depois execute RUN para instalá-las e, por último, copie o código-fonte. Essa mudança pode reduzir os builds seguintes de 10 minutos para 30 segundos.

Princípio de otimização:
• Coloque primeiro as instruções que mudam com menor frequência, como FROM e a instalação de dependências do sistema e da aplicação
• Deixe por último as instruções que mudam com mais frequência, como COPY do código-fonte

Antes da otimização:
• FROM node:18
• COPY . /app
• WORKDIR /app
• RUN npm install
• Qualquer alteração no código executa o npm install novamente

Depois da otimização:
• FROM node:18
• WORKDIR /app
• COPY package*.json ./
• RUN npm install
• COPY . .
• O npm install só é reexecutado quando package.json muda; alterações no código não afetam a instalação das dependências
Como usar uma montagem de cache do BuildKit?
Vale explorar quando o projeto tem muitas dependências ou elas mudam com frequência. A montagem de cache do BuildKit evita recomeçar do zero quando o cache de uma camada é invalidado.

Montagem de cache do BuildKit:
• Use --mount=type=cache para montar um diretório de cache
• Preserve o cache usado pelo npm install entre builds
• Reaproveite esse cache no build seguinte e acelere o processo em mais de 10 vezes

Exemplos:
• npm: RUN --mount=type=cache,target=/root/.npm npm install
• yarn: RUN --mount=type=cache,target=/root/.yarn yarn install --frozen-lockfile
• pip: RUN --mount=type=cache,target=/root/.cache/pip pip install -r requirements.txt
• apt: RUN --mount=type=cache,target=/var/cache/apt,sharing=locked apt-get update && apt-get install -y gcc

Observe o parâmetro sharing=locked no exemplo do apt. Como o apt precisa de acesso exclusivo ao cache, esse parâmetro evita conflitos entre builds simultâneos.
Qual é o resultado da otimização do build do Docker?
Resultados da otimização:
• Tempo de build reduzido de 10 minutos para 30 segundos, uma melhora de 20 vezes
• Tamanho da imagem reduzido de 1,2 GB para 680 MB
• Contexto de build reduzido de 800 MB para menos de 10 MB
• Tempo de transferência reduzido de 2 a 3 minutos para poucos segundos

No meu projeto, essas três mudanças reduziram o build de 10 minutos para 30 segundos e a imagem de 1,2 GB para 680 MB. Foi uma das otimizações com melhor relação entre esforço e resultado que já apliquei.

São três ações principais:
• Faça agora: crie o arquivo .dockerignore na raiz do projeto
• Ajuste hoje: reorganize as instruções do Dockerfile
• Explore quando fizer sentido: se o projeto tiver muitas dependências ou atualizações frequentes, use a montagem de cache do BuildKit

14 min de leitura · Publicado em: 17 dez 2025 · Atualizado em: 4 set 2026

Comentários

Entre com GitHub para comentar

Easton BlogEaston Blog