Um Dockerfile mal escrito pode gerar imagens de 2GB, builds lentos e deploys demorados. Com algumas técnicas simples, é possível reduzir a imagem para menos de 100MB e cortar o tempo de build pela metade.
O Dockerfile ingênuo
Este é o Dockerfile que a maioria dos devs escreve no primeiro dia:
FROM mcr.microsoft.com/dotnet/sdk:10.0
WORKDIR /app
COPY . .
RUN dotnet publish -c Release -o /out
EXPOSE 8080
ENTRYPOINT ["dotnet", "/out/MinhaApi.dll"]
Problemas:
- Imagem final inclui o SDK inteiro (~900MB)
- Todos os arquivos do repositório são copiados (incluindo
.git,node_modules, testes) - Nenhum cache de layers — qualquer mudança no código invalida tudo
- Roda como root
Multi-stage build: a base de tudo
Separe o build do runtime. O SDK só é necessário para compilar:
# === STAGE 1: Build ===
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
# Copia apenas os arquivos de projeto primeiro (cache de restore)
COPY *.sln .
COPY src/MinhaApi/*.csproj src/MinhaApi/
RUN dotnet restore
# Agora copia o código e publica
COPY src/ src/
RUN dotnet publish src/MinhaApi -c Release -o /app/publish --no-restore
# === STAGE 2: Runtime ===
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MinhaApi.dll"]
Resultado: imagem final usa apenas o runtime (~220MB em vez de ~900MB).
Otimização 1: cache de NuGet restore
A técnica mais impactante. Copie os .csproj antes do código-fonte:
# Esses arquivos mudam raramente → layer cacheada
COPY src/MinhaApi/*.csproj src/MinhaApi/
COPY src/MinhaApi.Domain/*.csproj src/MinhaApi.Domain/
COPY src/MinhaApi.Infra/*.csproj src/MinhaApi.Infra/
RUN dotnet restore
# Código muda frequentemente → layer separada
COPY src/ src/
RUN dotnet publish -c Release -o /app/publish --no-restore
Se você só mudou código C#, o dotnet restore usa cache e o build fica muito mais rápido.
Automatizando com glob pattern
Para projetos com muitos .csproj, use um script:
COPY *.sln .
COPY **/*.csproj ./
RUN find . -name "*.csproj" -exec sh -c \
'mkdir -p $(dirname {}) && mv {} $(dirname {})/' \;
RUN dotnet restore
Otimização 2: imagens chiseled (distroless)
Imagens chiseled da Microsoft não têm shell, package manager nem ferramentas desnecessárias:
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS runtime
| Imagem | Tamanho |
|---|---|
sdk:10.0 | ~900MB |
aspnet:10.0 | ~220MB |
aspnet:10.0-alpine | ~110MB |
aspnet:10.0-noble-chiseled | ~85MB |
A diferença é brutal — de 900MB para 85MB.
Otimização 3: Native AOT
Para APIs simples e workers, publique com AOT nativo. O resultado é um binário único, sem dependência do runtime .NET:
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -r linux-x64 \
--self-contained true \
-p:PublishAot=true \
-o /app/publish
FROM mcr.microsoft.com/dotnet/runtime-deps:10.0-noble-chiseled
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["./MinhaApi"]
Resultado: imagem final ~30-50MB, startup em milissegundos.
Atenção: AOT tem limitações com reflection, dynamic loading e alguns pacotes NuGet. Teste bem antes de adotar.
Otimização 4: .dockerignore
Crie um .dockerignore para evitar copiar lixo para o contexto do Docker:
**/.git
**/bin
**/obj
**/node_modules
**/.vs
**/.idea
**/TestResults
**/*.user
**/*.md
**/docker-compose*.yml
Isso reduz o tamanho do build context e acelera o COPY.
Otimização 5: não rode como root
Crie um usuário não-root para o container:
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS runtime
# Imagens chiseled já rodam como non-root por padrão
# Para imagens normais:
RUN adduser --disabled-password --gecos "" appuser
USER appuser
WORKDIR /app
COPY --from=build /app/publish .
EXPOSE 8080
ENTRYPOINT ["dotnet", "MinhaApi.dll"]
Otimização 6: health check
Adicione um health check direto no Dockerfile:
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
Ou para imagens sem curl (chiseled):
// No Program.cs
app.MapHealthChecks("/health");
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD ["dotnet", "MinhaApi.dll", "--urls", "http://localhost:8080/health"]
Dockerfile final otimizado
# === Build ===
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY *.sln .
COPY src/MinhaApi/*.csproj src/MinhaApi/
COPY src/MinhaApi.Domain/*.csproj src/MinhaApi.Domain/
COPY src/MinhaApi.Infra/*.csproj src/MinhaApi.Infra/
RUN dotnet restore
COPY src/ src/
RUN dotnet publish src/MinhaApi -c Release -o /app/publish --no-restore
# === Runtime ===
FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS runtime
WORKDIR /app
COPY --from=build /app/publish .
ENV ASPNETCORE_URLS=http://+:8080
EXPOSE 8080
ENTRYPOINT ["dotnet", "MinhaApi.dll"]
Checklist de otimização
- Multi-stage build (SDK → Runtime)
- Cache de restore (
.csprojantes do código) -
.dockerignoreconfigurado - Imagem chiseled ou alpine
- Non-root user
- Health check configurado
-
--no-restoreno publish - Variáveis de ambiente no runtime (não no build)
Conclusão
Docker e .NET funcionam muito bem juntos quando o Dockerfile é bem escrito. As técnicas deste artigo são aplicáveis a qualquer projeto — de uma API simples a um microsserviço complexo. O investimento em otimização se paga em cada deploy: builds mais rápidos, imagens menores e containers mais seguros.
Precisa de ajuda com sua infraestrutura de containers? A DevPlus é especialista em Cloud & DevOps. Fale com a gente.