用途
当需要为 .NET 服务生成容器镜像、又不想手写 Dockerfile 时,可使用本技能。它适用于以下场景:
- 想要为 .NET 服务生成容器镜像,且不想手写 Dockerfile。
- 想了解在没有 Dockerfile 的情况下,如何为 SDK 构建的镜像打标签、命名或暴露端口。
- 需要把容器发布接入 CI/CD 或发布流水线。
- SDK 构建的镜像被推送到错误的仓库、使用了错误的标签或选错了基础镜像。
- 需要镜像的 tarball 用于安全扫描或离线(air-gapped)加载。
不适用场景:需要在镜像中执行 RUN 步骤或任意层定制。SDK 工具无法模拟 RUN,此时应改为编写 Dockerfile。
主要能力
自 .NET 7 起,dotnet publish 可原生构建容器镜像。Microsoft.NET.Build.Containers 工具随 SDK 一起提供,因此无需 Dockerfile,构建镜像也不需要安装 Docker;仅在本地运行镜像时才需要 Docker 或 Podman。
最快的方式是一条命令:
dotnet publish /t:PublishContainer -c Release
该命令一步完成应用编译并产出镜像。镜像名称默认取项目的 AssemblyName,可通过 ContainerRepository 覆盖(早期版本中名为 ContainerImageName)。
三种输出模式
工具以三种方式之一写出镜像,按镜像最终去向选择:
- 本地守护进程(默认):用于本地开发,推送到正在运行的 Docker 或 Podman 守护进程,无需额外配置。
- Tarball:需要一个文件用于扫描、传输或后续加载时使用。设置 ContainerArchiveOutputPath 为 .tar.gz 路径,然后用 docker load -i 或 podman load -i 加载。适用于安全扫描工作流。
- 推送镜像仓库:用于发布。设置 ContainerRegistry(例如 ghcr.io 或内部仓库),镜像会被直接推送。
关键事实
- 构建不需要 Docker。SDK 自行创建镜像,仅在运行镜像时才需要运行时。
- 镜像名称默认取 AssemblyName,可通过 ContainerRepository 覆盖。
- 基础镜像会被推断。自包含项目使用 mcr.microsoft.com/dotnet/runtime-deps,ASP.NET Core 使用 dotnet/aspnet,其他应用使用 dotnet/runtime,并按 TFM 打标签。自 SDK 8.0.200 起,推断会考虑体积与安全:musl RID 选择 Alpine 变体,PublishAot=true 选择 chiseled AOT runtime-deps 变体。
- 自 .NET 8 起,未设置标签时 SDK 会将镜像标记为 latest。如果发布依赖版本标签,必须显式设置 ContainerImageTag(单个标签)或 ContainerImageTags(列表)。这是 CI 中常见的陷阱。
- Windows 镜像需要显式指定基础镜像。Microsoft 不再在清单列表中包含 Windows 变体。要面向 Windows,需将 ContainerBaseImage 设为具体的 nanoserver 标签(例如 mcr.microsoft.com/dotnet/aspnet:8.0-nanoserver-ltsc2022)。
- 多架构镜像通过 ContainerRuntimeIdentifiers 生成(以分号分隔,是 RuntimeIdentifiers 的子集),输出为 OCI 镜像索引。自 SDK 8.0.405、9.0.102 和 9.0.2xx 起支持。
- 不安全仓库通过 DOTNET_CONTAINER_INSECURE_REGISTRIES 环境变量传入(逗号分隔),自 SDK 9.0.100 起支持。自 .NET 8.0.400 起,SDK 会读取标准 Docker / Podman 配置来决定使用 HTTP 还是 HTTPS。
使用方式
最小属性集
大多数服务只覆盖需要的属性,其余交给 SDK 推断。一个最小的 csproj 如下:
<Project Sdk="Microsoft.NET.Sdk.Web"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ContainerRepository>my-service</ContainerRepository> <ContainerImageTags>$(VersionPrefix);latest</ContainerImageTags> </PropertyGroup>
<ItemGroup> <ContainerPort Include="8080" Type="tcp" /> </ItemGroup> </Project>
说明:
- 没有 ContainerRegistry、ContainerBaseImage、ContainerUser。SDK 会为大多数 ASP.NET 服务正确设置这些默认值。
- ContainerPort 项可在没有 Dockerfile 的情况下暴露端口。
- 标签遵循 semver:包版本加一个浮动的 latest——但前提是 $(VersionPrefix) 在该文件被求值之前已设置(在命令行、Directory.Build.props 中或项目文件更早位置)。SDK 会在项目主体之后应用其 1.0.0 默认值,因此普通的本地发布会将 $(VersionPrefix) 解析为空,镜像只被标记为 latest。
CI/CD 发布
在 CI 中,针对仓库发布并使用发布名称打标签。标签驱动的模式如下:
dotnet publish src/MyService/MyService.csproj
-p:VersionPrefix=${{ github.ref_name }}
-p:ContainerRegistry=docker.example.internal
-c Release /t:PublishContainer
-p:VersionPrefix 会填充项目中设置的 ContainerImageTags 列表,因此镜像会同时带有发布名称和 latest 标签。不要同时传入 -p:ContainerImageTag——SDK 会拒绝同时使用 ContainerImageTag 和 ContainerImageTags(CONTAINER2008)。
如果要在整个仓库集中管理镜像设置,可将其放入带 CI 条件判断的 Directory.Build.props 中,例如版本标签和标签(labels)。
适用场景
- 为 .NET 服务快速生成容器镜像,无需维护 Dockerfile。
- 在 CI/CD 或发布流水线中自动构建并推送镜像。
- 生成镜像 tarball 用于安全扫描或离线环境加载。
- 需要多架构镜像或针对自包含 / AOT 项目优化基础镜像。
限制
- 无法执行 RUN 步骤或任意层定制;需要时应编写 Dockerfile。
- 默认输出模式需要本地 Docker / Podman 守护进程;无守护进程时需改用 ContainerArchiveOutputPath 输出 tarball。
- Windows 镜像需显式指定基础镜像。
- 未显式设置标签时,自 .NET 8 起镜像会被标记为 latest,可能导致版本标签缺失。
- 镜像名称默认取 AssemblyName,默认推送到本地守护进程,可能推送到非预期位置。
故障排查
常见故障包括标签漂移、镜像名或仓库错误、无法执行 RUN、构建主机无守护进程、不安全仓库被拒、多 RID 发布竞争以及多目标发布。要点:显式设置标签(ContainerImageTag 或 ContainerImageTags)、显式设置 ContainerRepository 和 ContainerRegistry,在没有守护进程时使用 ContainerArchiveOutputPath。
- 标签漂移:流水线发布了镜像但找不到预期版本,只有 latest。原因是自 .NET 8 起,未设置 ContainerImageTag 或 ContainerImageTags 时 SDK 将镜像标记为 latest 而非 $(Version)。修复:在发布步骤中显式设置标签。
- 镜像名或仓库错误:镜像被推送到意外位置。原因是 SDK 从 AssemblyName 推导仓库名并默认推送到本地守护进程。修复:显式设置 ContainerRepository 和 ContainerRegistry。
- RUN 不可用:需要在镜像内运行命令但 SDK 不支持。原因是 SDK 工具在没有 Dockerfile 时无法执行 RUN 步骤。修复:通过 ContainerBaseImage 使用自定义基础镜像,或编写 Dockerfile。
- 构建主机无守护进程:发布失败,因为无法访问 Docker / Podman 守护进程。原因是默认输出模式推送到本地守护进程。修复:设置 ContainerArchiveOutputPath 写出 tarball,无需守护进程。
自包含与 AOT
自包含项目默认使用 runtime-deps 基础镜像。AOT 项目会获得 chiseled AOT runtime-deps 变体,以生成更小、更安全的镜像。
验证
确认镜像已构建并存在:
本地守护进程中的镜像
docker images | grep <repository>
已生成 tarball
ls -la <ContainerArchiveOutputPath>
推送成功(仓库模式)
docker pull <ContainerRegistry>/<ContainerRepository>:<tag>
相关技能
- dotnet-skills:testcontainers - 在测试中运行容器
- dotnet-skills:aot-trimming - AOT 与裁剪,与基础镜像选择相关