Docker  

Reducing Docker Image Size for .NET 11 Production Apps

Containerization has become the standard way to deploy modern ASP.NET Core applications. Smaller Docker images are faster to build, quicker to pull, consume less storage, and reduce deployment times across development, testing, and production environments.

Many .NET applications, however, ship with unnecessarily large Docker images due to inefficient Dockerfiles, unused dependencies, and poor layering strategies.

In this article, you'll learn practical techniques to reduce Docker image size for .NET 11 applications, improve build efficiency, and create production-ready container images.

Note: This article focuses on optimization techniques and build methodology. Actual image size reductions and startup improvements depend on your application, base image, dependencies, and deployment environment.

Why Docker Image Size Matters

Large Docker images introduce several operational challenges:

  • Longer CI/CD build times

  • Slower image downloads

  • Increased storage costs

  • Longer deployment times

  • Higher network bandwidth usage

Smaller images help applications scale more efficiently, especially in Kubernetes and cloud environments.

Understanding a Docker Image

A Docker image consists of multiple read-only layers.

Application
      │
      ▼
Published Files
      │
      ▼
.NET Runtime
      │
      ▼
Linux Base Image

Each Docker instruction creates a new layer. Poor layer management increases image size unnecessarily.

Choose the Right Base Image

Microsoft provides multiple .NET container images.

ImageUse Case
SDKBuilding applications
ASP.NET RuntimeRunning Web APIs
RuntimeConsole applications
Runtime-DependenciesSelf-contained deployments

A common mistake is deploying applications using the SDK image.

Avoid:

FROM mcr.microsoft.com/dotnet/sdk:11.0-preview

Instead, use the ASP.NET runtime image for production.

FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview

The runtime image contains only the components needed to run the application.

Use Multi-Stage Builds

Multi-stage builds separate compilation from runtime.

FROM mcr.microsoft.com/dotnet/sdk:11.0-preview AS build

WORKDIR /src

COPY . .

RUN dotnet publish -c Release -o /app

FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview

WORKDIR /app

COPY --from=build /app .

ENTRYPOINT ["dotnet","MyApi.dll"]

Why Multi-Stage Builds?

The SDK is only required during compilation.

By copying only the published output into the runtime image, unnecessary build tools and intermediate files are excluded.

Copy Only Required Files

Avoid copying the entire project immediately.

Instead:

COPY MyApi.csproj .
RUN dotnet restore

COPY . .

This improves Docker layer caching because dependency restoration is skipped when only source files change.

Use a .dockerignore File

Exclude unnecessary files from the build context.

Example:

bin/
obj/
.git/
.vscode/
TestResults/
README.md

A smaller build context reduces build time and avoids copying unnecessary content into the image.

Publish in Release Mode

Always publish production applications using Release mode.

dotnet publish -c Release

Release builds produce optimized binaries suitable for production deployments.

Enable Publish Trimming

Publish trimming removes unused framework code.

dotnet publish -c Release -p:PublishTrimmed=true

Before enabling trimming, verify that your application and third-party libraries are compatible, as aggressive trimming may remove code accessed through reflection.

Enable ReadyToRun Compilation

ReadyToRun precompiles assemblies to improve startup performance.

dotnet publish -c Release -p:PublishReadyToRun=true

This may increase the published output size while reducing application startup time. Evaluate the trade-off for your workload.

Use Invariant Globalization (When Appropriate)

Applications that do not require full globalization support can reduce image size.

<PropertyGroup>
    <InvariantGlobalization>true</InvariantGlobalization>
</PropertyGroup>

Only enable this option if your application does not depend on culture-specific formatting or localization.

Run as a Non-Root User

Production containers should avoid running as the root user.

RUN adduser --disabled-password appuser

USER appuser

Running as a non-root user improves container security without affecting image size significantly.

Optimize Docker Layers

Avoid multiple RUN instructions when possible.

Less efficient:

RUN apt-get update

RUN apt-get install curl

Better:

