dotnet-containers

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.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "dotnet-containers" with this command: npx skills add novotnyllc/dotnet-artisan/novotnyllc-dotnet-artisan-dotnet-containers

dotnet-containers

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.

Scope

  • Multi-stage Dockerfile patterns for .NET

  • SDK container publish (dotnet publish /t:PublishContainer )

  • Rootless container configuration and security

  • Optimized layer caching and base image selection

  • Container health checks

Out of scope

  • DI container mechanics and service lifetimes -- see [skill:dotnet-csharp-dependency-injection]

  • Kubernetes deployment manifests and Docker Compose -- see [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]

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.

Multi-Stage Dockerfiles

Multi-stage builds separate the build environment from the runtime environment, producing minimal final images.

Standard Multi-Stage Pattern

Stage 1: Build

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src

Copy project files first for layer caching

COPY ["src/MyApi/MyApi.csproj", "src/MyApi/"] COPY ["src/MyApi.Core/MyApi.Core.csproj", "src/MyApi.Core/"] COPY ["Directory.Build.props", "."] COPY ["Directory.Packages.props", "."] RUN dotnet restore "src/MyApi/MyApi.csproj"

Copy everything else and build

COPY . . WORKDIR "/src/src/MyApi" RUN dotnet publish -c Release -o /app/publish --no-restore

Stage 2: Runtime

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app EXPOSE 8080

COPY --from=build /app/publish . ENTRYPOINT ["dotnet", "MyApi.dll"]

Layer Caching Strategy

Order COPY instructions from least-frequently-changed to most-frequently-changed:

  • Project files and props -- change only when dependencies change

  • dotnet restore -- cached until project files change

  • Source code -- changes with every build

  • dotnet publish -- runs only when source or restore layer changes

Good: restore layer is cached when only source changes

COPY ["src/MyApi/MyApi.csproj", "src/MyApi/"] RUN dotnet restore COPY . . RUN dotnet publish

Bad: restore runs on every source change

COPY . . RUN dotnet restore RUN dotnet publish

Solution-Level Restore

For multi-project solutions, copy all .csproj files and the solution file to enable a single restore:

FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src

Copy solution and all project files for restore caching

COPY ["MyApp.sln", "."] COPY ["Directory.Build.props", "."] COPY ["Directory.Packages.props", "."] COPY ["src/MyApi/MyApi.csproj", "src/MyApi/"] COPY ["src/MyApi.Core/MyApi.Core.csproj", "src/MyApi.Core/"] COPY ["src/MyApi.Infrastructure/MyApi.Infrastructure.csproj", "src/MyApi.Infrastructure/"] RUN dotnet restore

COPY . . RUN dotnet publish "src/MyApi/MyApi.csproj" -c Release -o /app/publish --no-restore

dotnet publish Container Images (.NET 8+)

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).

Basic Usage

Publish as a container image to local Docker daemon

dotnet publish --os linux --arch x64 /t:PublishContainer

Publish to a remote registry

dotnet publish --os linux --arch x64 /t:PublishContainer
-p:ContainerRegistry=ghcr.io
-p:ContainerRepository=myorg/myapi

MSBuild Configuration

Configure container properties in the .csproj :

<PropertyGroup> <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:10.0</ContainerBaseImage> <ContainerImageName>myapi</ContainerImageName> <ContainerImageTag>$(Version)</ContainerImageTag> </PropertyGroup>

<ItemGroup> <ContainerPort Include="8080" Type="tcp" /> </ItemGroup>

Advanced Configuration

<PropertyGroup> <!-- Use chiseled (distroless) base image for smaller attack surface --> <ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled</ContainerBaseImage>

<!-- Run as non-root user (default for chiseled images) --> <ContainerUser>app</ContainerUser> </PropertyGroup>

<ItemGroup> <!-- Environment variables --> <ContainerEnvironmentVariable Include="ASPNETCORE_URLS" Value="http://+:8080" /> <ContainerEnvironmentVariable Include="DOTNET_RUNNING_IN_CONTAINER" Value="true" />

<!-- Labels --> <ContainerLabel Include="org.opencontainers.image.source" Value="https://github.com/myorg/myapi" /> </ItemGroup>

When to Use dotnet publish vs Dockerfile

Scenario Recommendation

Simple single-project API dotnet publish /t:PublishContainer -- less boilerplate

Multi-stage build with native dependencies Dockerfile -- full control over build environment

Need to install OS packages (e.g., libgdiplus ) Dockerfile -- RUN apt-get install not available in SDK publish

CI/CD with complex build steps Dockerfile -- explicit, reproducible

Quick local container testing dotnet publish /t:PublishContainer -- fastest iteration

Base Image Selection

Official .NET Container Images

Image Use Case Size

mcr.microsoft.com/dotnet/aspnet:10.0

ASP.NET Core apps (Ubuntu) ~220 MB

mcr.microsoft.com/dotnet/aspnet:10.0-alpine

ASP.NET Core apps (Alpine, smaller) ~110 MB

mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled

Distroless (no shell, no package manager) ~110 MB

mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra

Chiseled + globalization + time zones ~130 MB

mcr.microsoft.com/dotnet/runtime:10.0

Console apps, worker services ~190 MB

mcr.microsoft.com/dotnet/runtime-deps:10.0

Self-contained/AOT apps (runtime not needed) ~30 MB

Choosing a Base Image

  • Default: Use aspnet for web apps, runtime for worker services

  • Minimal footprint: Use chiseled variants (no shell, no root user, no package manager)

  • Globalization needed: Use chiseled-extra if your app uses culture-specific formatting or time zones

  • Self-contained or AOT: Use runtime-deps -- the runtime is bundled in your app

  • Alpine: Smaller than Ubuntu but uses musl libc; test for compatibility with native dependencies

