dotnet-containers
npx skills add https://github.com/novotnyllc/dotnet-artisan --skill dotnet-containers
Agent 安装分布
Skill 文档
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 /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
aspnetfor web apps,runtimefor worker services - Minimal footprint: Use
chiseledvariants (no shell, no root user, no package manager) - Globalization needed: Use
chiseled-extraif 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 /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 /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 \
CMD curl -f http://localhost:8080/health/live || exit 1
COPY /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 /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:PublishContainerfor simple projects — skip Dockerfile boilerplate - Run as non-root — use
USERdirective 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/sdkas the final image — SDK images are 800+ MB and include build tools. Always useaspnet,runtime, orruntime-depsfor the final stage. - Do not hardcode image tags to a patch version (e.g.,
10.0.1) — use10.0to receive security patches. Pin to patch versions only if you have a specific compatibility requirement. - Do not use
HEALTHCHECKwith chiseled images — chiseled images have nocurlor shell. Use orchestrator-level probes (KuberneteshttpGet, Docker Composetest) instead. - Do not forget
--no-restoreondotnet publishafter a separatedotnet restorestep — 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.