$npx -y skills add wshaddix/dotnet-skills --skill dotnet-containersContainerizing .NET apps. Multi-stage Dockerfiles, SDK container publish (.NET 8+), rootless.
| 1 | # dotnet-containers |
| 2 | |
| 3 | Best practices for containerizing .NET applications. Covers multi-stage Dockerfile patterns, the `dotnet publish` container image feature (.NET 8+), rootless container configuration, optimized layer caching, and container health checks. |
| 4 | |
| 5 | **Out of scope:** DI container mechanics and service lifetimes -- see [skill:dotnet-csharp-dependency-injection]. Kubernetes deployment manifests and Docker Compose orchestration are covered in [skill:dotnet-container-deployment]. CI/CD pipeline integration for building and pushing images -- see [skill:dotnet-gha-publish] and [skill:dotnet-ado-publish]. Testing containerized applications -- see [skill:dotnet-integration-testing] for Testcontainers patterns. |
| 6 | |
| 7 | Cross-references: [skill:dotnet-observability] for health check patterns, [skill:dotnet-container-deployment] for deploying containers to Kubernetes and local dev with Compose, [skill:dotnet-artifacts-output] for Dockerfile path adjustments when using centralized build output layout. |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Multi-Stage Dockerfiles |
| 12 | |
| 13 | Multi-stage builds separate the build environment from the runtime environment, producing minimal final images. |
| 14 | |
| 15 | ### Standard Multi-Stage Pattern |
| 16 | |
| 17 | ```dockerfile |
| 18 | # Stage 1: Build |
| 19 | FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build |
| 20 | WORKDIR /src |
| 21 | |
| 22 | # Copy project files first for layer caching |
| 23 | COPY ["src/MyApi/MyApi.csproj", "src/MyApi/"] |
| 24 | COPY ["src/MyApi.Core/MyApi.Core.csproj", "src/MyApi.Core/"] |
| 25 | COPY ["Directory.Build.props", "."] |
| 26 | COPY ["Directory.Packages.props", "."] |
| 27 | RUN dotnet restore "src/MyApi/MyApi.csproj" |
| 28 | |
| 29 | # Copy everything else and build |
| 30 | COPY . . |
| 31 | WORKDIR "/src/src/MyApi" |
| 32 | RUN dotnet publish -c Release -o /app/publish --no-restore |
| 33 | |
| 34 | # Stage 2: Runtime |
| 35 | FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime |
| 36 | WORKDIR /app |
| 37 | EXPOSE 8080 |
| 38 | |
| 39 | COPY --from=build /app/publish . |
| 40 | ENTRYPOINT ["dotnet", "MyApi.dll"] |
| 41 | ``` |
| 42 | |
| 43 | ### Layer Caching Strategy |
| 44 | |
| 45 | Order COPY instructions from least-frequently-changed to most-frequently-changed: |
| 46 | |
| 47 | 1. **Project files and props** -- change only when dependencies change |
| 48 | 2. **`dotnet restore`** -- cached until project files change |
| 49 | 3. **Source code** -- changes with every build |
| 50 | 4. **`dotnet publish`** -- runs only when source or restore layer changes |
| 51 | |
| 52 | ```dockerfile |
| 53 | # Good: restore layer is cached when only source changes |
| 54 | COPY ["src/MyApi/MyApi.csproj", "src/MyApi/"] |
| 55 | RUN dotnet restore |
| 56 | COPY . . |
| 57 | RUN dotnet publish |
| 58 | |
| 59 | # Bad: restore runs on every source change |
| 60 | COPY . . |
| 61 | RUN dotnet restore |
| 62 | RUN dotnet publish |
| 63 | ``` |
| 64 | |
| 65 | ### Solution-Level Restore |
| 66 | |
| 67 | For multi-project solutions, copy all `.csproj` files and the solution file to enable a single restore: |
| 68 | |
| 69 | ```dockerfile |
| 70 | FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build |
| 71 | WORKDIR /src |
| 72 | |
| 73 | # Copy solution and all project files for restore caching |
| 74 | COPY ["MyApp.sln", "."] |
| 75 | COPY ["Directory.Build.props", "."] |
| 76 | COPY ["Directory.Packages.props", "."] |
| 77 | COPY ["src/MyApi/MyApi.csproj", "src/MyApi/"] |
| 78 | COPY ["src/MyApi.Core/MyApi.Core.csproj", "src/MyApi.Core/"] |
| 79 | COPY ["src/MyApi.Infrastructure/MyApi.Infrastructure.csproj", "src/MyApi.Infrastructure/"] |
| 80 | RUN dotnet restore |
| 81 | |
| 82 | COPY . . |
| 83 | RUN dotnet publish "src/MyApi/MyApi.csproj" -c Release -o /app/publish --no-restore |
| 84 | ``` |
| 85 | |
| 86 | --- |
| 87 | |
| 88 | ## dotnet publish Container Images (.NET 8+) |
| 89 | |
| 90 | Starting with .NET 8, `dotnet publish` can produce OCI container images directly without a Dockerfile. This uses the `Microsoft.NET.Build.Containers` SDK (included in the .NET SDK). |
| 91 | |
| 92 | ### Basic Usage |
| 93 | |
| 94 | ```bash |
| 95 | # Publish as a container image to local Docker daemon |
| 96 | dotnet publish --os linux --arch x64 /t:PublishContainer |
| 97 | |
| 98 | # Publish to a remote registry |
| 99 | dotnet publish --os linux --arch x64 /t:PublishContainer \ |
| 100 | -p:ContainerRegistry=ghcr.io \ |
| 101 | -p:ContainerRepository=myorg/myapi |
| 102 | ``` |
| 103 | |
| 104 | ### MSBuild Configuration |
| 105 | |
| 106 | Configure container properties in the `.csproj`: |
| 107 | |
| 108 | ```xml |
| 109 | <PropertyGroup> |
| 110 | <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:10.0</ContainerBaseImage> |
| 111 | <ContainerImageName>myapi</ContainerImageName> |
| 112 | <ContainerImageTag>$(Version)</ContainerImageTag> |
| 113 | </PropertyGroup> |
| 114 | |
| 115 | <ItemGroup> |
| 116 | <ContainerPort Include="8080" Type="tcp" /> |
| 117 | </ItemGroup> |
| 118 | ``` |
| 119 | |
| 120 | ### Advanced Configuration |
| 121 | |
| 122 | ```xml |
| 123 | <PropertyGroup> |
| 124 | <!-- Use chiseled (distroless) base image for smaller attack surface --> |
| 125 | <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled</ContainerBaseImage> |
| 126 | |
| 127 | <!-- Run as non-root user (default for chiseled images) --> |
| 128 | <ContainerUser>app</ContainerUser> |
| 129 | </PropertyGroup> |
| 130 | |
| 131 | <ItemGroup> |
| 132 | <!-- Environment variables --> |
| 133 | <ContainerEnvironmentVariable Include="ASPNETCORE_URLS" Value="http://+:8080" /> |
| 134 | <ContainerEnvironmentVariable Include="DOTNET_RUNNING_IN_CONTAINER" Value="true" /> |
| 135 | |
| 136 | <!-- Labels --> |
| 137 | <ContainerLabel Include="org.opencontainers.image.source" Value="https://github.com/myorg/myapi" /> |
| 138 | </ItemGroup> |
| 139 | ``` |
| 140 | |
| 141 | ### When to Use dotnet publish vs Dockerfile |
| 142 | |
| 143 | | Scenario | Recommendation | |
| 144 | |----------|--------- |