Simplifying .NET Builds with Docker Multi-Stage Builds

Learn the power of Docker multi-stage builds to create smaller, more secure, and more efficient container images for your .NET applications. A crucial best practice for modern containerization.

When you first start containerizing a .NET application, a common approach is to create a single Dockerfile that does everything: it installs the .NET SDK, restores dependencies, builds your application, and then sets the entry point. While this works, it leads to a major problem: a massive final container image.

Your production container image ends up containing the entire .NET SDK, your application's source code, and other build artifacts that are completely unnecessary for simply running the application. This makes your image larger, less secure, and slower to deploy.

The solution to this is Docker multi-stage builds.

What is a Multi-Stage Build?

A multi-stage build is a feature of Docker that allows you to use multiple FROM instructions in a single Dockerfile. Each FROM instruction begins a new "stage" of the build. You can then selectively copy artifacts from one stage to another, discarding everything you don't need in the final stage.

This allows you to use a large, tool-rich image (like the .NET SDK image) for building and compiling your application, and then copy only the final, published binaries into a small, clean, and secure runtime image.

A Standard Multi-Stage Dockerfile for .NET

Let's look at a typical multi-stage Dockerfile for an ASP.NET Core application in 2023.

# Stage 1: The Build Stage
# We use the full .NET 7 SDK image, which has all the tools we need.
FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build
WORKDIR /src

# Copy the project file and restore dependencies first.
# This is a Docker caching optimization.
COPY ["MyWebApp.csproj", "."]
RUN dotnet restore "MyWebApp.csproj"

# Copy the rest of the application's source code.
COPY . .

# Build and publish the application in Release configuration.
RUN dotnet publish "MyWebApp.csproj" -c Release -o /app/publish

# --- End of the first stage ---

# Stage 2: The Final Stage
# We start from a new, much smaller base image: the ASP.NET runtime image.
FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS final
WORKDIR /app

# Copy the published output from the 'build' stage into this new stage.
COPY --from=build /app/publish .

# Define the command to run the application.
ENTRYPOINT ["dotnet", "MyWebApp.dll"]

Let's break down the magic:

  • FROM ... AS build: The first FROM instruction starts our first stage and we give it a name, build. This stage uses the full SDK image.
  • RUN dotnet publish ...: This command compiles our application and places the output—the minimal set of DLLs and files needed to run the app—into the /app/publish directory inside this stage.
  • FROM ... AS final: This starts a completely new, clean stage. We use the much smaller aspnet runtime image, which doesn't contain the SDK.
  • COPY --from=build ...: This is the key instruction. It tells Docker to copy the contents of the /app/publish directory from the build stage into the current directory of our new final stage.

The Benefits

  1. Smaller Image Size: The final image is dramatically smaller. Instead of being over 1 GB (with the SDK), the final runtime image is often only around 200 MB. This means faster push/pull times from your container registry and faster startup times.

  2. Improved Security: The final image has a much smaller attack surface. It doesn't contain the .NET SDK, your source code, or any other build tools that a potential attacker could exploit.

  3. Simpler Build Process: You don't need complex shell scripts to manage the build and cleanup process. The entire logic is self-contained and declarative within a single Dockerfile.

  4. Better Caching: By copying the .csproj file and running dotnet restore first, you take advantage of Docker's layer caching. If you only change your application code without changing your dependencies, Docker can reuse the cached layer from the dotnet restore step, making subsequent builds much faster.

Conclusion

Docker multi-stage builds are not just a neat trick; they are the standard, essential best practice for containerizing .NET applications. They solve the critical problem of separating the build environment from the runtime environment, leading to container images that are smaller, more secure, and more efficient. If you are putting .NET in a container, you should be using a multi-stage build.