Introduction
Docker has become a standard tool for packaging and deploying applications. However, many developers unknowingly create Docker images that are much larger than necessary because they include build tools, source code, and temporary files that aren't required at runtime.
Docker Multi-Stage Builds solve this problem by separating the build environment from the final runtime environment. This approach produces smaller, faster, and more secure container images, making it especially valuable for .NET applications deployed to cloud platforms such as Azure Kubernetes Service (AKS), Azure Container Apps, and Docker environments.
In this article, you'll learn what Docker Multi-Stage Builds are, how they work, and how to use them to optimize your .NET applications.
What Are Docker Multi-Stage Builds?
A Multi-Stage Build allows you to use multiple stages within a single Dockerfile.
Typically, one stage is responsible for:
The final stage contains only the files required to run the application.
This approach removes unnecessary build tools and temporary files from the final Docker image.
Why Use Multi-Stage Builds?
Traditional Docker builds often include everything needed to build the application.
This can result in images that contain:
Source code
SDKs
Build tools
Package caches
Temporary files
Multi-stage builds provide several advantages:
These benefits are particularly important for cloud-native applications.
Traditional Build vs Multi-Stage Build
Here's a comparison of the two approaches.
| Feature | Traditional Build | Multi-Stage Build |
|---|
| Image Size | Larger | Smaller |
| Includes SDK | Yes | No |
| Runtime Only | No | Yes |
| Security | Lower | Higher |
| Deployment Speed | Slower | Faster |
Multi-stage builds create cleaner and more efficient production images.
Understanding the Build Process
A typical multi-stage build follows these steps:
Restore NuGet packages.
Build the application.
Publish the application.
Copy the published output to a lightweight runtime image.
Only the published application is included in the final image.
Create an ASP.NET Core Application
Suppose you have an ASP.NET Core Web API.
ProductApi
│
├── Controllers
├── Models
├── Services
├── Program.cs
├── ProductApi.csproj
└── Dockerfile
The Dockerfile will contain multiple stages for building and running the application.
Build Stage
The first stage uses the .NET SDK image.
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish
This stage restores dependencies and publishes the application.
The SDK image is only used during the build process.
Runtime Stage
The second stage uses the smaller ASP.NET Core runtime image.
FROM mcr.microsoft.com/dotnet/aspnet:10.0
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "ProductApi.dll"]
The runtime image contains only the published application and the .NET runtime.
This significantly reduces the final image size.
Build the Docker Image
Build the image using the Docker CLI.
docker build -t productapi .
Docker automatically executes each stage and produces the final optimized image.
Run the Container
Start the container using:
docker run -p 8080:80 productapi
Your ASP.NET Core application is now running inside a lightweight production-ready container.
Use a .dockerignore File
A .dockerignore file prevents unnecessary files from being copied into the build context.
Example:
bin/
obj/
.git/
.vs/
*.user
*.log
Excluding unnecessary files:
This is considered a best practice for Docker projects.
Cache Dependencies Efficiently
Docker builds layers sequentially.
To improve caching, copy the project file before copying the entire source code.
COPY ProductApi.csproj .
RUN dotnet restore
COPY . .
If only the source code changes, Docker can reuse the cached dependency layer, reducing build times.
Keep Images Small
Smaller images offer several advantages.
They:
Always include only the files required to run your application.
Scan Images for Vulnerabilities
Before deploying container images, scan them for known security vulnerabilities.
Popular tools include:
Regular scanning helps identify outdated packages and security issues before deployment.
Optimize for Production
Production container images should contain only runtime components.
Avoid including:
Source code
Development tools
Test projects
Build artifacts
Debugging utilities
A minimal image reduces both attack surface and maintenance effort.
Best Practices
When creating Docker Multi-Stage Builds, follow these recommendations:
Separate build and runtime stages.
Use the official .NET SDK image only for building.
Use the ASP.NET Core runtime image for production.
Add a .dockerignore file.
Take advantage of Docker layer caching.
Remove unnecessary files from the final image.
Scan container images regularly for vulnerabilities.
Keep base images updated with the latest security patches.
Test your container locally before deploying it.
These practices help create secure and efficient container images.
Common Mistakes to Avoid
Developers new to Docker often make mistakes that reduce the effectiveness of their containers.
Avoid these common issues:
Using the SDK image in production.
Copying the entire project before restoring dependencies.
Ignoring the .dockerignore file.
Including source code in the runtime image.
Running applications as the root user when unnecessary.
Failing to update base images regularly.
Deploying unscanned images to production.
Avoiding these issues improves both performance and security.
Conclusion
Docker Multi-Stage Builds are one of the most effective ways to optimize containerized .NET applications. By separating the build environment from the runtime environment, you can significantly reduce image size, improve deployment speed, and strengthen application security.
Whether you're deploying to Docker, Kubernetes, Azure Kubernetes Service, or another cloud platform, adopting multi-stage builds is a best practice that results in cleaner, faster, and more maintainable container images. Combined with efficient layer caching, proper use of .dockerignore, and regular vulnerability scanning, multi-stage builds provide a solid foundation for modern containerized applications.