Rootless Containers

Running containers as non-root reduces the attack surface. .NET 8+ chiseled images run as non-root by default.

Non-Root with Standard Images

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app

Create non-root user and switch to it

RUN adduser --disabled-password --gecos "" --uid 1001 appuser USER appuser

COPY --from=build --chown=appuser:appuser /app/publish . ENTRYPOINT ["dotnet", "MyApi.dll"]

Non-Root with Chiseled Images

Chiseled images include a pre-configured app user (UID 1654). No additional configuration needed:

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS runtime WORKDIR /app

Already runs as non-root 'app' user (UID 1654)

COPY --from=build /app/publish . ENTRYPOINT ["dotnet", "MyApi.dll"]

Port Configuration

Non-root users cannot bind to ports below 1024. ASP.NET Core defaults to port 8080 in containers (set via ASPNETCORE_HTTP_PORTS ):

Default in .NET 8+ container images -- no explicit config needed

ASPNETCORE_HTTP_PORTS=8080

If you need a different port:

ENV ASPNETCORE_HTTP_PORTS=5000 EXPOSE 5000

Container Health Checks

Health checks allow container runtimes to monitor application readiness. The application-level health check endpoints (see [skill:dotnet-observability]) are consumed by Docker and Kubernetes probes.

Docker HEALTHCHECK

FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime WORKDIR /app

Health check using curl (not available in chiseled images)

HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3
CMD curl -f http://localhost:8080/health/live || exit 1

COPY --from=build /app/publish . ENTRYPOINT ["dotnet", "MyApi.dll"]

For chiseled images (no curl ), use a dedicated health check binary or rely on orchestrator-level probes (Kubernetes httpGet , Docker Compose test ):

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled AS runtime WORKDIR /app

No HEALTHCHECK directive -- use orchestrator probes instead

See [skill:dotnet-container-deployment] for Kubernetes probe configuration

COPY --from=build /app/publish . ENTRYPOINT ["dotnet", "MyApi.dll"]

Health Check Endpoints

Register health check endpoints in your application (see [skill:dotnet-observability] for full guidance):

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks() .AddCheck("self", () => HealthCheckResult.Healthy(), tags: ["live"]) .AddNpgSql( builder.Configuration.GetConnectionString("DefaultConnection")!, name: "database", tags: ["ready"]);

var app = builder.Build();

app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = check => check.Tags.Contains("live") });

app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") });

Container Optimization

.dockerignore

Always include a .dockerignore to exclude unnecessary files from the build context:

**/.git **/.vs **/.vscode **/bin **/obj **/node_modules **/.user **/.suo */Dockerfile */docker-compose **/.dockerignore **/README.md **/LICENSE

Globalization and Time Zones

If your app needs globalization support (culture-specific formatting, time zones), configure ICU:

Option 1: Use the chiseled-extra image (includes ICU + tzdata)

FROM mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra

Option 2: Disable globalization for smaller images (if not needed)

ENV DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=true

Memory Limits

Configure .NET to respect container memory limits:

.NET automatically detects container memory limits and adjusts GC heap size.

Override only if needed:

ENV DOTNET_GCHeapHardLimit=0x10000000 # 256 MB hard limit

.NET automatically reads cgroup memory limits. The GC adjusts its heap size to stay within the container memory budget. Avoid setting DOTNET_GCHeapHardLimit unless you have a specific reason.

ReadOnlyRootFilesystem

For defense-in-depth, run with a read-only root filesystem. Ensure writable paths for temp files:

ENV DOTNET_EnableDiagnostics=0

Or mount a tmpfs at /tmp for diagnostics support

Key Principles

  • Use multi-stage builds -- keep build tools out of the final image

  • Order COPY for layer caching -- project files and restore before source code

  • Prefer chiseled images for production -- no shell, no root, minimal attack surface

  • Use dotnet publish /t:PublishContainer for simple projects -- skip Dockerfile boilerplate

  • Run as non-root -- use USER directive or chiseled images (non-root by default)

  • Set health check endpoints -- enable orchestrators to monitor application state (see [skill:dotnet-observability])

  • Include .dockerignore -- keep build context small and exclude secrets

Agent Gotchas

  • Do not use mcr.microsoft.com/dotnet/sdk as the final image -- SDK images are 800+ MB and include build tools. Always use aspnet , runtime , or runtime-deps for the final stage.

  • Do not hardcode image tags to a patch version (e.g., 10.0.1 ) -- use 10.0 to receive security patches. Pin to patch versions only if you have a specific compatibility requirement.

  • Do not use HEALTHCHECK with chiseled images -- chiseled images have no curl or shell. Use orchestrator-level probes (Kubernetes httpGet , Docker Compose test ) instead.

  • Do not forget --no-restore on dotnet publish after a separate dotnet restore step -- without it, restore runs again and breaks layer caching.

  • Do not bind to ports below 1024 in non-root containers -- .NET defaults to port 8080 in container images. If you override ASPNETCORE_HTTP_PORTS , ensure the port is >= 1024.

  • Do not omit .dockerignore -- without it, the build context includes .git , bin/obj , and potentially secrets, increasing build time and image size.

References

  • .NET container images

  • Containerize a .NET app with dotnet publish

  • .NET container image variants

  • Chiseled Ubuntu containers for .NET

  • ASP.NET Core health checks

  • Docker multi-stage builds

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

dotnet-ui

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-csharp

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-api

No summary provided by upstream source.

Repository SourceNeeds Review
General

dotnet-advisor

No summary provided by upstream source.

Repository SourceNeeds Review