Dicas para escrever Dockerfiles multi-stage para imagens menores

1. Fundamentos do Multi-stage Build

Multi-stage build é uma funcionalidade do Docker que permite utilizar múltiplas instruções FROM em um único Dockerfile, criando estágios separados de construção. Cada estágio pode usar uma imagem base diferente, e apenas os artefatos necessários são copiados para o estágio final.

A principal vantagem é a redução drástica do tamanho da imagem final. Enquanto uma imagem tradicional pode conter todo o SDK, compiladores e bibliotecas de desenvolvimento, o multi-stage permite descartar esses componentes após a compilação.

A sintaxe básica utiliza FROM múltiplo e COPY --from para transferir arquivos entre estágios:

# Estágio de build
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o meuapp .

# Estágio final (runtime)
FROM alpine:3.19
COPY --from=builder /app/meuapp /usr/local/bin/meuapp
CMD ["meuapp"]

2. Escolhendo as Imagens Base Certas em Cada Estágio

A seleção cuidadosa das imagens base é crucial para otimizar o tamanho final. No estágio de build, use imagens que contenham as ferramentas necessárias para compilação, mas evite versões desnecessariamente grandes.

Para o estágio final, priorize imagens minimalistas:

# Estágio de build com imagem oficial específica
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

# Estágio final com imagem Alpine leve
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html

Evite usar ubuntu:latest ou debian:latest no runtime quando uma imagem Alpine (cerca de 5MB) ou distroless (cerca de 2MB) for suficiente.

3. Isolamento de Dependências de Compilação e Runtime

Um dos maiores benefícios do multi-stage é separar completamente as dependências de desenvolvimento das de produção. Ferramentas como compiladores, linters e bibliotecas de desenvolvimento não devem estar presentes na imagem final.

Exemplo com Python:

# Estágio de build
FROM python:3.12-slim AS builder
WORKDIR /app
COPY requirements.txt .
RUN pip install --user -r requirements.txt

# Estágio final
FROM python:3.12-slim
COPY --from=builder /root/.local /root/.local
COPY . /app
WORKDIR /app
ENV PATH=/root/.local/bin:$PATH
CMD ["python", "app.py"]

Remova artefatos temporários como caches de pacotes (/var/cache/apk/* no Alpine) e arquivos objeto (.o) entre estágios.

4. Estratégias de Cópia Inteligente com COPY --from

Copie apenas o essencial do estágio de build para o runtime. Evite copiar pastas inteiras sem filtros, pois isso pode incluir arquivos desnecessários.

# Copiando apenas o binário compilado
COPY --from=builder /app/bin/app /usr/local/bin/

# Copiando bibliotecas específicas
COPY --from=builder /usr/lib/x86_64-linux-gnu/libssl.so* /usr/lib/

# Múltiplos estágios intermediários para dependências separadas
FROM node:20-alpine AS dependencies
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS build
COPY --from=dependencies /app/node_modules ./node_modules
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

5. Otimização de Camadas com Multi-stage e Cache

A ordenação das instruções no Dockerfile impacta diretamente o cache do Docker. Coloque instruções que mudam com menos frequência (como instalação de dependências) antes das que mudam frequentemente (como cópia do código fonte).

# Ordem otimizada para cache
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o app .

FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /app/app /app
CMD ["/app"]

Combine comandos RUN para reduzir o número de camadas:

RUN apk add --no-cache curl && \
    curl -sL https://example.com/script.sh | sh && \
    apk del curl

Use .dockerignore para excluir arquivos desnecessários:

node_modules
.git
*.md
Dockerfile
.dockerignore

6. Tratamento de Dependências Dinâmicas e Bibliotecas Compartilhadas

Aplicações compiladas com ligação dinâmica podem precisar de bibliotecas compartilhadas no runtime. Use ldd para identificar dependências e copie-as manualmente:

FROM alpine:3.19 AS builder
RUN apk add --no-cache build-base
COPY app.c .
RUN gcc -o app app.c

FROM alpine:3.19
COPY --from=builder /app /app
RUN ldd /app | grep "=> /" | awk '{print $3}' | xargs -I {} cp {} /usr/lib/
CMD ["/app"]

Para runtime mínimo, considere imagens distroless como gcr.io/distroless/base:

FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o app .

FROM gcr.io/distroless/base
COPY --from=builder /app/app /app
CMD ["/app"]

7. Exemplo Prático: Dockerfile Multi-stage para uma Aplicação Go

Aplicação Go é um caso clássico onde multi-stage gera redução impressionante de tamanho:

# Estágio 1: Compilação
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/server .

# Estágio 2: Runtime
FROM scratch
COPY --from=builder /app/server /server
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 8080
CMD ["/server"]

Resultado: uma imagem que normalmente seria de 800MB com golang:latest reduz para aproximadamente 10MB com scratch.

8. Erros Comuns e Boas Práticas Finais

Erro 1: Esquecer de definir WORKDIR consistente entre estágios. Sempre defina WORKDIR explicitamente em cada estágio.

FROM node:20-alpine AS builder
WORKDIR /app  # Importante!
COPY . .
RUN npm run build

FROM nginx:alpine
WORKDIR /usr/share/nginx/html  # Diferente, mas necessário
COPY --from=builder /app/dist .

Erro 2: Manter segredos ou credenciais em estágios intermediários. Use --mount=type=secret para evitar vazamentos:

RUN --mount=type=secret,id=api_key \
    API_KEY=$(cat /run/secrets/api_key) go build -o app .

Erro 3: Não testar segurança da imagem final. Use ferramentas como Trivy ou Snyk:

trivy image minha-imagem:latest

Boas práticas finais:
- Sempre especifique tags exatas (evite latest)
- Use --no-cache em gerenciadores de pacotes
- Configure HEALTHCHECK para aplicações web
- Documente cada estágio com comentários claros

Multi-stage build é uma técnica essencial para qualquer profissional que busca eficiência em containers. Com as dicas acima, você pode reduzir significativamente o tamanho de suas imagens, melhorar a segurança e acelerar deploys.

Referências