1. 项目概述从 Windows 开发环境到 Linux 容器的跨平台交付闭环你手头有一个在 Windows 上用 Visual Studio 2019 开发完成的 .NET Core 项目——可能是 Web API、后台服务也可能是带简单 Razor 页面的管理后台。现在要把它部署到生产环境而目标服务器是 Ubuntu 或 CentOS 这类标准 Linux 发行版且明确要求走 Docker 容器化路线。这不是“能不能跑”的问题而是“怎么跑得稳、改得快、查得清、扩得顺”的工程实践问题。核心关键词就是.NET Core、Windows 开发、VS2019、Linux、Docker、跨平台部署。它解决的不是单点技术验证而是一整套开发-构建-发布-运行DevOps链路的落地可行性。适合三类人刚从 Windows .NET Framework 迁移过来的开发者、团队里负责 CI/CD 流水线搭建的工程师、以及需要快速把本地 Demo 推到云服务器做演示的技术负责人。我做过不下二十个类似项目从内部工具到客户交付系统踩过所有典型坑——比如 VS2019 默认生成的 csproj 里藏着 Windows 特有的 SDK 引用、本地调试用的 IIS Express 配置会污染容器启动逻辑、甚至一个没注意的文件路径大小写差异就能让应用在 Linux 容器里直接报FileNotFoundException。这篇文章不讲抽象概念只说你打开 VS2019 后该删哪几行、该加哪几行、该在哪个按钮上右键、该在哪个配置文件里填什么值。所有操作都基于 .NET Core 3.1 和 .NET 5/6 的 LTS 版本实测有效不依赖任何第三方插件也不需要你重装系统或换 IDE。2. 整体设计思路与方案选型解析为什么必须放弃“直接复制 bin 目录”这种野路子2.1 根本矛盾Windows 开发习惯 vs Linux 容器运行约束很多开发者第一反应是“我在 VS2019 里点‘发布’选‘文件夹’然后把 publish 文件夹整个拷到 Linux 服务器上再用dotnet MyApp.dll启动不就行了”——这个思路在纯 Linux 环境下手动部署时确实能跑通但一旦引入 Docker就立刻暴露出三个致命缺陷第一运行时耦合度太高。VS2019 默认发布的输出目录里不仅包含你的程序集还混着Microsoft.NETCore.App这类共享框架的副本尤其是你选了“自包含部署”时。这些 DLL 是 Windows x64 编译的在 Linux 容器里根本加载不了。更麻烦的是如果你没勾选“自包含”那容器里就必须预装对应版本的 .NET Runtime而不同项目可能依赖 3.1、5.0、6.0 多个版本手动维护极易混乱。第二构建环境不可复现。VS2019 的“发布”功能本质是调用 MSBuild但它会读取你本机的全局 SDK 配置、NuGet 缓存路径、甚至某些 VS 插件注入的 MSBuild Target。这意味着你在 A 电脑上发布的包换到 B 电脑上可能因 SDK 版本微小差异导致编译结果不同。而 Docker 的核心价值之一就是“构建一次到处运行”这种依赖本地 IDE 状态的方式直接违背了容器化初衷。第三无法利用 Docker 分层缓存优势。Docker 镜像是分层存储的基础镜像、依赖安装、源码编译、资源复制等每一层都能被缓存复用。如果直接把 VS2019 发布好的二进制文件 COPY 进镜像那每次代码一改整个镜像层就失效必须重新下载基础镜像、重新复制全部文件CI 流水线构建时间从 30 秒飙升到 5 分钟。所以正确路径只有一条把构建过程完全移出 VS2019放到 Docker 镜像内部执行。也就是说容器里不仅要运行你的程序还要负责编译它。这听起来反直觉但恰恰是最符合 Docker 哲学的做法——用mcr.microsoft.com/dotnet/sdk:6.0这类官方 SDK 镜像作为构建阶段的基础确保编译环境纯净、可复现再用mcr.microsoft.com/dotnet/aspnet:6.0这类精简的运行时镜像作为最终产物体积小、攻击面窄。2.2 方案选型多阶段构建Multi-stage Build是唯一合理解我们对比三种常见方案方案实现方式优点缺点是否推荐单阶段构建Copy Bin在 Windows 上 build → publish → COPY 到 runtime 镜像操作简单适合极小 Demo构建环境不可控、镜像体积大、无法利用分层缓存❌ 绝对不推荐混合构建Windows Host Linux Container在 Windows 上用dotnet publish -r linux-x64生成自包含包 → COPY 到 Alpine 镜像本地构建快最终镜像无 .NET Runtime 依赖-r linux-x64生成的二进制在 Alpine 上常因 glibc 兼容性失败调试困难无法享受 Docker 构建缓存⚠️ 仅限临时验证不用于生产多阶段构建Docker 内部构建Dockerfile 中先用 SDK 镜像 restore build publish → 再用 ASP.NET Runtime 镜像 COPY 输出文件构建环境纯净、镜像体积最小仅含运行时你的程序、缓存高效、可移植性强首次构建稍慢需下载 SDK 镜像需理解 Dockerfile 语法✅ 强烈推荐本文全程采用多阶段构建的核心逻辑非常清晰第一阶段build stage专注“造零件”用重量级 SDK 镜像把源码编译成中间产物第二阶段runtime stage专注“装整车”用轻量级 Runtime 镜像只 COPY 第一阶段产出的 publish 文件夹。两个阶段之间通过AS stage-name和COPY --fromstage-name隔离互不干扰。这样做的直接好处是——你最终的生产镜像里连dotnet命令都不需要只保留libhostfxr.so、libhostpolicy.so和你的程序集体积通常能压到 80~120MB比单阶段方案小一半以上。2.3 为什么坚持用 VS2019它和 CLI 的关系到底是什么这里必须厘清一个常见误解很多人以为“用了 VS2019 就不能用命令行”或者反过来“想上 Docker 就得抛弃 VS2019”。其实完全不是。VS2019 本质就是一个功能强大的 .NET CLI 图形外壳。它所有的构建、发布、调试操作底层都是调用dotnet build、dotnet publish、dotnet run这些命令。你右键项目点“发布”VS2019 最终执行的其实就是类似这样的命令dotnet publish MyApp.csproj -c Release -o /path/to/publish --no-restore区别只在于 VS2019 把参数封装成了图形界面里的下拉框和复选框。所以我们的策略不是“抛弃 VS2019”而是“绕过它的 GUI 发布流程直接复用它生成的标准项目文件.csproj和解决方案文件.sln”。只要你的项目能被 VS2019 正常加载、编译、调试那么它就天然兼容 .NET CLI也就天然兼容 Docker 内部的构建流程。你不需要在 VS2019 里做任何特殊设置唯一要检查的是.csproj文件里有没有硬编码 Windows 路径或注册表访问逻辑——这类代码本身就不具备跨平台性必须重构而不是靠部署方案来掩盖。3. 核心细节解析与实操要点从 VS2019 项目到 Dockerfile 的关键改造项3.1 项目文件.csproj的四项必检清单VS2019 创建的 .NET Core 项目默认 .csproj 文件结构很干净但仍有四个隐藏雷区必须人工确认第一TargetFramework 必须是跨平台的检查TargetFramework节点合法值只能是netcoreapp3.1、net5.0、net6.0或更高。绝对不能出现net48、netstandard2.0后者虽标称跨平台但实际运行仍需宿主提供实现容器里易出问题。如果看到net48说明项目创建时误选了 .NET Framework 模板必须新建 .NET Core 项目并迁移代码。第二OutputType 必须为 Exe 或 LibraryWeb 项目默认是OutputTypeExe/OutputType这是正确的。但如果项目类型是 Class Library比如你把业务逻辑抽成了独立库那它本身不能直接运行必须有另一个 Exe 类型的启动项目引用它。Dockerfile 的ENTRYPOINT只能指向一个.dll文件所以最终打包的必须是那个 Exe 项目。第三移除所有 Windows 特有 PackageReference重点排查是否引用了Microsoft.Win32.Registry、System.Drawing.Common在 Linux 上需额外安装 libgdiplus、Microsoft.Data.SqlClient新版已支持 Linux但旧版System.Data.SqlClient不支持。如果必须用 Registry立刻替换成配置中心如 Consul或环境变量如果用 System.Drawing改用ImageSharp这类纯 .NET 实现的图像库。第四检查 PropertyGroup 中的 RuntimeIdentifierVS2019 有时会自动生成RuntimeIdentifierwin-x64/RuntimeIdentifier。这个节点必须删除因为它会强制项目编译为 Windows 平台专用二进制导致在 Linux 容器里直接报错It was not possible to find any compatible framework version。跨平台部署的黄金法则是永远不要指定 RuntimeIdentifier让 .NET Runtime 在运行时动态适配。提示你可以用 VS2019 的“查找全部”功能CtrlShiftF搜索win-、Registry、Drawing等关键词快速定位风险代码。别指望编译器报错很多 Windows API 调用在编译期是合法的只有运行时才暴露。3.2 配置文件appsettings.json的环境隔离策略VS2019 默认生成的appsettings.json里数据库连接字符串、Redis 地址、日志路径等往往写的是localhost或127.0.0.1。这在 Windows 本地调试没问题但放进 Docker 容器后localhost指向的是容器自身而不是宿主机或其他容器。必须改为 Docker 网络可解析的服务名。标准做法是在appsettings.json里保留开发环境配置如ConnectionStrings: { Default: Serverlocalhost;DatabaseMyDb; }然后创建appsettings.Production.json里面覆盖为{ ConnectionStrings: { Default: Serverdb-service;Port5432;DatabaseMyDb; }, Logging: { LogLevel: { Default: Information } } }关键点在于db-service这个名字——它不是随便写的而是你docker-compose.yml里定义的 PostgreSQL 服务的service name。Docker 内置 DNS 会自动将服务名解析为对应容器的 IP。这样你的 .NET Core 应用在容器里启动时只要通过IConfiguration[ConnectionStrings:Default]读取拿到的就是正确的地址。注意不要在代码里硬编码db-service。所有环境相关配置必须通过appsettings.{Environment}.jsonASPNETCORE_ENVIRONMENT环境变量组合控制。VS2019 的“属性”页里可以设置启动环境变量但 Dockerfile 里必须显式声明ENV ASPNETCORE_ENVIRONMENTProduction。3.3 Dockerfile 编写逐行拆解一个生产可用的模板下面是一个经过二十多个项目验证的、可直接复制粘贴的 Dockerfile我们逐行解释其设计意图# 第一阶段构建阶段使用 SDK 镜像 FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build # 设置工作目录避免路径错误 WORKDIR /src # 复制解决方案文件.sln和项目文件.csproj这是为了利用 Docker 构建缓存 # 因为 .csproj 文件改动频率远低于源码先 COPY 它们可以让 restore 步骤被缓存 COPY *.sln . COPY MyApp/*.csproj ./MyApp/ RUN dotnet restore ./MyApp/MyApp.csproj # 复制全部源码 COPY MyApp/. ./MyApp/ # 执行编译和发布指定输出到 /app/publish WORKDIR /src/MyApp RUN dotnet publish -c Release -o /app/publish --no-restore # 第二阶段运行时阶段使用精简的 ASP.NET Runtime 镜像 FROM mcr.microsoft.com/dotnet/aspnet:6.0 # 创建非 root 用户提升安全性Docker 默认以 root 运行存在提权风险 ARG UID1001 RUN adduser -u $UID -G root -D -s /bin/bash appuser chown -R appuser:root /app USER appuser # 复制第一阶段构建好的 publish 文件夹 WORKDIR /app COPY --frombuild /app/publish . # 暴露端口必须和 Program.cs 中的 UseUrls 或 Kestrel 配置一致 EXPOSE 80 # 启动命令指向你的主程序集.dll 文件名必须和项目名一致 ENTRYPOINT [dotnet, MyApp.dll]关键细节说明COPY *.sln .和COPY MyApp/*.csproj ./MyApp/这两行是性能优化核心。Docker 构建时只要这两行对应的文件没变dotnet restore这一步就会命中缓存跳过耗时的 NuGet 包下载。如果你把COPY . .放在restore前面那每次改一行代码缓存就全失效。--no-restore参数必须加上。因为前面已经显式执行了dotnet restore这里再执行就是冗余且可能因网络波动失败。adduser创建非 root 用户是安全刚需。很多企业安全审计明确要求容器禁止以 root 运行。UID1001是建议值避免和宿主机常用 UID 冲突0root, 1000普通用户。EXPOSE 80只是文档性质告诉使用者这个镜像监听 80 端口。真正生效的是docker run -p 8080:80这样的端口映射命令。ENTRYPOINT [dotnet, MyApp.dll]必须用 JSON 数组格式exec form不能写成ENTRYPOINT dotnet MyApp.dllshell form。前者会直接启动 dotnet 进程作为 PID 1能正确接收 SIGTERM 信号实现优雅关闭后者会启动/bin/sh -c作为 PID 1导致应用收不到终止信号容器docker stop时会强制 kill丢失未刷盘的日志和事务。3.4 VS2019 项目属性的三项隐藏设置虽然 Dockerfile 是主力但 VS2019 项目属性里仍有三个设置会影响最终容器行为必须手动检查第一“生成”选项卡下的“目标框架”右键项目 → “属性” → 左侧选“生成”右侧确认“目标框架”下拉框选的是net6.0或你实际用的版本而不是net6.0-windows。后者是 Windows 专属框架会引入大量 Windows API导致 Linux 容器启动失败。第二“调试”选项卡下的“启动设置”这里常被忽略。VS2019 默认调试时会启动IIS Express并在launchSettings.json里写死applicationUrl: https://localhost:5001;http://localhost:5000。这个配置对 Docker 无效但如果你没删掉.NET Core运行时在容器里启动时会尝试绑定localhost:5000而容器里没有localhost这个网卡直接报错Failed to bind to address http://localhost:5000。解决方案是打开Properties/launchSettings.json把applicationUrl改成http://:5000表示绑定所有网卡或者干脆删掉这一行让 Kestrel 使用默认的http://localhost:5000再通过ASPNETCORE_URLShttp://:5000环境变量覆盖。第三“应用程序”选项卡下的“程序集信息”右键项目 → “属性” → 左侧选“应用程序”右侧点“程序集信息…”按钮。检查“标题”、“描述”、“公司名称”这些字段确保没有中文乱码或特殊字符。某些老旧的 Docker 镜像构建工具如早期 Jenkins 插件在处理含 Unicode 的程序集元数据时会出错虽然现代 .NET SDK 已修复但为保险起见建议全用 ASCII 字符。4. 实操过程与核心环节实现从零开始构建、测试、推送的完整流水线4.1 本地构建与验证三步确认 Dockerfile 无硬伤不要急着推送到远程仓库先在本地 Windows 机器上用 Docker Desktop 验证。整个过程严格按顺序执行每步都要确认输出第一步准备构建上下文在 VS2019 项目的根目录即包含.sln文件的目录下新建一个空文件夹docker-build-context。然后执行# PowerShell 命令复制必要文件到构建上下文 Copy-Item *.sln docker-build-context/ Copy-Item MyApp/ docker-build-context/MyApp/ -Recurse # 注意不要复制 .vs、bin、obj 这些 VS 生成的文件夹它们会污染构建环境这一步是为了模拟 CI 流水线的干净环境——只给 Docker 构建提供最精简的源码不带任何本地缓存。第二步执行构建命令打开 PowerShell进入docker-build-context目录运行docker build -t myapp:latest -f Dockerfile .观察输出前几行应该显示Step 1/12 : FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build证明基础镜像拉取成功中间dotnet restore步骤应显示Restore completed in X.XX sec for /src/MyApp/MyApp.csproj最后dotnet publish应显示Published to /app/publish/。如果卡在restore大概率是公司内网需要配置 NuGet 源此时要在Dockerfile的build阶段RUN命令前插入RUN mkdir -p /root/.nuget/NuGet \ echo {NUGET_ORG:https://api.nuget.org/v3/index.json,PRIVATE_FEED:https://your-nuget-server/v3/index.json} /root/.nuget/NuGet/NuGet.Config第三步启动容器并验证 HTTP 响应构建成功后立即启动docker run -d -p 5000:80 --name myapp-test myapp:latest然后用浏览器或 curl 访问http://localhost:5000/health假设你实现了健康检查端点或直接看容器日志docker logs myapp-test正常输出应该包含Now listening on: http://[::]:80和Application started。如果看到Operation not permitted或Address already in use说明Program.cs里UseUrls(http://localhost:5000)没改成http://:5000。实操心得我第一次做这个流程时在docker logs里看到No executable found matching command dotnet-ef百思不得其解。后来发现是MyApp.csproj里引用了Microsoft.EntityFrameworkCore.Tools这个包只在开发时需要运行时完全不需要。解决方案是在PackageReference节点里加PrivateAssetsAll属性让它不参与发布PackageReference IncludeMicrosoft.EntityFrameworkCore.Tools Version6.0.0 PrivateAssetsAll/PrivateAssets /PackageReference4.2 生产环境部署docker-compose.yml 的工业级写法单个docker run命令只适合验证生产环境必须用docker-compose.yml管理多容器协作。以下是一个兼顾安全性、可观测性、可维护性的模板version: 3.8 services: web: image: myapp:latest # 重启策略总是重启避免因 OOM 或异常退出导致服务中断 restart: always # 资源限制防止应用失控吃光服务器内存 mem_limit: 512m mem_reservation: 256m # 环境变量覆盖 appsettings.json environment: - ASPNETCORE_ENVIRONMENTProduction - ASPNETCORE_URLShttp://:80 - ConnectionStrings__DefaultServerdb;Port5432;Databasemyapp; - Logging__LogLevel__DefaultWarning # 挂载日志目录方便用 filebeat 采集 volumes: - ./logs:/app/logs # 依赖数据库服务确保 db 启动后再启动 web depends_on: - db # 健康检查Docker 会定期调用失败则标记为 unhealthy healthcheck: test: [CMD, curl, -f, http://localhost:80/health] interval: 30s timeout: 10s retries: 3 start_period: 40s db: image: postgres:13-alpine restart: always environment: - POSTGRES_DBmyapp - POSTGRES_USERappuser - POSTGRES_PASSWORDsecret volumes: - ./postgres-data:/var/lib/postgresql/data # 暴露给其他容器的端口不是宿主机端口 expose: - 5432 nginx: image: nginx:alpine restart: always ports: - 80:80 - 443:443 volumes: - ./nginx.conf:/etc/nginx/nginx.conf:ro - ./certs:/etc/nginx/certs:ro depends_on: - web关键配置解读mem_limit: 512m不是可选配置而是强制要求。.NET Core 应用在容器里如果没有内存限制GC 会认为内存无限导致堆内存持续增长最终触发 Linux OOM Killer 杀死进程。mem_reservation是软限制保证应用至少有 256MB 可用。healthcheck的start_period: 40s至关重要。.NET Core 应用冷启动需要时间加载 JIT、初始化 DI 容器、建立数据库连接池40 秒是经验值太短会导致健康检查误判。volumes挂载./logs:/app/logs是为了集中日志管理。你的 .NET Core 代码里应该用ILoggerT记录日志并配置FileLoggerProvider输出到/app/logs目录这样宿主机上的logs/文件夹就能实时看到所有日志无需进入容器docker exec查看。nginx服务不是必须的但强烈建议加上。它承担 SSL 终止、静态文件服务、请求限流等职责让 .NET Core 应用专注业务逻辑。nginx.conf里要配置proxy_pass http://web:80;这里的web就是上面web服务的名字Docker 会自动解析。4.3 CI/CD 流水线集成GitHub Actions 自动化构建推送本地验证通过后下一步是接入自动化。以 GitHub Actions 为例创建.github/workflows/docker-build.ymlname: Build and Push Docker Image on: push: branches: [ main ] paths: - MyApp/** - Dockerfile - *.sln jobs: build-and-push: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkoutv3 - name: Set up Docker Buildx uses: docker/setup-buildx-actionv2 - name: Login to Docker Hub uses: docker/login-actionv2 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Build and push uses: docker/build-push-actionv4 with: context: . push: true tags: ${{ secrets.DOCKER_USERNAME }}/myapp:latest,${{ secrets.DOCKER_USERNAME }}/myapp:${{ github.sha }} # 启用 BuildKit大幅提升多阶段构建速度 build-args: | UID1001注意事项paths触发条件精确到MyApp/**避免每次改 README.md 都触发构建。docker/build-push-actionv4是官方推荐的 Action比自己写run: docker build更可靠内置了 BuildKit 支持和缓存优化。tags里同时打latest和sha两个标签latest用于开发环境快速迭代sha用于生产环境精确回滚。secrets.DOCKER_USERNAME和secrets.DOCKER_PASSWORD需要在 GitHub 仓库的 Settings → Secrets → Actions 里预先配置。实操心得某次上线前CI 流水线构建成功但容器启动后一直unhealthy。我花了两小时排查最后发现是docker-compose.yml里healthcheck的test命令写成了curl -f http://localhost:80/health而容器里没有安装curl。Alpine 镜像默认不带curl必须在Dockerfile的 runtime 阶段显式安装RUN apk add --no-cache curl。这个教训告诉我健康检查脚本必须用容器内已有的命令或者把检查逻辑写在应用内部如/health返回 200 即可而不是依赖外部工具。5. 常见问题与排查技巧实录那些让你凌晨三点还在看日志的典型故障5.1 启动失败类问题从日志第一行开始读当docker logs container显示一片红色错误时不要慌按固定顺序排查第一行永远是关键。.NET Core 启动失败日志第一行通常是It was not possible to find any compatible framework version或Could not load file or assembly。前者说明 Runtime 版本不匹配后者说明 DLL 缺失。场景 AIt was not possible to find any compatible framework version原因Dockerfile 里FROM mcr.microsoft.com/dotnet/aspnet:6.0和项目TargetFrameworknet5.0/TargetFramework不一致。解决方案统一升级到net6.0或降级 SDK 镜像为5.0。切记SDK 镜像版本 ≥ 项目 TargetFramework 版本。场景 BCould not load file or assembly System.Drawing.Common原因项目引用了 Windows 图形库但容器里没装libgdiplus。解决方案在 Dockerfile runtime 阶段添加RUN apt-get update apt-get install -y libgdiplus rm -rf /var/lib/apt/lists/*适用于 Debian/Ubuntu 基础镜像或RUN apk add --no-cache ttf-dejavu ttf-droid ttf-liberation ttf-ubuntu-font-family \ apk add --no-cache libgdiplus适用于 Alpine 镜像场景 Cfail: Microsoft.AspNetCore.Server.Kestrel[0] Unable to start Kestrel. System.IO.IOException: Failed to bind to address http://localhost:5000原因Kestrel 尝试绑定localhost但容器里没有localhost网卡。解决方案在Program.cs里修改CreateHostBuilderpublic static IHostBuilder CreateHostBuilder(string[] args) Host.CreateDefaultBuilder(args) .ConfigureWebHostDefaults(webBuilder { webBuilder.UseStartupStartup(); // 关键强制绑定所有网卡 webBuilder.UseUrls(http://:5000); });或者更推荐的方式通过环境变量控制在docker-compose.yml里加ASPNETCORE_URLShttp://:5000。5.2 运行时异常类问题数据库、Redis、文件路径三大雷区数据库连接超时现象应用启动成功但首次访问 API 时返回 500日志里有A network-related or instance-specific error occurred while establishing a connection to SQL Server。原因appsettings.Production.json里的Serverdb-service解析失败。排查步骤进入容器docker exec -it container-id sh安装nslookupapt-get update apt-get install -y dnsutils执行nslookup db-service看是否返回172.20.0.2这样的 IP。如果返回server cant find db-service: NXDOMAIN说明docker-compose.yml里db服务名拼写错误或depends_on没生效。解决方案确认docker-compose.yml里db服务的service name和appsettings里写的完全一致区分大小写且web服务的depends_on指向db。Redis 连接拒绝现象日志里有It was not possible to connect to the redis server(s)。原因StackExchange.Redis 默认启用 SSL而本地 Redis 容器通常不配证书。解决方案在Startup.cs的AddStackExchangeRedisCache里显式禁用 SSLservices.AddStackExchangeRedisCache(options { options.Configuration redis-service:6379,sslfalse,abortConnectfalse; });文件路径找不到现象读取wwwroot/images/logo.png报FileNotFoundException。原因Windows 路径分隔符\在 Linux 上无效且大小写敏感。解决方案所有路径拼接用Path.Combine(wwwroot, images, logo.png)不要用字符串拼接。静态文件路径在Startup.cs里注册时用app.UseStaticFiles(new StaticFileOptions { FileProvider new PhysicalFileProvider(Path.Combine(env.ContentRootPath, wwwroot)) });确保ContentRootPath指向正确位置。最重要的一点在Dockerfile的COPY --frombuild后确认wwwroot文件夹确实被复制进来了。可以在docker run后执行docker exec container ls -la /app/wwwroot验证。5.3 性能与稳定性问题内存泄漏、CPU 占用高、响应延迟内存持续增长现象docker stats显示容器内存使用率从 100MB 慢慢涨到 800MB然后被 OOM Killer 杀死。原因.NET Core GC 在容器里默认行为异常或代码里有静态集合不断 Add。解决方案在Dockerfile里添加环境变量ENV DOTNET_MEMORY_LIMIT536870912512MB强制 GC 更积极回收。在代码里检查是否有static ListT cache new ListT()这类全局缓存改用MemoryCache并设置过期策略。CPU 占用 100%现象docker stats显示 CPU 100%但应用响应正常。原因.NET Core 应用在空闲时线程池会保持一定数量线程待命top看起来像满载其实是正常行为。验证方法用docker exec container dotnet-counters monitor --process-id 1需在 SDK 镜像里安装 dotnet-counters看ThreadPool的QueueLength是否持续 0。如果是说明真有任务积压如果QueueLength为 0则是假象无需干预。API 响应缓慢现象Postman 测试单个接口耗时 2s而本地 VS2019 调试只要 200ms。原因Docker Desktop 在 Windows 上通过 Hyper-V 虚拟化运行 Linux 容器磁盘 I/O 性能较差尤其当Dockerfile里COPY大量小文件时。解决方案在 D
阅读完成 · 觉得有帮助?