Modern .NET applications are increasingly deployed as containers, making Docker image optimization an essential part of application development. Large container images consume more storage, take longer to build and deploy, increase network transfer time, and often include unnecessary packages that expand the attack surface.
Fortunately, .NET provides several features to create smaller, faster, and more secure container images. Two of the most impactful techniques are multi-stage builds and chiseled images. Used together, they can significantly reduce image size while improving deployment efficiency and security.
In this article, you'll learn how these techniques work, when to use them, and the trade-offs involved when building production-ready .NET applications.
Why Docker Image Optimization Matters
A typical Docker image contains far more than just your application. It may include the .NET SDK, build tools, package managers, debugging utilities, and operating system components that aren't required at runtime.
Large images can lead to:
Slower CI/CD pipelines
Increased registry storage costs
Longer deployment times
Higher bandwidth consumption
Larger security attack surface
Optimizing your images helps deliver applications faster while reducing operational overhead.
Understanding Multi-Stage Builds
A common mistake is using the .NET SDK image for both building and running an application. While convenient, the SDK image contains compilers and development tools that are unnecessary in production.
A multi-stage build separates the build environment from the runtime environment.
Without Multi-Stage Builds
FROM mcr.microsoft.com/dotnet/sdk:8.0
WORKDIR /app
COPY . .
RUN dotnet publish -c Release -o /publish
ENTRYPOINT ["dotnet", "/publish/MyApp.dll"]
Although functional, the final image still contains the entire .NET SDK.
Using Multi-Stage Builds
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /src
COPY . .
RUN dotnet publish -c Release -o /app/publish
FROM mcr.microsoft.com/dotnet/aspnet:8.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
The first stage compiles the application, while the second stage copies only the published output into a lightweight runtime image.
This approach creates significantly smaller production images without changing application behavior.
What Are Chiseled Images?
Introduced for .NET container workloads, chiseled images are minimal runtime images that remove everything not required to run your application.
Unlike traditional runtime images, chiseled images exclude:
Shell utilities
Package managers
Many operating system libraries
Diagnostic tools
This results in:
Smaller image sizes
Reduced attack surface
Faster downloads
Improved startup efficiency
A typical runtime image can be replaced with a chiseled image simply by changing the base image.
FROM mcr.microsoft.com/dotnet/aspnet:8.0-jammy-chiseled
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
For cloud-native applications running in Kubernetes or Azure Container Apps, chiseled images are often an excellent choice.
Multi-Stage Builds vs Chiseled Images
| Feature | Multi-Stage Build | Chiseled Image |
|---|---|---|
| Reduces image size | ✔ | ✔ |
| Removes SDK from production | ✔ | ✖ |
| Reduces operating system components | ✖ | ✔ |
| Improves security | ✔ | ✔ |
| Faster deployments | ✔ | ✔ |
| Can be used together | ✔ | ✔ |
These techniques are complementary rather than competing approaches.
Additional Image Optimization Techniques
Use a .dockerignore File
Avoid copying unnecessary files into the Docker build context.
Example:
bin/
obj/
.git/
.vscode/
README.md
A smaller build context results in faster builds and better layer caching.
Optimize Docker Layer Caching
Docker caches layers independently. Place instructions that change less frequently near the top of the Dockerfile.
Instead of copying the entire project immediately:
COPY *.csproj .
RUN dotnet restore
COPY . .
RUN dotnet publish -c Release -o /app/publish
If only source files change, Docker can reuse the cached restore layer, reducing build time.
Publish in Release Mode
Always publish production applications using the Release configuration.
dotnet publish -c Release
Release builds include compiler optimizations that improve runtime performance and reduce unnecessary debugging information.
Consider Trimming for Smaller Applications
For applications that don't rely heavily on reflection, publishing with trimming can reduce deployment size.
dotnet publish -c Release -p:PublishTrimmed=true
However, trimming isn't suitable for every application. Frameworks that use reflection extensively may require additional configuration to prevent runtime issues. Always test thoroughly before enabling trimming in production.
When Native AOT Makes Sense
.NET also supports Native Ahead-of-Time (Native AOT) compilation.
Native AOT offers:
Faster application startup
Lower memory usage
Self-contained native executables
However, it comes with trade-offs, including limited support for certain reflection-based libraries and longer build times. It's best suited for APIs, microservices, and command-line tools where startup performance is critical.
Best Practices
Use multi-stage builds for every production application.
Choose runtime or chiseled images instead of SDK images for deployment.
Maintain a comprehensive
.dockerignorefile.Publish using the Release configuration.
Leverage Docker layer caching to speed up builds.
Regularly update base images with security patches.
Evaluate trimming and Native AOT only after verifying application compatibility.
Common Mistakes
Deploying SDK Images
SDK images are intended for development and build environments. Using them in production increases image size and exposes unnecessary tooling.
Copying the Entire Repository First
Copying all files before restoring dependencies invalidates Docker's cache, leading to slower builds.
Ignoring Base Image Updates
Outdated base images may contain known vulnerabilities. Regularly rebuild images using the latest supported .NET runtime images.
Assuming Smaller Always Means Better
Minimal images improve security and deployment speed, but they also remove tools commonly used for debugging. Ensure your operational workflow accounts for this limitation before adopting chiseled images.
Conclusion
Docker image optimization is more than reducing image size—it's about improving deployment speed, strengthening security, and creating efficient cloud-native applications.
Multi-stage builds eliminate unnecessary build tools from production images, while chiseled images remove non-essential operating system components to create minimal runtime environments. Together, they form a powerful combination for modern .NET deployments.
By following best practices such as using Release builds, optimizing Docker layer caching, maintaining a .dockerignore file, and selecting the appropriate runtime image, you can build containerized .NET applications that are faster to deploy, easier to maintain, and better suited for production environments. Thoughtful image optimization not only improves developer productivity but also contributes to more reliable and cost-effective deployments across any container platform.

Join the conversation! Your thoughts help the community grow.