Roadmap › Docker & Containers › Module: Core Concepts
Lessons in this course (8)

3. Multi-stage Dockerfile for ASP.NET Core

Intermediate★ Must knowLesson 3 of 8
dockerfile
# syntax=docker/dockerfile:1
# ---- build stage ----
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["src/Shop.Api/Shop.Api.csproj", "src/Shop.Api/"]
COPY ["src/Shop.Core/Shop.Core.csproj", "src/Shop.Core/"]
RUN dotnet restore "src/Shop.Api/Shop.Api.csproj"          # cached until a .csproj changes
COPY . .
RUN dotnet publish "src/Shop.Api/Shop.Api.csproj" -c Release -o /app/publish /p:UseAppHost=false

# ---- runtime stage ----
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/publish .
USER app                                                     # non-root user built into .NET 8+ images
EXPOSE 8080
ENV ASPNETCORE_HTTP_PORTS=8080
ENTRYPOINT ["dotnet", "Shop.Api.dll"]

Health checks: the aspnet images don't include curl/wget. In Kubernetes or Container Apps, use HTTP probes against app.MapHealthChecks("/health"). For a plain Docker HEALTHCHECK, install a small tool in the final stage, or ship a tiny health-check executable.

Why multi-stage: the final image contains only the runtime + published output (~100–200 MB) instead of the full SDK (~800 MB+), so it is smaller, faster to pull, and has a smaller attack surface. Copy .csproj files and restore before copying the source to maximise layer caching.

.dockerignore (like .gitignore for the build context): bin/, obj/, .git, .vs, node_modules, *.user, secrets.

Alternative without a Dockerfile:

bash
dotnet publish -t:PublishContainer -p:ContainerRepository=shop-api -p:ContainerImageTag=1.0.0

Even smaller images: -chiseled Ubuntu variants (distroless-style, no shell or package manager), Alpine variants, or Native AOT on runtime-deps.