Olá! Na lição anterior, criamos um Dockerfile funcional e robusto para a nossa API .NET, definindo seu ponto de entrada com ENTRYPOINT. No entanto, como vimos, a imagem resultante é bastante grande. Isso ocorre porque ela contém todo o .NET SDK, ferramentas de compilação e o código-fonte — dependências necessárias para construir a aplicação, mas completamente desnecessárias para apenas executá-la.
Nesta lição, vamos resolver esse problema de forma elegante. O nosso objetivo é que você consiga implementar multi-stage builds (builds de múltiplos estágios) para otimizar o tamanho da imagem e separar as dependências de tempo de compilação das de tempo de execução. Esta é uma técnica fundamental e padrão na criação de imagens Docker para produção, resultando em contêineres mais enxutos, seguros e eficientes.
O Problema: Imagens de Estágio Único
O Dockerfile que temos até agora segue um padrão de estágio único. Ele usa uma única imagem base (mcr.microsoft.com/dotnet/sdk:8.0) para todas as tarefas: restaurar pacotes, compilar o código e, finalmente, empacotar a aplicação.
As desvantagens dessa abordagem são significativas:
- Tamanho da Imagem: A imagem final contém todo o .NET SDK, que pode ter mais de 1GB. Imagens grandes consomem mais espaço de armazenamento, demoram mais para serem enviadas e baixadas de registros (como Docker Hub ou AWS ECR) e aumentam os custos de rede.
- Segurança (Superfície de Ataque): A imagem de produção contém ferramentas de compilação (
dotnet build, compiladores, etc.) e possivelmente o código-fonte. Esses artefatos não são necessários para a execução e representam uma superfície de ataque maior. Se um invasor conseguir acesso ao contêiner, ele terá um ambiente de desenvolvimento completo à sua disposição.
Para entender conceitualmente por que isso é um problema, assista ao trecho a seguir, que, embora use um exemplo em Python, explica perfeitamente a sobrecarga desnecessária que carregamos em uma imagem de estágio único.
Day-26 | Multi Stage Docker Builds | Reduce Image Size by 800 % | Distroless Container Images | #k8s
Este vídeo do canal Abhishek.Veeramalla explica de forma clara as ineficiências de um Dockerfile tradicional de estágio único, detalhando como as dependências de build e a sobrecarga da imagem base levam a imagens Docker desnecessariamente grandes.
Assista do início em 01:35 até 05:02. Foque em entender como a imagem final acaba contendo não apenas o que é necessário para executar a aplicação (o runtime), mas também tudo o que foi usado para construí-la.
A Solução: Builds de Múltiplos Estágios
A solução do Docker para esse problema é o multi-stage build. A ideia é usar múltiplas instruções FROM dentro de um único Dockerfile. Cada instrução FROM inicia um novo "estágio" de build, que é temporário e pode ser descartado. Podemos então copiar seletivamente os artefatos de um estágio para o outro, resultando em uma imagem final que contém apenas o estritamente necessário.