RUN apt-get update && \
    apt-get install -y curl && \
    rm -rf /var/lib/apt/lists/*

Combining related commands reduces unnecessary image layers and removes temporary package metadata.

End-to-End Dockerfile

A production-oriented example:

FROM mcr.microsoft.com/dotnet/sdk:11.0-preview AS build

WORKDIR /src

COPY MyApi.csproj .

RUN dotnet restore

COPY . .

RUN dotnet publish -c Release -o /app \
    -p:PublishTrimmed=true

FROM mcr.microsoft.com/dotnet/aspnet:11.0-preview

WORKDIR /app

COPY --from=build /app .

ENTRYPOINT ["dotnet","MyApi.dll"]

This approach produces a cleaner runtime image by separating build and execution environments.

Layer Optimization Comparison

TechniqueBenefit
Multi-stage buildRemoves SDK from runtime image
Runtime imageSmaller than SDK image
.dockerignoreSmaller build context
Release buildOptimized binaries
Publish trimmingRemoves unused framework code
Layer optimizationReduces duplicate image layers
ReadyToRunFaster startup (larger binaries)
Non-root userImproved container security

Build Optimization Methodology

The research brief mentions startup benchmarks but does not include measured results or a benchmark environment. Instead of presenting unsupported numbers, use the following methodology to evaluate your own builds.

Test Environment

Keep the following consistent:

  • .NET SDK version

  • Docker version

  • Base image

  • Build configuration

  • Hardware

  • Operating system

Build Scenarios

Compare:

  • SDK image vs runtime image

  • Single-stage vs multi-stage build

  • Trimmed vs non-trimmed publish

  • ReadyToRun enabled vs disabled

  • Different base images

Metrics to Measure

Collect:

  • Final image size

  • Build duration

  • Container startup time

  • Memory usage

  • CPU utilization

  • Image pull time

Useful Tools

Useful tools include:

  • Docker CLI

  • docker image ls

  • docker history

  • docker inspect

  • docker stats

  • BenchmarkDotNet (application startup scenarios)

  • k6 or Bombardier (API load testing after deployment)

Validate optimizations under production-like conditions before adopting them across all services.

Best Practices

  • Use multi-stage Docker builds.

  • Prefer runtime images for production.

  • Exclude unnecessary files using .dockerignore.

  • Publish in Release mode.

  • Evaluate trimming before enabling it globally.

  • Run containers as non-root users.

  • Keep Dockerfiles simple and maintainable.

  • Regularly update base images with supported releases.

Common Mistakes

MistakeImpact
Using SDK image in productionLarger image size
Copying the entire project earlyPoor layer caching
Missing .dockerignoreLarger build context
Keeping temporary package filesIncreased image size
Running as rootReduced security
Ignoring base image updatesSecurity and maintenance risks

Troubleshooting

Docker Image Is Larger Than Expected

Review:

  • Base image selection

  • Multi-stage build configuration

  • Published output

  • .dockerignore

  • Additional installed packages

Use docker history to identify unexpectedly large layers.

Build Cache Is Not Working

Ensure project files are copied before the remaining source files so Docker can reuse the restore layer when dependencies have not changed.

Application Fails After Enabling Trimming

Some libraries rely on reflection or dynamically loaded types. Test the application thoroughly and disable trimming for incompatible assemblies if necessary.

FAQs

Why shouldn't I use the SDK image in production?

The SDK image contains build tools that are unnecessary for running an application, making it significantly larger than the runtime image.

What is a multi-stage build?

A Docker build process that separates compilation from runtime, allowing only the published application to be included in the final image.

Does trimming always reduce image size safely?

No. While trimming can significantly reduce the published output, some applications and libraries require additional configuration to work correctly after trimming.

Does ReadyToRun reduce image size?

No. ReadyToRun generally increases binary size in exchange for improved application startup performance.

How can I inspect Docker image layers?

Use commands such as docker history, docker inspect, and docker image ls to review image composition and identify optimization opportunities.

Conclusion

Optimizing Docker images is an important part of building efficient .NET applications for production. Smaller images improve build speed, reduce deployment time, lower storage costs, and simplify scaling across cloud and container orchestration platforms.

By using multi-stage builds, selecting appropriate base images, reducing build context, enabling Release optimizations, and validating changes through repeatable measurements, you can create production-ready Docker images that are both efficient and maintainable.