关于 dotnet-containers
面向 .NET 应用容器化的实践指南,涵盖 Dockerfile 和 dotnet publish 构建镜像、镜像与层选择、非 root 运行及健康检查。
主要功能
- 01多阶段 Dockerfile 模式
- 02通过 dotnet publish 构建容器镜像
- 03镜像选择与层缓存
- 04非 root 容器配置
- 05容器健康检查指导
适用场景
- 01将 .NET API 打包为容器
- 02利用层缓存减少重复构建
- 03以非 root 权限运行 .NET 容器
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill dotnet-containers安装后在 Claude Code 中输入/dotnet-containers确认已加载。
详细介绍
dotnet-containers
.NET 应用容器化最佳实践。涵盖多阶段 Dockerfile 模式、dotnet publish 容器映像功能(.NET 8+)、无根容器配置、优化层缓存以及容器运行状况检查。
不在本指南范围内: DI 容器机制和服务生命周期——请参阅 [skill:dotnet-csharp-dependency-injection]。Kubernetes 部署清单和 Docker Compose 编排,请参阅 [skill:dotnet-container-deployment]。构建和推送映像的 CI/CD 管道集成,请参阅 [skill:dotnet-gha-publish] 和 [skill:dotnet-ado-publish]。容器化应用测试,请参阅 [skill:dotnet-integration-testing] 了解 Testcontainers 模式。
交叉引用:[skill:dotnet-observability] 介绍运行状况检查模式;[skill:dotnet-container-deployment] 介绍将容器部署到 Kubernetes 以及使用 Compose 进行本地开发;[skill:dotnet-artifacts-output] 介绍使用集中式生成输出布局时如何调整 Dockerfile 路径。
多阶段 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"]
层缓存策略
按更改频率从低到高排列 COPY 指令:
- 项目文件和 props —— 仅在依赖项更改时更新
dotnet restore—— 在项目文件更改前一直使用缓存- 源代码 —— 每次构建都会更改
dotnet publish—— 仅在源代码或还原层更改时运行
# 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
解决方案级还原
对于多项目解决方案,复制所有 .csproj 文件和解决方案文件,以便执行一次还原:
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 容器映像(.NET 8+)
从 .NET 8 开始,dotnet publish 可以直接生成 OCI 容器映像,无需 Dockerfile。此功能使用 Microsoft.NET.Build.Containers SDK(包含在 .NET SDK 中)。
基本用法
# 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 配置
在 .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>
高级配置
<PropertyGroup>
<ContainerBaseImage>mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled</ContainerBaseImage>
<ContainerUser>app</ContainerUser>
</PropertyGroup>
<ItemGroup>
<ContainerEnvironmentVariable Include="ASPNETCORE_URLS" Value="http://+:8080" />
<ContainerEnvironmentVariable Include="DOTNET_RUNNING_IN_CONTAINER" Value="true" />
<ContainerLabel Include="org.opencontainers.image.source" Value="https://github.com/myorg/myapi" />
</ItemGroup>
何时使用 dotnet publish,何时使用 Dockerfile
| 场景 | 建议 |
|---|---|
| 简单的单项目 API | dotnet publish /t:PublishContainer —— 样板代码更少 |
| 含本机依赖项的多阶段构建 | Dockerfile —— 可完全控制构建环境 |
需要安装 OS 软件包(例如 libgdiplus) | Dockerfile —— SDK 发布不支持 RUN apt-get install |
| 复杂构建步骤的 CI/CD | Dockerfile —— 明确且可复现 |
| 快速进行本地容器测试 | dotnet publish /t:PublishContainer —— 迭代速度最快 |
选择基础映像
官方 .NET 容器映像
| 映像 | 用途 | 大小 |
|---|---|---|
mcr.microsoft.com/dotnet/aspnet:10.0 | ASP.NET Core 应用(Ubuntu) | ~220 MB |
mcr.microsoft.com/dotnet/aspnet:10.0-alpine | ASP.NET Core 应用(Alpine,更小) | ~110 MB |
mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled | Distroless(无 shell、无软件包管理器) | ~110 MB |
mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extra | Chiseled + 全球化支持 + 时区 | ~130 MB |
mcr.microsoft.com/dotnet/runtime:10.0 | 控制台应用、工作进程服务 | ~190 MB |
mcr.microsoft.com/dotnet/runtime-deps:10.0 | 自包含/AOT 应用(无需运行时) | ~30 MB |
选择基础映像
- 默认选择: Web 应用使用
aspnet,工作进程服务使用runtime - 最小占用: 使用
chiseled变体(无 shell、无 root 用户、无软件包管理器) - 需要全球化支持: 如果应用使用特定区域性的格式设置或时区,请使用
chiseled-extra - 自包含或 AOT: 使用
runtime-deps——运行时已包含在应用中 - Alpine: 比 Ubuntu 更小,但使用 musl libc;请测试与本机依赖项的兼容性
无根容器
以非 root 身份运行容器可缩小攻击面。.NET 8+ chiseled 镜像默认以非 root 身份运行。
使用标准镜像以非 root 身份运行
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"]
使用 Chiseled 镜像以非 root 身份运行
Chiseled 镜像预先配置了 app 用户(UID 1654)。无需其他配置:
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"]
端口配置
非 root 用户无法绑定到 1024 以下的端口。ASP.NET Core 在容器中默认使用 8080 端口(通过 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
容器运行状况检查
运行状况检查可让容器运行时监控应用是否就绪。应用级运行状况检查端点(参见 [skill:dotnet-observability])由 Docker 和 Kubernetes 探针调用。
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"]
对于 Chiseled 镜像(不含 curl),请使用专用的运行状况检查二进制文件,或依赖编排器级探针(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"]
运行状况检查端点
在应用中注册运行状况检查端点(完整指南请参见 [skill:dotnet-observability]):
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")
});
容器优化
.dockerignore
始终包含 .dockerignore,以从构建上下文中排除不必要的文件:
**/.git
**/.vs
**/.vscode
**/bin
**/obj
**/node_modules
**/*.user
**/*.suo
**/Dockerfile*
**/docker-compose*
**/.dockerignore
**/README.md
**/LICENSE
全球化和时区
如果应用需要全球化支持(特定区域性格式、时区),请配置 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
内存限制
配置 .NET,使其遵守容器内存限制:
# .NET automatically detects container memory limits and adjusts GC heap size.
# Override only if needed:
ENV DOTNET_GCHeapHardLimit=0x10000000 # 256 MB hard limit
.NET 会自动读取 cgroup 内存限制。GC 会调整堆大小,使其保持在容器内存预算范围内。除非有特定原因,否则请避免设置 DOTNET_GCHeapHardLimit。
ReadOnlyRootFilesystem
为纵深防御,请使用只读根文件系统运行。确保临时文件路径可写:
ENV DOTNET_EnableDiagnostics=0
# Or mount a tmpfs at /tmp for diagnostics support
关键原则
- 使用多阶段构建 -- 避免将构建工具带入最终镜像
- 合理安排 COPY 顺序以利用层缓存 -- 先复制项目文件并还原,再复制源代码
- 生产环境优先使用 chiseled 镜像 -- 不含 shell、无需 root、攻击面最小
- 简单项目使用
dotnet publish /t:PublishContainer-- 无需编写冗长的 Dockerfile - 以非 root 身份运行 -- 使用
USER指令或 chiseled 镜像(默认非 root) - 设置运行状况检查端点 -- 让编排器监控应用状态(参见 [skill:dotnet-observability])
- 包含
.dockerignore-- 缩小构建上下文并排除机密信息
Agent 注意事项
- 不要将
mcr.microsoft.com/dotnet/sdk用作最终镜像 -- SDK 镜像大小超过 800 MB,且包含构建工具。最终阶段始终使用aspnet、runtime或runtime-deps。 - 不要将镜像标签硬编码为补丁版本(例如
10.0.1)-- 使用10.0以接收安全补丁。只有在有特定兼容性要求时,才锁定到补丁版本。 - 不要在 chiseled 镜像中使用
HEALTHCHECK-- chiseled 镜像不含curl或 shell。请改用编排器级探针(KuberneteshttpGet、Docker Composetest)。 - 单独执行
dotnet restore后,不要忘记在dotnet publish上添加--no-restore-- 否则会再次执行还原,破坏层缓存。 - 不要在非 root 容器中绑定到 1024 以下的端口 -- .NET 在容器镜像中默认使用 8080 端口。如果覆盖
ASPNETCORE_HTTP_PORTS,请确保端口 >= 1024。 - 不要省略
.dockerignore-- 否则构建上下文会包含.git、bin/obj,以及可能存在的机密信息,从而增加构建时间和镜像大小。