Docker � �10 min read

Multi-Stage Docker Builds for .NET

Multi-stage Docker builds provide a cleaner and more efficient way to package .NET applications. By separating the build environment from the final runtime image, you can keep development tools out of production, reduce image size and create more focused containers. In this article, we’ll look at how multi-stage Docker builds work and how to use them with modern .NET applications.

Admin
Admin
.NET & IoT Developer
Multi-Stage Docker Builds for .NET

What Is a Multi-Stage Docker Build?

When you first start putting a .NET application into Docker, it is tempting to use a single image containing everything required to build and run the application.

For example, a .NET SDK image contains the .NET compiler, NuGet tooling and other development components required to restore, build and publish an application. While this is useful during development, none of those tools are normally required when the application is actually running.

A multi-stage Docker build solves this problem by allowing a Dockerfile to contain several stages. One stage can contain everything required to build the application, while another contains only the files and runtime components required to run it.

Docker starts a new stage whenever another FROM instruction is encountered. Files can then be copied from an earlier stage into a later stage using COPY --from. This allows the final image to contain the published application without including the complete development environment.

Why Use Multi-Stage Builds with .NET?

.NET applications generally need the SDK during development and compilation, but a deployed ASP.NET Core application only needs the appropriate runtime components.

A .NET SDK image is therefore considerably more capable than the final application actually needs.

A traditional single-stage Dockerfile might use the SDK image to restore packages, compile the application and run it. The resulting image can contain development tooling and intermediate files that serve no purpose once the application is running.

With a multi-stage Dockerfile, the SDK can be used in an intermediate build stage. The application is then published and the resulting files are copied into a separate runtime image.

This separation can make the final image smaller and reduces the number of components that need to be present in the production container. Docker specifically recommends multi-stage builds as a way of separating the build environment from the final output.

A Simple .NET Multi-Stage Dockerfile

Let's look at a straightforward example for an ASP.NET Core application.

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

WORKDIR /src

COPY ["MyWebApp.csproj", "./"]

RUN dotnet restore "MyWebApp.csproj"

COPY . .

RUN dotnet publish "MyWebApp.csproj" \
    -c Release \
    -o /app/publish \
    --no-restore


FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final

WORKDIR /app

COPY --from=build /app/publish .

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

There are two important stages in this example.

The first stage is called build. It uses the .NET SDK image because the SDK contains everything required to restore NuGet packages and compile the application.

The second stage is called final. It uses the ASP.NET Core runtime image rather than the SDK image.

The published application is copied from the build stage into the final stage with the following instruction:

COPY --from=build /app/publish .

The SDK image is therefore used to create the application, but it does not become part of the final runtime image.

Understanding the Build Stage

The first stage begins with:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

The FROM instruction specifies the base image, while AS build gives the stage a name.

Naming stages is useful because it makes the Dockerfile easier to understand and allows later stages to refer to the stage by name rather than by number. Docker recommends naming stages because this also makes the Dockerfile more resilient if stages are later reordered.

The working directory is then established:

WORKDIR /src

The project file is copied separately:

COPY ["MyWebApp.csproj", "./"]

The application dependencies can then be restored:

RUN dotnet restore "MyWebApp.csproj"

The remaining source code is copied into the image:

COPY . .

Finally, the application is published:

RUN dotnet publish "MyWebApp.csproj" \
    -c Release \
    -o /app/publish \
    --no-restore

The dotnet publish command produces the files required to deploy the application. The --no-restore option is used because the packages have already been restored earlier in the Dockerfile.

Understanding the Final Stage

The second stage starts with:

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final

This is an important difference.

The build stage uses the SDK image, whereas the final stage uses the ASP.NET Core runtime image. The final container therefore does not need the complete .NET SDK.

The working directory is established:

WORKDIR /app

The published application is then copied from the previous stage:

COPY --from=build /app/publish .

The application can finally be started with:

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

At this point the final image contains the runtime environment and the published application rather than the complete development environment.

Visual Studio's Docker support follows a similar concept, commonly separating stages such as base, build, publish and final. Microsoft documents this approach as a way of keeping build tooling out of the final container.

Why Copy the Project File First?

One small detail in the Dockerfile is particularly useful:

COPY ["MyWebApp.csproj", "./"]

RUN dotnet restore "MyWebApp.csproj"

COPY . .

You could simply copy the entire project before running dotnet restore, but separating these operations can make Docker's build cache much more useful.

The project file normally changes less frequently than the application source code. If the project file and its package references have not changed, Docker can potentially reuse the cached restore layer when only source files have been modified.

This can make repeated Docker builds significantly quicker.

Docker's build system includes caching specifically to avoid unnecessarily repeating expensive build operations.

Building the Docker Image

Once the Dockerfile has been created, the image can be built from the project directory:

docker build -t mywebapp .

Docker processes the stages in the Dockerfile and produces the final image from the last stage.

The intermediate build stage is still used during the build process, but the resulting production image is based on the final runtime stage.

The image can then be started with:

docker run -p 8080:8080 mywebapp

The exact port used will depend on the application's configuration and the ports exposed by the container.

Multi-Stage Builds and Docker BuildKit

