As builds multi-stage do Docker mudaram a forma como faço deploy de software. Antes de as usar, as minhas imagens de produção estavam inchadas com ferramentas de build, compiladores e dependências de testes que não tinham nada a ver com produção. Uma API simples em .NET a ocupar 1.2 GB. Uma app React com toda a toolchain do Node.js embutida. Era vergonhoso.
O Que São as Builds Multi-Stage
Uma build multi-stage permite usar múltiplas instruções FROM num único Dockerfile. Cada stage é isolado. Podes copiar apenas os artefactos necessários de um stage para o seguinte. A imagem final contém apenas o que colocares explicitamente nela.
Aqui está como fica um Dockerfile típico de uma API .NET com builds multi-stage:
# Stage 1: build
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish
# Stage 2: runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]
A imagem do SDK tem cerca de 750 MB. A imagem de runtime do ASP.NET tem cerca de 220 MB. A imagem final apenas inclui o stage de runtime. O stage de build é descartado.
O Impacto na Performance É Real
Para além do tamanho da imagem, as builds multi-stage afectam os tempos de build em CI. O Docker faz cache de cada layer. Se as dependências não mudarem entre commits, o cache no passo de restauro de dependências poupa minutos em cada execução do pipeline.
Num projeto em que trabalhei, a mudança para builds multi-stage reduziu a imagem Docker de 1.1 GB para 180 MB. O tempo de pull no Kubernetes baixou de 40 segundos para menos de 8. Isso acumula ao longo de dezenas de deploys por semana.
Um Exemplo com React no Frontend
O mesmo padrão aplica-se a builds de frontend:
# Stage 1: build
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Stage 2: serve
FROM nginx:alpine AS runtime
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
O stage de build com Node.js faz o trabalho pesado. O stage de runtime é apenas o nginx com os ficheiros estáticos. Sem node_modules. Sem código-fonte. Uma app React típica fica abaixo dos 30 MB desta forma.
Correr Testes Dentro da Build
Um padrão que acho pouco usado é correr os testes como um stage dedicado. Se o stage de testes falhar, a build para. Nada é publicado.
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
FROM build AS test
RUN dotnet test --no-restore
FROM build AS publish
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "MyApi.dll"]
Constróis o stage de testes apenas quando precisas. Em CI, aponta explicitamente para ele:
docker build --target test -t myapp:test .
Isto separa a execução dos testes da build da imagem de produção de forma limpa.
Erros Comuns a Evitar
O maior erro que vejo é copiar as coisas erradas entre stages. As pessoas por vezes copiam toda a árvore de código-fonte para o stage de runtime em vez de apenas o output compilado. O ponto central das builds multi-stage é a selectividade.
Outro problema é ignorar o ficheiro .dockerignore. Se copiares todo o diretório do projeto sem um .dockerignore adequado, envias os teus node_modules ou pastas bin locais para o contexto de build. Isso atrasa todas as builds, mesmo com cache.
Um .dockerignore mínimo para um projeto .NET:
**/bin/
**/obj/
**/.git/
**/*.user
Quando as Builds Multi-Stage Não Chegam
As builds multi-stage resolvem o inchaço das imagens e simplificam os Dockerfiles. Não substituem uma estratégia adequada de imagens base. Se ainda estiveres a usar debian:latest como base de runtime, estás a carregar muita superfície desnecessária. As imagens baseadas em Alpine ou as imagens distroless vão ainda mais longe.
Para serviços de produção onde o tamanho da imagem e a superfície de ataque realmente importam, combina builds multi-stage com imagens distroless. A Google publica imagens distroless para .NET, Java e Go. O resultado é uma imagem sem shell, sem gestor de pacotes e com um conjunto de binários instalados drasticamente reduzido.
A Conclusão
As builds multi-stage são uma dessas funcionalidades do Docker que compensam o pequeno investimento de imediato. Imagens mais pequenas fazem pull mais depressa, reduzem custos de armazenamento e diminuem a superfície de ataque. Mover os testes para o pipeline de build apanha falhas mais cedo. Não há nenhuma boa razão para publicar uma imagem de 1 GB quando uma de 150 MB faz o mesmo trabalho.
Se ainda não estás a usar builds multi-stage para todos os serviços que containerizas, começa agora. O Dockerfile fica mais longo em dez linhas. Os benefícios acumulam-se com o tempo.



