Claude
Skills
Sign in
Back

dotnet-containers

Included with Lifetime
$97 forever

Containerizing .NET apps. Multi-stage Dockerfiles, SDK container publish (.NET 8+), rootless.

Backend & APIs

What this skill does


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

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

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

```dockerfile
# 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:

1. **Project files and props** -- change only when dependencies change
2. **`dotnet restore`** -- cached until project files change
3. **Source code** -- changes with every build
4. **`dotnet publish`** -- runs only when source or restore layer changes

```dockerfile
# 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:

```dockerfile
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

```bash
# 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`:

```xml
<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

```xml
<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

```dockerfile
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:

```dockerfile
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`):

```dockerfile
# 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

```dockerfile
FRO

Related in Backend & APIs