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
ImagemTamanho
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 (.csproj antes do código)
  • .dockerignore configurado
  • Imagem chiseled ou alpine
  • Non-root user
  • Health check configurado
  • --no-restore no 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.