Modern versions of Docker use BuildKit as the build engine. BuildKit provides a number of improvements to Docker builds, including more efficient processing and improved build caching.

One useful feature is that BuildKit can avoid building stages that are not required for the selected target.

For example, a Dockerfile could contain separate stages for development, testing and production. A specific stage can be selected using the --target option:

docker build --target build -t mywebapp-build .

This can be useful when troubleshooting a Dockerfile or when creating separate development and production workflows.

Adding a Test Stage

Multi-stage builds don't have to be limited to a build stage and a final runtime stage.

You can introduce a testing stage as well.

For example:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build

WORKDIR /src

COPY . .

RUN dotnet restore

RUN dotnet build -c Release --no-restore


FROM build AS test

RUN dotnet test -c Release --no-build


FROM build AS publish

RUN dotnet publish -c Release -o /app/publish --no-restore


FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final

WORKDIR /app

COPY --from=publish /app/publish .

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

This structure allows the same Dockerfile to contain the steps required to build, test and publish the application.

It also means that the Docker build process can become part of the application's development and deployment workflow rather than relying entirely on the developer's local machine.

Keeping the Final Image Focused

One of the biggest advantages of multi-stage builds is that the final image only needs to contain what the application requires at runtime.

The build stage can contain the .NET SDK, source code, NuGet packages and intermediate build files.

The final stage does not need all of those components.

This separation is particularly useful for production deployments because there is less unnecessary software inside the running container. Docker describes this as reducing the size and attack surface of the resulting image.

It is worth remembering, however, that a smaller image is not automatically a secure image. You should still keep your base images updated, use appropriate runtime images and follow good container security practices.

Multi-Stage Builds in Visual Studio

If you use Visual Studio to develop .NET applications, you may already have encountered multi-stage Dockerfiles without necessarily realising it.

Visual Studio can generate Dockerfiles containing multiple stages. A typical Dockerfile can contain a runtime base stage, a build stage using the .NET SDK, a publish stage and a final stage.

The build stage is responsible for compiling the application, while the publish stage creates the deployable output. The final stage then copies that output into the runtime image.

This is one of those Docker features that initially looks more complicated than it really is. Once you understand that each FROM creates a new stage, the structure becomes much easier to follow.

Using a .dockerignore File

A multi-stage Dockerfile can still be inefficient if you send unnecessary files to Docker during the build.

A .dockerignore file can be used to prevent files such as build output, Git files and other unnecessary content from being included in the Docker build context.

For a typical .NET application, you might have entries such as:

bin/
obj/
.git/
.vs/
*.user
*.suo

The exact contents should depend on your project, but the principle is simple: Docker only needs the files required to build the application.

Keeping the build context clean can make Docker builds more efficient and avoids accidentally sending unnecessary files to the Docker daemon or build environment.

A Practical Production Structure

For a typical ASP.NET Core application, I generally think of the Dockerfile as having three main responsibilities.

The first responsibility is to provide an environment capable of building the application.

The second is to produce the published application files.

The third is to provide a clean runtime environment in which those files can execute.

A simplified structure therefore looks like this:

Build
  ↓
Restore NuGet packages
  ↓
Compile application
  ↓
Publish application
  ↓
Runtime image
  ↓
Copy published files
  ↓
Run application

This separation makes the Dockerfile easier to reason about and provides a clear distinction between development requirements and production requirements.

When Should You Use Multi-Stage Builds?

For most containerised .NET applications, multi-stage builds are a sensible default.

They are particularly useful for ASP.NET Core applications, APIs, background services and other applications where the application can be compiled and published before being deployed.

They are also useful when you want to include testing or other intermediate processes within the same Docker build workflow.

The main idea is not simply to make a Dockerfile more complicated. It is to make the final container more focused.

Your development environment might need the compiler, SDK, debugging tools and source code. Your production application generally does not.

Conclusion

Multi-stage Docker builds are a simple but powerful feature that can significantly improve the way .NET applications are packaged.

The key concept is separating the environment used to build the application from the environment used to run it. The .NET SDK can remain in the build stage while the final image uses an appropriate .NET runtime image containing only the published application and the components required to run it.

Once you become comfortable with FROM, named stages and COPY --from, multi-stage Dockerfiles become much easier to understand.

For anyone developing modern .NET applications and deploying them using containers, multi-stage builds are well worth understanding. They provide a cleaner separation between development and production and can help produce smaller, more focused container images.

Share:

Become a member

Get the latest news right in your inbox. It's free and you can unsubscribe at any time. We hate spam as much as we do, so we never spam!

Read next

Deploying a .NET App with Docker Compose

Docker makes it easy to package a .NET application and its dependencies into a consistent, portable environment. Docker Compose takes this a step further by allowing you to define and run your application and supporting services from a single configuration file. In this article, we’ll look at how to deploy a .NET application using Docker Compose.

Admin 06-Oct-2026

The Bedroom Coder — retro computers, modern .NET, and late-night experiments.

Navigation

Contact

Want to talk retro tech or modern coding? I'd love to hear your thoughts.

© 2026 The Bedroom Coder. All rights reserved.