DEV Community

Cover image for Dockerfile na prática - camadas, cache de build e boas práticas
Rafael Dutra for apsis-cc

Posted on

Dockerfile na prática - camadas, cache de build e boas práticas

1. Retomando: do Dockerfile mínimo a um Dockerfile de verdade

Na segunda parte desta série, um Dockerfile de poucas linhas já foi suficiente para empacotar uma aplicação Python. Isso funciona, mas um Dockerfile escrito sem pensar em camadas e cache de build gera imagens maiores do que precisam ser e builds que demoram muito mais do que deveriam a cada mudança pequena no código. Este artigo aprofunda como o Docker constrói uma imagem por dentro, e como escrever um Dockerfile que tira proveito disso.

2. Como funcionam as camadas (layers)

Cada instrução de um Dockerfile (FROM, RUN, COPY, ADD) que modifica o sistema de arquivos gera uma camada — um diff read-only armazenado separadamente e empilhado sobre as anteriores. A imagem final é simplesmente a soma de todas essas camadas, e o container em execução adiciona uma camada gravável no topo (union filesystem).

Container (camada gravável)
──────────────────────────
Camada 4: COPY . .
Camada 3: RUN pip install -r requirements.txt
Camada 2: COPY requirements.txt .
Camada 1: FROM python:3.12-slim
Enter fullscreen mode Exit fullscreen mode

Duas consequências práticas importantes:

  • Camadas são reaproveitadas entre imagens. Se duas imagens diferentes compartilham as mesmas primeiras instruções (por exemplo, a mesma FROM e o mesmo RUN apt-get install), o Docker armazena essa camada uma única vez em disco, mesmo que várias imagens a usem.
  • Camadas são cacheadas entre builds. Ao rodar docker build de novo, o Docker verifica cada instrução, na ordem: se a instrução e seus arquivos de entrada não mudaram desde o último build, ele reaproveita a camada já construída em vez de refazer o trabalho. Isso é a base de todo o próximo tópico.

3. Cache de build: ordenar o Dockerfile por frequência de mudança

O cache de build é invalidado a partir do primeiro ponto de mudança: se a instrução N mudou (ou um arquivo que ela copia mudou), toda camada a partir de N é reconstruída — mesmo que as instruções seguintes sejam idênticas ao build anterior. Isso significa que a ordem das instruções no Dockerfile importa tanto quanto o conteúdo delas.

O erro mais comum é copiar todo o código-fonte antes de instalar dependências:

# Ruim: qualquer mudança no código invalida o cache do pip install
FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install --no-cache-dir -r requirements.txt
CMD ["python3", "app.py"]
Enter fullscreen mode Exit fullscreen mode

Como COPY . . copia o código inteiro, qualquer alteração em qualquer arquivo — inclusive um README.md — muda o conteúdo dessa camada e invalida tudo que vem depois, incluindo o pip install, que normalmente é o passo mais lento do build.

A correção é separar o que muda com frequência (código) do que muda pouco (lista de dependências), copiando e instalando dependências primeiro:

# Bom: pip install só roda de novo se requirements.txt mudar
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python3", "app.py"]
Enter fullscreen mode Exit fullscreen mode

Agora, alterar apenas o código da aplicação (o caso mais comum durante o desenvolvimento) reaproveita a camada do pip install do cache, e o build inteiro cai de dezenas de segundos (ou minutos, em projetos com muitas dependências) para menos de um segundo nessa etapa.

O mesmo princípio vale para qualquer gerenciador de pacotes — Node.js (package.json antes do código), Go (go.mod/go.sum antes do código), Ruby (Gemfile antes do código): copiar o manifesto de dependências, instalar, só então copiar o resto.

4. Minimizando o número e o tamanho das camadas

Cada RUN gera uma nova camada, e camadas nunca "encolhem" — se um RUN baixa um arquivo temporário e um RUN seguinte o apaga, o espaço ocupado por ele na camada anterior continua contando no tamanho final da imagem. Por isso, passos relacionados costumam ser combinados em um único RUN:

# Ruim: cache do apt fica preso numa camada, mesmo "removido" depois
RUN apt-get update
RUN apt-get install -y curl git
RUN rm -rf /var/lib/apt/lists/*

# Bom: um único RUN, uma única camada, sem sobra
RUN apt-get update && \
    apt-get install -y --no-install-recommends curl git && \
    rm -rf /var/lib/apt/lists/*
Enter fullscreen mode Exit fullscreen mode

--no-install-recommends evita instalar pacotes "sugeridos" que o apt-get traria por padrão, mas que raramente são necessários dentro de um container — outra fonte comum de peso desnecessário na imagem.

5. Escolhendo a imagem base certa

A imagem base declarada em FROM é o maior fator isolado no tamanho final. Para a mesma linguagem, variantes existem com trade-offs diferentes:

Variante Tamanho aproximado Quando usar
python:3.12 ~1 GB Precisa de ferramentas de build completas
python:3.12-slim ~150 MB Maioria dos casos — Debian mínimo
python:3.12-alpine ~50 MB Tamanho é crítico; cuidado com compatibilidade

alpine usa musl em vez de glibc, o que ocasionalmente quebra dependências Python com extensões compiladas em C (como numpy ou psycopg2) — vale testar antes de adotar em produção, em vez de assumir que é sempre a melhor escolha só por ser a menor imagem.

6. .dockerignore: cortando o que não deveria nem chegar ao build

Antes de qualquer instrução rodar, o Docker envia todo o contexto de build (o diretório passado como argumento final de docker build, por exemplo .) para o daemon. Sem um .dockerignore, isso costuma incluir lixo irrelevante — e às vezes sensível:

# .dockerignore
.git
node_modules
__pycache__
*.pyc
.env
.venv
*.log
Enter fullscreen mode Exit fullscreen mode

Além de acelerar o envio do contexto (relevante em projetos com dependências pesadas versionadas localmente, como node_modules), isso evita vazar segredos: um .env com credenciais copiado por acidente via COPY . . fica embutido permanentemente em uma camada da imagem, mesmo que um passo posterior o apague.

7. Conclusão e próximos passos

Entender camadas e cache de build muda a forma de escrever Dockerfiles: ordenar instruções da menos volátil para a mais volátil, combinar RUNs relacionados, escolher a imagem base certa para o caso de uso e manter um .dockerignore atualizado são hábitos que se pagam em builds mais rápidos e imagens menores desde o primeiro dia. No próximo artigo, o assunto muda de como as imagens são construídas para como os containers se comunicam: redes, volumes e persistência de dados.


Imagem de capa: Logo oficial do Docker — Wikimedia Commons, fonte: docker.com/company/newsroom/media-resources

Referências:

  1. Docker Documentation — Dockerfile Best Practices
  2. Docker Documentation — Build Cache

Top comments (0)