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

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.
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:
- 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.
- 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. - Adicione a barra ao nome do diretório.
node_modules/representa um diretório, enquantonode_modulesrepresenta 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:
- Camada 1: para a instrução FROM, verifica se a imagem node:18 está disponível localmente. Se estiver, usa o cache.
- Camada 2: para a instrução RUN, verifica se o texto do comando é idêntico. Se for, usa o cache.
- Camada 3: para a instrução COPY, calcula o checksum de package.json. Se o arquivo não mudou, usa o cache.
- Camada 4: para a instrução RUN, continua a verificação.
- 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:
- Imagem base — quase nunca muda
- Dependências do sistema — mudam de vez em quando
- Dependências do projeto — mudam ocasionalmente
- 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:
- Enquanto package.json não mudar, a camada do
npm ciusará o cache. - Uma alteração no código-fonte invalida apenas a camada de
COPY . .; o cache da instalação das dependências continua disponível. - 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=cacheindica que se trata de uma montagem de cachetarget=/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
-
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.
-
Nem todo projeto precisa disso: se houver poucas dependências, como apenas uma dezena de pacotes, a montagem de cache fará pouca diferença.
-
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
Step 1: Entenda por que o build é lento: contexto de build e cache de camadas
Problemas no contexto de build: -
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
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
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?
• 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?
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?
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?
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?
• 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
Guia prático Docker
Se você chegou pela busca, o caminho mais rápido é ir para o post anterior ou próximo desta série.
Anterior
Docker multi-stage build na prática: reduzindo a imagem de produção de 1 GB para 10 MB
Domine o Docker multi-stage build e reduza uma imagem de produção de 1 GB para 10 MB. Veja modelos para Go, Node.js e Python, uma comparação entre Alpine e Distroless e soluções para 5 erros comuns.
Parte 6 de 34
Próximo
Docker Compose com vários serviços: inicie o ambiente de desenvolvimento local com um único comando
Use o Docker Compose para orquestrar vários serviços e iniciar Web, API, MySQL e Redis no ambiente de desenvolvimento local com um único comando. Evite instalações manuais trabalhosas, conflitos de versão e portas ocupadas; novos integrantes conseguem clonar o repositório e começar a desenvolver em 5 minutos, além de alternar entre projetos em segundos.
Parte 8 de 34



Comentários
Entre com GitHub para comentar