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
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
FROMe o mesmoRUN 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 buildde 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"]
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"]
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/*
--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
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:
Top comments (0)