In this lesson, you will learn how to leverage multi-stage builds to produce minimal, security-hardened Docker images that contain only the necessary runtime assets for your .NET applications.
The Problem with Single-Stage Builds
When you build a standard .NET image, you often include the entire .NET SDK, build tools, source code, and intermediate artifacts. This results in bloated images (often over 800MB) that increase your attack surface by including unnecessary compilers and source files, and significantly slow down your CI/CD deployment pipelines.
The Multi-Stage Solution
Multi-stage builds allow you to use a "build" container to compile your code and a separate, minimal "runtime" container to execute it. By separating these concerns, you discard the heavy SDK and source code at the end of the process.
Consider this optimized Dockerfile structure:
# Stage 1: Build the application
FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
WORKDIR /app
COPY . .
RUN dotnet publish -c Release -o /app/out --no-self-contained
# Stage 2: Serve the application
FROM mcr.microsoft.com/dotnet/aspnet:8.0 AS runtime
WORKDIR /app
# Only copy the published output
COPY --from=build /app/out .
# Run as a non-privileged user for security hardening
USER app
ENTRYPOINT ["dotnet", "YourApp.dll"]
Security and Optimization Principles
- Use Distroless or Alpine: For the runtime stage, use the
aspnetbase image, which is optimized for hosting. To further harden the image, consider usingchiseledUbuntu images, which remove the shell and package manager entirely, leaving zero room for attackers to execute unauthorized commands. - Layer Caching: Always copy your
.csprojfiles and rundotnet restorebefore copying the rest of your source code. This ensures that Docker caches your dependencies, preventing a full re-restore unless the project file itself changes. - Non-Root Execution: Always explicitly set the
USERdirective to a non-privileged user. This prevents potential vulnerabilities in your application from escalating into host-level compromises.
By discarding the build environment and shipping only the compiled binaries, you drastically reduce the image footprint and tighten your application’s security perimeter.
Key Takeaway: Use multi-stage builds to strictly separate your build-time dependencies from the runtime environment, ensuring your final image is small, non-privileged, and contains only the binaries required to execute your code.