Docker  

Docker Multi-Stage Builds

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:

  • Restoring dependencies

  • Compiling the application

  • Running tests

  • Publishing the application

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:

  • Smaller image size

  • Faster deployments

  • Improved security

  • Reduced storage requirements

  • Faster image downloads

  • Better container startup performance

These benefits are particularly important for cloud-native applications.

Traditional Build vs Multi-Stage Build

Here's a comparison of the two approaches.

FeatureTraditional BuildMulti-Stage Build
Image SizeLargerSmaller
Includes SDKYesNo
Runtime OnlyNoYes
SecurityLowerHigher
Deployment SpeedSlowerFaster

Multi-stage builds create cleaner and more efficient production images.

Understanding the Build Process

A typical multi-stage build follows these steps:

  1. Restore NuGet packages.

  2. Build the application.

  3. Publish the application.

  4. 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:

  • Speeds up builds

  • Reduces build context size

  • Improves build efficiency

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:

  • Download faster

  • Deploy more quickly

  • Consume less storage

  • Reduce network bandwidth

  • Start containers more rapidly

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:

  • Docker Scout

  • Microsoft Defender for Containers

  • Trivy

  • Snyk

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.