Multi-stage builds: imagens menores e mais seguras
1. O Problema das Imagens Monolíticas
1.1. Imagens inchadas: dependências de build vs. dependências de runtime
No desenvolvimento tradicional de Dockerfiles, é comum usar uma única imagem base que contém tudo o que é necessário para compilar e executar a aplicação. Isso resulta em imagens que carregam compiladores, bibliotecas de desenvolvimento, ferramentas de depuração e outros artefatos que são úteis apenas durante o processo de build, mas completamente desnecessários em produção.
# Dockerfile tradicional - Problema: tudo em uma única imagem
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "start"]
Essa abordagem gera imagens que podem facilmente ultrapassar 1GB, sendo que apenas uma fração desse conteúdo é realmente necessária para executar a aplicação.
1.2. Aumento da superfície de ataque
Cada biblioteca, ferramenta ou binário adicional na imagem representa um potencial vetor de ataque. Compiladores como gcc, interpretadores como bash, e utilitários como curl ou wget são frequentemente explorados em ataques. Uma imagem que contém apenas o essencial para rodar a aplicação reduz drasticamente a superfície de ataque.
1.3. Impacto no desempenho no Kubernetes
Em clusters Kubernetes, imagens maiores impactam diretamente:
- Tempo de pull: nós precisam baixar a imagem inteira antes de iniciar o container
- Armazenamento: cada nó armazena múltiplas versões de imagens
- Escalabilidade: durante escalonamento horizontal, o tempo de inicialização aumenta proporcionalmente ao tamanho da imagem
2. Conceitos Fundamentais do Multi-stage Build
2.1. Estrutura básica: múltiplos blocos FROM
O multi-stage build permite usar múltiplas instruções FROM em um único Dockerfile. Cada FROM inicia um novo estágio, que pode usar uma imagem base diferente.
# Estrutura básica de multi-stage build
FROM imagem-base-build AS build-stage
# ... comandos de build
FROM imagem-base-producao AS final-stage
# ... apenas o necessário para execução
2.2. Copiando artefatos entre estágios com COPY --from=
O comando COPY --from=nome-do-estagio permite copiar arquivos específicos de um estágio anterior para o estágio atual, sem carregar todo o contexto do estágio de origem.
COPY --from=build-stage /app/dist /app
2.3. Estágio final: imagem de produção enxuta
O último estágio deve usar imagens base mínimas como alpine (5MB), slim ou as imagens distroless do Google (que não possuem shell, package manager ou qualquer utilitário).
3. Exemplo Prático: Aplicação Go
3.1. Estágio de build
# Dockerfile para aplicação Go com multi-stage build
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o /app/server ./cmd/server
3.2. Estágio final com imagem scratch
FROM scratch AS final
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]
A imagem scratch é o menor ponto de partida possível — um sistema de arquivos vazio. Para aplicações Go compiladas estaticamente, é a escolha ideal.
3.3. Comparação de tamanho
# Imagem tradicional (Go SDK completo)
REPOSITORY TAG SIZE
myapp latest 812MB
# Imagem multi-stage (scratch)
REPOSITORY TAG SIZE
myapp latest 12MB
# Redução de aproximadamente 98%
4. Exemplo Prático: Aplicação Node.js
4.1. Estágio de build com TypeScript
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json tsconfig.json ./
RUN npm ci
COPY src/ ./src/
RUN npm run build
4.2. Estágio de produção
FROM node:18-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]
4.3. Imagens base slim no estágio final
Usar node:18-alpine no estágio final reduz a imagem para aproximadamente 120MB, contra mais de 900MB da imagem node:18 completa. A instrução USER node garante execução como não-root, aumentando a segurança.
5. Estratégias de Segurança com Multi-stage Builds
5.1. Remoção de ferramentas de desenvolvimento
No estágio final, não há compiladores (gcc, typescript), gerenciadores de pacotes (npm, apt), shells (bash, sh) ou debuggers (gdb, lldb). Isso elimina vetores de ataque comuns.
5.2. Execução como usuário não-root
FROM node:18-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
COPY --from=builder --chown=appuser:appgroup /app/dist ./dist
5.3. Uso de imagens distroless
As imagens distroless do Google contêm apenas a aplicação e suas dependências de runtime, sem sistema operacional completo:
FROM golang:1.21 AS builder
# ... build
FROM gcr.io/distroless/base-debian12 AS final
COPY --from=builder /app/server /server
ENTRYPOINT ["/server"]
6. Integração com Kubernetes e CI/CD
6.1. Otimização de pull em clusters
Imagens menores reduzem o tempo de pull em clusters Kubernetes, especialmente durante:
- Rolling updates
- Escalonamento horizontal (HPA)
- Recuperação de nós falhos
# Com multi-stage: pull em ~2 segundos
# Sem multi-stage: pull em ~15 segundos
6.2. Pipeline CI com multi-stage
# Exemplo de pipeline GitLab CI
build:
stage: build
script:
- docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
- docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
6.3. Estratégias de cache de camadas
Para acelerar builds, organize as camadas que mudam com menos frequência no início:
# Camadas de dependências (mudam raramente)
COPY package*.json ./
RUN npm ci
# Camadas de código (mudam frequentemente)
COPY . .
RUN npm run build
7. Boas Práticas e Armadilhas Comuns
7.1. Estágios organizados com nomes descritivos
FROM node:18 AS dependencies
FROM node:18 AS build
FROM node:18-alpine AS test
FROM node:18-alpine AS production
7.2. Cuidado com segredos
Nunca copie arquivos com credenciais para o estágio final:
# ERRADO: .env com senhas no estágio final
COPY --from=builder /app/.env ./
# CERTO: usar secrets do Docker BuildKit
RUN --mount=type=secret,id=api_key npm run build
7.3. Evitar estágios desnecessários
Mantenha o número de estágios entre 2 e 4. Estágios excessivos aumentam a complexidade sem benefício proporcional.
8. Conclusão e Próximos Passos
O multi-stage build é uma técnica essencial no ecossistema DevOps com Docker e Kubernetes. Ele entrega três benefícios fundamentais:
- Redução drástica de tamanho: de centenas de MB para dezenas de MB
- Aumento de segurança: eliminação de ferramentas e bibliotecas desnecessárias
- Melhoria de performance: pull/push mais rápidos e inicialização mais ágil em clusters
Para se aprofundar, explore os próximos artigos da série sobre otimização de imagens com Docker Registry, segurança em containers e estratégias avançadas de deployment no Kubernetes.
Desafio prático: Pegue um Dockerfile existente em seu projeto e refatore-o para usar multi-stage builds. Meça a diferença de tamanho antes e depois.
Referências
- Docker官方文档: Multi-stage builds — Documentação oficial da Docker sobre multi-stage builds, com exemplos detalhados e melhores práticas
- Google Cloud: Distroless container images — Repositório oficial das imagens distroless do Google, com explicações sobre segurança e tamanho reduzido
- Kubernetes官方文档: Container images — Guia oficial do Kubernetes sobre otimização de imagens para clusters
- Node.js Docker: Melhores práticas oficiais — Guia da Node.js Foundation sobre criação de Dockerfiles otimizados para aplicações Node
- Snyk: Segurança em imagens Docker — Artigo técnico sobre 10 práticas de segurança para imagens Docker, incluindo multi-stage builds
- GitLab CI: Multi-stage Docker builds — Documentação do GitLab sobre integração de multi-stage builds em pipelines CI/CD
- Alpine Linux: Imagens base mínimas — Site oficial do Alpine Linux, imagem base utilizada nos exemplos práticos do artigo