Uma imagem Docker de 1,2GB carrega compiladores, gerenciadores de pacote e devDependencies para servir uma aplicação que precisa de 90MB. Multi-stage builds resolvem isso separando compilação de execução: você constrói em um estágio e copia só o artefato para o estágio final. O app Express deste exemplo caiu de 1,1GB para 150MB com Alpine e para 90MB com distroless, sem mudar uma linha de código.

O que sobra em uma imagem single-stage?

O estágio único mistura o que constrói com o que executa. node_modules completo com devDependencies, gcc, curl e cache do pip entram em produção junto. Cada pacote extra aparece no relatório do Trivy e amplia a superfície de ataque. Pulls de 1GB em cada nó do cluster custam minutos por deploy.

Anatomia de um multi-stage build

O Dockerfile declara dois FROM. O estágio builder instala tudo e compila. O estágio runtime parte de uma base limpa e recebe só o resultado com COPY --from=builder /app/dist ./dist. Camadas do builder jamais chegam à imagem final.

Base image para o runtime

Variantes Alpine encolhem a imagem, e musl libc quebra módulos nativos compilados contra glibc. Teste antes de migrar. Imagens distroless shipam sem shell e sem gerenciador de pacotes, deixando nada para um atacante explorar. A tag :debug devolve busybox e salva o incident response.

Layer caching

A ordem das instruções define o cache. Copie package.json antes do código-fonte, rode a instalação de dependências e só então copie o resto. Uma alteração de código reaproveita as camadas de dependência e transforma builds de minutos em segundos.

Por linguagem

  • Node: rode npm ci --omit=dev no estágio final e copie a saída do tsc do builder
  • Go: compile com CGO_ENABLED=0 e suba um binário estático de ~10MB sobre scratch
  • Python: mantenha as dependências de build do pip no builder e copie as wheels para o estágio slim

Endurecimento da imagem

Adicione um usuário non-root com USER. Fixe a base por digest (node@sha256:...) em vez de tag móvel. Use COPY --chown para evitar arquivos owned pelo root. Em Kubernetes, monte o root filesystem como read-only.

Verificação do resultado

dive inspeciona camada por camada e aponta desperdício. docker history expõe segredo colado em ARG ou RUN. O Trivy roda no CI e bloqueia o deploy quando CVE alta aparece na imagem final.

O ganho composto aparece no pipeline: pulls dez vezes mais rápidos, scan enxuto e menos pacote para patchear na próxima emergência. Comece pelo Dockerfile mais antigo do monorepo; o de 1,2GB sempre existe.