Layers e cache no build do Docker

1. Entendendo Layers no Docker

Cada instrução em um Dockerfile gera uma camada imutável (layer). Quando você executa docker build, o Docker processa cada comando sequencialmente e cria uma nova camada sobre a anterior. Essas camadas são armazenadas no sistema de arquivos union (OverlayFS, AUFS) e empilhadas para formar a imagem final.

FROM ubuntu:22.04        # Layer 1: base image
RUN apt-get update        # Layer 2: atualiza pacotes
RUN apt-get install -y python3  # Layer 3: instala Python
COPY app.py /app/         # Layer 4: adiciona código
CMD ["python3", "/app/app.py"]  # Layer 5: comando final

Cada layer é apenas a diferença em relação à anterior. Isso permite reuso eficiente: se você tem 10 imagens baseadas em Ubuntu, as layers do Ubuntu são compartilhadas entre elas.

2. Mecanismo de Cache no Build

O cache de build funciona através de hash das instruções. Quando você executa docker build, o Docker calcula um hash para cada instrução baseado em:

  • Conteúdo da instrução (comando exato)
  • Arquivos copiados (hash do conteúdo)
  • Contexto de build
  • Layers anteriores
# Exemplo de hit de cache
$ docker build -t myapp:latest .
Step 1/5 : FROM ubuntu:22.04
 ---> a1b2c3d4e5f6
Step 2/5 : RUN apt-get update
 ---> Using cache
 ---> b2c3d4e5f6a1

O cache é invalidado quando:
- A instrução muda
- Arquivos copiados mudam
- A ordem das instruções muda
- O contexto de build muda

3. Boas Práticas para Otimizar o Cache

A regra de ouro: ordene as instruções do menos volátil para o mais volátil.

# RUIM: cache quebrado frequentemente
FROM node:18
COPY . /app           # Qualquer mudança no código invalida todo o cache
RUN npm install       # Precisa reinstalar dependências
RUN npm run build

# BOM: otimizado para cache
FROM node:18
WORKDIR /app
COPY package.json package-lock.json ./   # Dependências primeiro
RUN npm install                          # Cache hit se package.json não mudou
COPY . .                                 # Só o código muda
RUN npm run build                        # Rebuild rápido

Combine comandos RUN para reduzir número de layers:

# RUIM: muitas layers
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y git
RUN rm -rf /var/lib/apt/lists/*

# BOM: uma layer otimizada
RUN apt-get update && \
    apt-get install -y curl git && \
    rm -rf /var/lib/apt/lists/*

4. Estratégias com COPY e ADD

O .dockerignore é fundamental para evitar que arquivos desnecessários invalidem o cache:

# .dockerignore
node_modules/
.git/
*.log
.env
dist/
coverage/

Copie arquivos essenciais antes do código completo:

# Para aplicações Node.js
FROM node:18
WORKDIR /app
COPY package.json package-lock.json ./   # Cache de dependências
RUN npm install --production
COPY src/ ./src/                         # Cache de código
COPY public/ ./public/

Timestamps e permissões também afetam o cache. O Docker usa o conteúdo dos arquivos, não o timestamp, mas mudanças de permissão podem invalidar.

5. Gerenciamento de Contexto de Build

O contexto de build (docker build .) inclui todos os arquivos no diretório atual. Arquivos não utilizados geram overhead e podem invalidar cache.

# Enviando contexto grande para o daemon
$ docker build -t myapp .
Sending build context to Docker daemon  1.2GB  # Muito grande!

# Com .dockerignore eficiente
Sending build context to Docker daemon  15MB   # Aceitável

Técnicas avançadas:

# Build em subdiretório específico
$ docker build -f docker/Dockerfile -t myapp ./src

# Uso de --target para builds multi-estágio
$ docker build --target builder -t myapp:builder .
$ docker build --target runtime -t myapp:latest .

6. Multi-stage Builds e Cache

Multi-stage builds permitem cache granular entre estágios:

# Dockerfile para aplicação Go
# Estágio 1: builder
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download                    # Cache de dependências
COPY . .
RUN CGO_ENABLED=0 go build -o app .

# Estágio 2: runtime (imagem final pequena)
FROM alpine:3.18
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/app /app/app
CMD ["/app/app"]

O cache do estágio builder é preservado mesmo que o runtime mude, e vice-versa. Isso é especialmente útil em CI/CD onde os estágios de compilação são caros.

7. Cache Remoto e CI/CD

Em pipelines CI/CD, o cache local não persiste entre builds. Use --cache-from para reutilizar cache de registries:

# Pipeline Jenkins/GitLab CI
# 1. Puxar imagem anterior como cache
docker pull registry.example.com/myapp:latest || true

# 2. Build com cache da imagem remota
docker build \
  --cache-from registry.example.com/myapp:latest \
  -t registry.example.com/myapp:latest .

# 3. Push da nova imagem
docker push registry.example.com/myapp:latest

Para cache distribuído em equipes:

# Usando buildx com cache remoto
docker buildx build \
  --cache-from type=registry,ref=registry.example.com/myapp:cache \
  --cache-to type=registry,ref=registry.example.com/myapp:cache,mode=max \
  -t registry.example.com/myapp:latest .

8. Diagnóstico e Troubleshooting

Inspecione layers para entender o que está consumindo espaço:

# Ver histórico de layers
docker history myapp:latest
IMAGE          CREATED       CREATED BY                                      SIZE
a1b2c3d4e5f6   2 hours ago   CMD ["python3" "/app/app.py"]                  0B
b2c3d4e5f6a1   2 hours ago   COPY app.py /app/                              5kB
c3d4e5f6a1b2   2 hours ago   RUN apt-get install -y python3                 150MB
d4e5f6a1b2c3   2 hours ago   RUN apt-get update                             50MB
e5f6a1b2c3d4   2 hours ago   FROM ubuntu:22.04                              77MB

# Inspecionar detalhes de uma imagem
docker inspect myapp:latest

Ferramentas auxiliares:

# dive - análise visual de layers
dive myapp:latest

# hadolint - lint para Dockerfile
hadolint Dockerfile

# buildx - debug de cache
docker buildx build --no-cache-filter=myapp -t myapp:latest .

Para identificar quebra de cache, analise os logs de build:

$ docker build -t myapp:latest .
Step 1/5 : FROM node:18
 ---> abc123
Step 2/5 : WORKDIR /app
 ---> Using cache
Step 3/5 : COPY package.json ./
 ---> 9f8e7d6c5b4a    # Cache miss! package.json mudou
Step 4/5 : RUN npm install
 ---> Running in 1a2b3c4d5e6f  # Precisa reinstalar

Referências