【实战分享】.NET 10 + ABP WebAPI 项目发布部署至 Docker Desktop 避坑与实践记录
前言
最近在进行一个基于.NET 10 + ABP 框架的项目重构与容器化部署工作。在将本地运行良好的项目打包发布到Docker Desktop的过程中,由于技术版本较新(.NET 10)以及多项目依赖架构下的一些隐蔽设计,踩了几处关于“NuGet中央包管理”、“网络自动加速”以及“容器端口映射”的坑。
特此整理成文,希望能为同样在做 .NET 10 容器化发布和 ABP 部署的同行们提供一些实战参考。
项目环境
开发环境:Visual Studio 2026 + Windows 11
目标框架:.NET 10.0 (net10.0)
数据库:MySQL 8.0 + Redis
部署目标:Docker Desktop for Windows (WSL2)
脱敏项目命名:GZ.WebApi(包含 GZ.WebApi.Host、GZ.WebApi.Application 等 6 个子项目)
一、 Docker 发布踩坑与解决方案
坑一:中央包管理(CPM)导致 Docker 还原(Restore)大面积报错
现象:
在编写多阶段构建的 Dockerfile 时,为了优化构建缓存,我们通常会先把各个子项目的 .csproj 文件单独 COPY 进容器进行 restore。但在执行 RUN dotnet restore 时,系统抛出大面积的error NU1015: The following PackageReference item(s) do not have a version specified...错误。原因分析:
新版项目模板中默认启用了中央包管理(Central Package Management, CPM)机制。所有的 NuGet 包版本并不是写在各自的 .csproj 里,而是统一托管在解决方案根目录下的Directory.Packages.props文件中。
由于我们单独拷贝 .csproj 时漏掉了这个属性文件,导致容器内的 NuGet 编译器找不到任何包的版本信息,直接报错。解决方案:
对于多项目且启用了 CPM 的方案,最保险、最不易出错的方式是在 Dockerfile 中直接使用“一键全拷贝”策略,完整还原本地目录结构:codeDockerfile
WORKDIR /src # 直接全拷贝,确保 Directory.Packages.props 一并带入容器 COPY . . RUN dotnet restore "src/GZ.WebApi.Host/GZ.WebApi.Host.csproj"
坑二:.NET SDK 编译版本与目标框架冲突(NETSDK1045)
现象:
在执行 Docker 编译时报错:error NETSDK1045: The current .NET SDK does not support targeting .NET 10.0.,并且在国内直连拉取海外镜像极慢。原因分析:
最初在编写 Dockerfile 时,误用了 .NET 9.0 的编译镜像(sdk:9.0)。当 9.0 的工具链去尝试编译目标框架为 net10.0 的项目时,就会触发版本不支持的报错。
微软官方的容器镜像中心(mcr.microsoft.com)物理服务器全部位于海外,国内直连拉取 1GB 左右的 SDK 镜像极易发生网络超时和断流。
解决方案:
将 Dockerfile 里的基础镜像和编译镜像版本统一提升至 .NET 10.0,并直接替换为微软官方为中国区开发者提供的Azure 中国区官方托管高速源 mcr.azure.cn,不仅解决了版本问题,下载速度也瞬间拉满:codeDockerfile
# 🌟 替换为微软中国官方高速源,并对齐 .NET 10.0 FROM mcr.azure.cn/dotnet/aspnet:10.0 AS base ... FROM mcr.azure.cn/dotnet/sdk:10.0 AS build
坑三:端口映射不一致导致容器运行但无法访问(ERR_EMPTY_RESPONSE)
现象:
Docker 构建镜像成功,并在 Docker Desktop 中顺利运行(显示为绿色的 Running),且日志里没有任何报错,但浏览器访问 http://localhost:15888/swagger/index.html 时直接提示 ERR_EMPTY_RESPONSE。原因分析:
观察容器运行日志发现一行输出:Now listening on: http://[::]:15888。
原来我们在本地的 appsettings.json 中配置了 Kestrel 绑定端口为 15888。而我们在执行 docker run 时的启动命令是 -p 15888:8080(将宿主机的 15888 映射到容器内的默认端口 8080)。
由于容器内没有进程在监听 8080(Kestrel 实际跑在容器内的 15888),导致端口通道落空。解决方案:
不需要重新打包镜像,只需在运行容器时,将端口映射调整为对齐容器内部真实的 15888 端口:codeBash
docker run -d -p 15888:15888 --name gz-api-service gz-api
二、 终极完整 Dockerfile
在主项目 GZ.WebApi.Host 根目录下,创建一个名为 Dockerfile(无任何后缀)的文件,内容如下:
codeDockerfile
# 1. 运行阶段基础镜像 (采用微软中国高速源,对齐 .NET 10.0) FROM mcr.azure.cn/dotnet/aspnet:10.0 AS base WORKDIR /app EXPOSE 15888 # 2. 编译阶段 SDK 镜像 FROM mcr.azure.cn/dotnet/sdk:10.0 AS build ARG BUILD_CONFIGURATION=Release WORKDIR /src # 3. 一键全拷贝:直接复制本地所有物理文件,完美还原您本地的目录结构,确保中央包管理配置和子项目全部带入 COPY . . # 4. 执行依赖还原 RUN dotnet restore "src/GZ.WebApi.Host/GZ.WebApi.Host.csproj" # 5. 执行编译 RUN dotnet build "src/GZ.WebApi.Host/GZ.WebApi.Host.csproj" -c $BUILD_CONFIGURATION -o /app/build # 6. 执行发布 (自动将子项目 XML 物理文件打包输出) FROM build AS publish ARG BUILD_CONFIGURATION=Release RUN dotnet publish "src/GZ.WebApi.Host/GZ.WebApi.Host.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false # 7. 组装最终轻量运行镜像 FROM base AS final WORKDIR /app COPY --from=publish /app/publish . ENTRYPOINT ["dotnet", "GZ.WebApi.Host.dll"]三、 终端实战编译与启动命令 🌟
在准备好 Dockerfile 后,我们在终端中执行以下步骤来进行物理编译与启动运行:
1. 打开终端并进入解决方案根目录
在 Visual Studio 中右键点击主项目 GZ.WebApi.Host -> 选择“在终端中打开”。
由于多项目依赖关系,我们必须退回到**解决方案根目录(包含 .sln 文件的那一层,即 src 文件夹的外层)**下执行编译。在终端中输入命令回退两级:
codeBash
cd ../..2. 执行编译命令(强行禁用缓存构建)
在终端中执行以下命令进行物理打包。由于之前可能存在历史缓存干扰,建议加入 --no-cache 标志确保彻底应用全新的 COPY . . 配置:
codeBash
docker build --no-cache -t gz-api -f src/GZ.WebApi.Host/Dockerfile .3. 运行容器
编译成功后,我们通过以下命令在 Docker Desktop 中将容器跑起来。
(注意:在此之前,确保您的开发机已经彻底退出了 IIS Express,防止本地端口被占用)
codeBash
# 强制物理删除可能存在的历史旧容器名 docker rm -f gz-api-service # 启动并绑定 15888:15888 端口 docker run -d -p 15888:15888 --name gz-api-service gz-api四、 进阶部署:生产/测试服务器离线发布 🌟
如果您的服务器部署在隔离的**企业局域网(厂区内网)**中,无法直接连接外网。我们可以利用 Docker 的导出导入机制,完成 100% 离线无缝平替部署:
1. 本地导出离线压缩包
在您有网的开发机上成功执行 docker build 生成镜像后,在终端执行以下命令,将镜像导出为一个普通的压缩包文件:
codeBash
docker save -o gz-api.tar gz-api2. 上传并导入服务器
使用文件传输工具(如 MobaXterm)将 gz-api.tar 拷贝到服务器的任意目录下(如 /root/app/),并在服务器终端执行导入命令:
codeBash
docker load -i gz-api.tar3. 服务器一键启动(挂载物理配置文件)
为了解决开发环境与服务器数据库连接 IP 不同的问题,我们直接在服务器的 /root/app/ 目录下放置一个服务器专属的 appsettings.json(里面配置服务器真实的 MySQL IP),然后通过 -v 参数强行挂载替换启动,无需重新打包:
codeBash
docker run -d \ -p 15888:15888 \ -v /root/app/appsettings.json:/app/appsettings.json \ --name gz-api-service \ gz-api五、 总结
将 .NET 10 + ABP 复杂的多项目微服务框架发布部署到 Docker 时,看似步骤简单,但实操中对于像中央包管理(CPM)、中国区镜像源替换、端口对齐以及多项目依赖搬运这样的细节处理至关重要。
通过这次实践,我们完成了对老项目架构的高吞吐容器化升级。希望这篇简洁的发布避坑记录能帮到有需要的朋友!
