Lessons in this course (8)
3. Multi-stage Dockerfile for ASP.NET Core
# 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:
dotnet publish -t:PublishContainer -p:ContainerRepository=shop-api -p:ContainerImageTag=1.0.0Even smaller images: -chiseled Ubuntu variants (distroless-style, no shell or package manager), Alpine variants, or Native AOT on runtime-deps.