Multi-Stage Docker Builds
Multi-stage Docker builds produce tiny, secure production images: the SDK stage (1.2GB) compiles the app; the final runtime stage (200MB) contains only the compiled binaries — SDK tools, source code, and build artifacts are never in production. The result is a faster image pull, smaller attack surface, and faster container startup.
Production-Optimized Dockerfile
# ━━ Multi-stage Dockerfile ━━
# Stage 1: Restore NuGet packages (cached layer — only re-runs when .csproj changes)
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS restore
WORKDIR /app
# Copy only .csproj files first — maximizes layer cache
COPY ["src/MyApp.Api/MyApp.Api.csproj", "src/MyApp.Api/"]
COPY ["src/MyApp.Application/MyApp.Application.csproj", "src/MyApp.Application/"]
COPY ["src/MyApp.Domain/MyApp.Domain.csproj", "src/MyApp.Domain/"]
COPY ["src/MyApp.Infrastructure/MyApp.Infrastructure.csproj", "src/MyApp.Infrastructure/"]
RUN dotnet restore "src/MyApp.Api/MyApp.Api.csproj" \
--runtime linux-musl-x64 \ # target Alpine Linux (musl) for smaller image
--no-cache
# Stage 2: Build
FROM restore AS build
COPY . .
RUN dotnet build "src/MyApp.Api/MyApp.Api.csproj" \
--configuration Release \
--no-restore
# Stage 3: Publish (optimized — ReadyToRun, trim unused IL)
FROM build AS publish
RUN dotnet publish "src/MyApp.Api/MyApp.Api.csproj" \
--configuration Release \
--runtime linux-musl-x64 \
--self-contained true \ # includes dotnet runtime → no runtime image needed
--output /app/publish \
-p:PublishSingleFile=false \
-p:PublishReadyToRun=true \ # AOT-precompile hot paths → faster cold start
-p:EnableCompressionInSingleFile=true
# Stage 4: Final runtime image (Alpine — 5MB base, no SDK, no shell)
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS final
WORKDIR /app
# Security: run as non-root user
RUN addgroup --system -g 1001 appgroup && \
adduser --system -u 1001 --ingroup appgroup appuser
USER appuser
# Health check — Docker restarts container if unhealthy
HEALTHCHECK --interval=30s --timeout=10s --start-period=30s --retries=3 \
CMD curl -f http://localhost:8080/health || exit 1
EXPOSE 8080
ENV ASPNETCORE_URLS=http://+:8080
ENV DOTNET_RUNNING_IN_CONTAINER=true
COPY --from=publish --chown=appuser:appgroup /app/publish .
ENTRYPOINT ["./MyApp.Api"]
# ━━ .dockerignore — exclude from build context ━━
# **/bin/
# **/obj/
# **/*.user
# **/secrets*.json
# .git/
# .github/
# tests/
# README.md
# docker-compose*.ymlTip
Tip
Practice MultiStage Docker Builds in small, isolated examples before integrating into larger projects. Breaking concepts into small experiments builds genuine understanding faster than reading alone.
Alpine + multi-stage + non-root = production-ready.
Practice Task
Note
Practice Task — (1) Write a working example of MultiStage Docker Builds from scratch without looking at notes. (2) Modify it to handle an edge case (empty input, null value, or error state). (3) Share your solution in the Priygop community for feedback.
Quick Quiz
Common Mistake
Warning
A common mistake with MultiStage Docker Builds is skipping edge case testing — empty inputs, null values, and unexpected data types. Always validate boundary conditions to write robust, production-ready dotnet code.