多阶段构建通过分离编译与运行环境保障C#源码安全并精简镜像:第一阶段用SDK镜像编译,第二阶段仅复制publish输出到轻量runtime镜像,禁用源码、调试符号及NuGet缓存,配合Trim和Native AOT可将镜像压至20–40MB且无IL与JIT。

用多阶段构建保护 C# 源码并精简镜像,核心在于把编译环境和运行环境彻底分开——源码、SDK、编译工具只留在构建阶段,最终镜像里只有可执行文件和运行时依赖。
明确划分构建与运行两个阶段
第一阶段用完整 .NET SDK 镜像编译项目,第二阶段用轻量运行时镜像(如 mcr.microsoft.com/dotnet/runtime-deps:8.0-alpine 或 runtime:8.0-slim)承载输出。这样源码、.csproj、NuGet 缓存、调试符号等全部被隔离在构建阶段,不会进入最终镜像。
- 构建阶段推荐使用
mcr.microsoft.com/dotnet/sdk:8.0,确保能执行dotnet publish - 运行阶段避免用
aspnet:8.0(含 ASP.NET Core 运行时+开发工具),优先选runtime-deps或runtime,体积更小、攻击面更窄 - 若项目是控制台应用或 Windows Service,甚至可考虑 Native AOT 发布,进一步剔除 JIT 和反射元数据
只复制 publish 输出,不复制源码或中间产物
关键操作是用 COPY --from=builder 精准复制 dotnet publish 生成的发布目录,而不是整个工作区。务必确认构建阶段执行了 dotnet publish -c Release -o /app/publish,然后运行阶段只拉取 /app/publish 下的内容。
- 不要写
COPY --from=builder /app .—— 这会把源码、obj、bin 全带进来 - 推荐写法:
COPY --from=builder /app/publish /app/,再设WORKDIR /app - 若需配置文件,应在构建阶段就注入(如用
dotnet publish --self-contained false+ 外部挂载 config),而非把appsettings.json随源码一起复制
启用 ReadyToRun 和 Trim(可选但强烈建议)
在 dotnet publish 命令中加入 --self-contained false、--runtime linux-x64、--trim 和 --rd.xml(如有反射需求),能显著缩小体积并移除未使用的程序集。
-
--trim可裁剪掉未被调用的 .NET 库代码,对 CLI 工具类项目效果尤其明显 -
--no-restore和--no-self-contained可减少冗余依赖打包,配合 slim runtime 使用更安全 - 若用 Native AOT(
-p:PublishAot=true),最终镜像可降至 20–40MB,且无 IL、无 JIT、无反射入口,源码逆向难度大幅提高
验证镜像是否真正“干净”
构建完成后,用 docker run --rm -it <image> sh 进入容器检查:是否搜不到 .cs 文件?有没有 dotnet 命令?/app 下是否只有 MyApp.dll、MyApp.deps.json、MyApp.runtimeconfig.json 和必要资源?这些是判断是否成功的直接依据。
- 执行
find / -name "*.cs" 2>/dev/null,结果应为空 - 执行
which dotnet,应返回空;dotnet --list-runtimes应报错 - 用
docker history <image>查看各层大小,确认基础镜像层(如 alpine 或 slim)占比最大,应用层尽量扁平


















