mcpField
D
dotnet-containers

dotnet-containers

github.com/wshaddix云与 DevOps8687 次安装35 周前更新
代码仓库

关于 dotnet-containers

面向 .NET 应用容器化的实践指南,涵盖 Dockerfile 和 dotnet publish 构建镜像、镜像与层选择、非 root 运行及健康检查。

主要功能

  1. 01多阶段 Dockerfile 模式
  2. 02通过 dotnet publish 构建容器镜像
  3. 03镜像选择与层缓存
  4. 04非 root 容器配置
  5. 05容器健康检查指导

适用场景

  1. 01将 .NET API 打包为容器
  2. 02利用层缓存减少重复构建
  3. 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 指令:

  1. 项目文件和 props —— 仅在依赖项更改时更新
  2. dotnet restore —— 在项目文件更改前一直使用缓存
  3. 源代码 —— 每次构建都会更改
  4. 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
场景建议
简单的单项目 APIdotnet publish /t:PublishContainer —— 样板代码更少
含本机依赖项的多阶段构建Dockerfile —— 可完全控制构建环境
需要安装 OS 软件包(例如 libgdiplus)Dockerfile —— SDK 发布不支持 RUN apt-get install
复杂构建步骤的 CI/CDDockerfile —— 明确且可复现
快速进行本地容器测试dotnet publish /t:PublishContainer —— 迭代速度最快

选择基础映像

官方 .NET 容器映像
映像用途大小
mcr.microsoft.com/dotnet/aspnet:10.0ASP.NET Core 应用(Ubuntu)~220 MB
mcr.microsoft.com/dotnet/aspnet:10.0-alpineASP.NET Core 应用(Alpine,更小)~110 MB
mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseledDistroless(无 shell、无软件包管理器)~110 MB
mcr.microsoft.com/dotnet/aspnet:10.0-noble-chiseled-extraChiseled + 全球化支持 + 时区~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 注意事项

  1. 不要将 mcr.microsoft.com/dotnet/sdk 用作最终镜像 -- SDK 镜像大小超过 800 MB,且包含构建工具。最终阶段始终使用 aspnet、runtime 或 runtime-deps。
  2. 不要将镜像标签硬编码为补丁版本(例如 10.0.1)-- 使用 10.0 以接收安全补丁。只有在有特定兼容性要求时,才锁定到补丁版本。
  3. 不要在 chiseled 镜像中使用 HEALTHCHECK -- chiseled 镜像不含 curl 或 shell。请改用编排器级探针(Kubernetes httpGet、Docker Compose test)。
  4. 单独执行 dotnet restore 后,不要忘记在 dotnet publish 上添加 --no-restore -- 否则会再次执行还原,破坏层缓存。
  5. 不要在非 root 容器中绑定到 1024 以下的端口 -- .NET 在容器镜像中默认使用 8080 端口。如果覆盖 ASPNETCORE_HTTP_PORTS,请确保端口 >= 1024。
  6. 不要省略 .dockerignore -- 否则构建上下文会包含 .git、bin/obj,以及可能存在的机密信息,从而增加构建时间和镜像大小。

参考资料

评论

支持 Markdown