Vamos aplicar essa técnica ao nosso projeto HelloApi. Nosso plano será:
- Estágio
build: Usar a imagem completa do .NET SDK para restaurar, compilar e publicar nossa aplicação. - Estágio
final: Começar com uma imagem base limpa e muito menor, a do ASP.NET Runtime, e copiar apenas os arquivos publicados do estágiobuildpara ela.
Escrevendo o Dockerfile de Múltiplos Estágios
Vamos reescrever nosso Dockerfile. Você pode substituir o conteúdo do seu arquivo Dockerfile existente por este:
# --- Estágio 1: Build ---
# Usamos a imagem completa do SDK e nomeamos este estágio como 'build'
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app
# Copia os arquivos de projeto e restaura as dependências primeiro para otimizar o cache
COPY *.csproj .
RUN dotnet restore
# Copia o restante do código-fonte e publica a aplicação
COPY . .
RUN dotnet publish -c Release -o /app/publish
# --- Estágio 2: Final ---
# Começamos de novo, com a imagem leve do ASP.NET Runtime
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
# Copia APENAS os artefatos publicados do estágio 'build'
COPY --from=build /app/publish .
# Define a porta e o ponto de entrada para a aplicação
ENV ASPNETCORE_URLS=http://+:8080
ENTRYPOINT ["dotnet", "HelloApi.dll"]
Vamos analisar as novidades:
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build: A palavra-chaveAS buildnomeia este primeiro estágio. Podemos usar qualquer nome, masbuildoubuilderé uma convenção comum.FROM mcr.microsoft.com/dotnet/aspnet:8.0: Esta linha inicia um novo estágio, completamente independente do anterior. A imagemaspnetcontém apenas o runtime necessário para executar aplicações ASP.NET Core, sendo muito menor que a imagemsdk.COPY --from=build /app/publish .: Esta é a instrução chave. Ela diz ao Docker para copiar o conteúdo do diretório/app/publishdo estágio que nomeamos comobuildpara o diretório de trabalho atual (/app) do nosso estágio final.
Para uma visão detalhada e passo a passo específica para .NET, o artigo a seguir é uma excelente leitura.
Multi-Stage Docker Builds: A .NET developer's guide
Este guia de Victor Frye, 'Multi-Stage Docker Builds: A .NET developer's guide', detalha o processo de criação de um Dockerfile multi-stage para uma API .NET, explicando a lógica por trás de cada etapa.
Leia o artigo inteiro. Preste atenção especial em como o autor estrutura os estágios (base, build, test, publish, final) e na dramática redução de tamanho da imagem final (de 2GB para 167.5MB).
O Impacto na Prática
Agora, vamos ver a mágica acontecer.
-
Com seu novo
Dockerfilesalvo, execute o comando de build no seu terminal:docker build -t hello-api:multi-stage . -
Após o build ser concluído, vamos comparar os tamanhos das imagens. Execute:
docker images hello-api
Você verá algo parecido com isto (os tamanhos exatos podem variar):
| REPOSITORY | TAG | IMAGE ID | CREATED | SIZE |
|---|---|---|---|---|
| hello-api | multi-stage | ... | ... | ~210MB |
| hello-api | 3.0 | ... | ... | ~1.37GB |
A diferença é impressionante! Reduzimos o tamanho da nossa imagem em mais de 80%, simplesmente separando o ambiente de build do ambiente de execução. A nova imagem hello-api:multi-stage é muito mais adequada para produção.
Para reforçar a universalidade e o poder desse conceito, assista a este vídeo que demonstra um caso extremo de otimização com uma aplicação em Go, onde a redução de tamanho é ainda mais espetacular.
Dockerize Your .NET Application in 5 Minutes!
Este vídeo de Amichai Mantinband mostra de forma rápida e prática como criar um Dockerfile multi-stage para uma aplicação .NET.
Assista ao vídeo inteiro (é curto). Ele demonstra um fluxo de três estágios (build, publish, run) que reforça o que acabamos de fazer. Observe como ele usa imagens base diferentes para cada finalidade.
O vídeo anterior usou scratch, uma imagem "distroless" (sem distribuição). Essas são imagens extremamente minimalistas, que contêm apenas a aplicação e suas dependências diretas, sem shell ou outras ferramentas do sistema operacional. Isso oferece uma segurança ainda maior, reduzindo a superfície de ataque ao mínimo absoluto.
Teste seu entendimento!
Você tem o seguinte Dockerfile de estágio único para uma aplicação Node.js:
# Usa a imagem completa do Node.js que inclui npm
FROM node:18
WORKDIR /usr/src/app
# Copia os arquivos de dependência e os instala
COPY package*.json ./
RUN npm install
# Copia o código da aplicação
COPY . .
# Expõe a porta e inicia a aplicação
EXPOSE 3000
CMD [ "node", "server.js" ]
A imagem node:18 é grande porque contém todo o ambiente de desenvolvimento Node.js. Como você converteria este Dockerfile para um multi-stage build para criar uma imagem de produção otimizada?
Dica: A imagem node:18-alpine é uma alternativa muito menor que contém apenas o runtime do Node.js.
Show answer
Uma solução de múltiplos estágios seria:
# --- Estágio 1: Build ---
FROM node:18 AS builder
WORKDIR /usr/src/app
COPY package*.json ./
# O 'npm ci' é frequentemente preferido em CI/CD para instalações determinísticas
RUN npm ci
COPY . .
# --- Estágio 2: Final ---
# Usa uma imagem Alpine, que é muito menor
FROM node:18-alpine
WORKDIR /usr/src/app
# Copia as dependências instaladas e o código do estágio de build
COPY --from=builder /usr/src/app/node_modules ./node_modules
COPY --from=builder /usr/src/app .
EXPOSE 3000
CMD [ "node", "server.js" ]
Este Dockerfile usa a imagem node:18 completa para instalar as dependências (npm ci), e depois copia apenas os node_modules e o código-fonte para a imagem final e enxuta node:18-alpine, que só precisa do runtime para executar a aplicação.
Conclusão
Nesta lição, você dominou uma das técnicas mais importantes para a criação de imagens Docker prontas para produção.
Principais pontos a reter:
- Separação de Interesses: Multi-stage builds permitem separar o ambiente de build (com SDKs e ferramentas pesadas) do ambiente de runtime (apenas com as dependências de execução).
- Otimização de Tamanho: O resultado são imagens finais drasticamente menores, mais rápidas para transferir e mais baratas para armazenar.
- Segurança Aprimorada: Ao excluir ferramentas de build e código-fonte da imagem final, você reduz significativamente a superfície de ataque do seu contêiner.
- Sintaxe Chave: O padrão se baseia em múltiplas instruções
FROM, nomeando os estágios comAS nome_do_estagioe copiando artefatos entre eles comCOPY --from=nome_do_estagio.
Com um Dockerfile mais complexo e otimizado, entender como o Docker constrói as imagens camada por camada se torna ainda mais crucial para otimizar os tempos de build.
Na próxima lição, vamos mergulhar no funcionamento interno do cache do Docker. Você aprenderá como o cache de camadas otimiza o processo de build de imagens e como estruturar seu Dockerfile para aproveitar ao máximo esse mecanismo, acelerando significativamente seus builds subsequentes.