C# WebApi 脱离IIS:Kestrel自托管部署实践

发布时间:2026/10/14 17:23:16
C# WebApi 脱离IIS:Kestrel自托管部署实践 简介C#构建与IIS解耦的WebApi服务Demo面向需要在非Windows环境或容器化场景中部署服务的.NET开发者。该演示项目通过自我托管方式借助轻量级Web服务器创建不依赖互联网信息服务的网络API服务实现跨平台运行与生命周期自主管理。资源共188个文件压缩包约5.92MB包含C#源代码、项目配置文件、NuGet依赖包以及编译生成的DLL和XML文档项目结构清晰可帮助读者直接查看实现细节并复用部署方案。已有750人学习适合对网络API解耦原理、RESTful接口设计及微服务部署感兴趣的初中级开发人员。通过此项目可掌握服务自托管配置、HTTP通信方案及解耦部署的实际落地方法为后续容器化或跨平台项目提供参考。1. 为什么要把 WebApi 从 IIS 里“拆”出来C# 开发者做 WebApi 服务默认动作往往是新建一个 ASP.NET Core 项目然后发布到 IIS。这套流程在内部系统里确实省事但一旦你遇到发布时要停站点、端口被占用、服务器上要装一堆运行时组件、或者想把它部署到 Linux 容器里IIS 就成了最大的牵制。所谓“C# 构建与 IIS 解耦的 WebApi 服务 Demo”核心就一句话让服务进程不再寄生在 IIS 的工作进程里而是用 Kestrel 自己监听端口、自己管理生命周期。这样服务变成了一个独立进程可以注册成 Windows 服务、可以放进 Docker、也可以直接dotnet run跑起来发布和运维的灵活性完全不一样。这篇笔记适合已经能写 Basic CRUD、但没试过自托管部署方式的开发者我会把从空项目到独立服务的最小路径、参数设置和踩坑点一次性讲透。2. 先想清楚解耦 IIS 到底解耦什么2.1 三个解耦层次别混为一谈第一个层次是“不依赖 IIS 作为宿主”。你的服务启动时自己创建 Host监听 TCP 端口和 IIS 的请求管道没有关系。这是最基础的一层也是本文 Demo 要做的。第二个层次是“不依赖 IIS 作为反向代理”。即使你的服务是自托管的很多人还是习惯在 IIS 前面挂一个反向代理把 80/443 的流量转给 Kestrel。这在混合部署时很常见但严格来说你的服务已经和 IIS 没有进程级绑定了。第三个层次是“不依赖 Windows 平台”。如果你用了 .NET 的跨平台能力服务可以直接跑在 Linux 上用 Nginx 做反向代理。这层最彻底但涉及的东西也最多我在最后一章会给出一个最小验证路径。很多教程把这三层混在一起讲导致新手以为“不用 IIS 就是不用代理”。事实上解耦的目的是让服务本身变得可移植而不是强迫你抛弃所有现有设施。你自己判断一下当前需求属于哪一层再做对应改造否则容易过度设计。2.2 为什么默认模板仍然绑定 IIS用 Visual Studio 新建 ASP.NET Core WebApi 项目时模板会自动生成Properties/launchSettings.json里面默认的profile是 IIS Express而且Program.cs里往往带有IISIntegration()相关的配置。这不是说你必须用 IIS而是模板默认帮你配好了开发时的快捷启动方式。真正的坑点在这里很多人发布的时侯直接从 Visual Studio “发布”按钮选 IIS然后用 Web Deploy 传到服务器。这个流程本身没问题但它会把你的服务配置成一个“必须由 IIS 管理的应用程序”。一旦你改了监听端口、加了 gRPC、用了 WebSocketIIS 这边就可能出现玄学问题最常见的翻车现场就是 HTTP 502 或连接被重置。我一般会建议从新建项目那一刻起就删掉 IIS Express 相关的依赖让 Kestrel 直接控制生命周期。发布时只需要拷文件不需要在 IIS 里建站点。2.3 解耦后的服务结构长什么样解耦后的 WebApi 服务最终组成是这样的一个执行文件MyApi.exe或MyApi.dll一个appsettings.json配置监听地址、日志级别、数据库连接串一个wwwroot目录如果提供静态文件运行时依赖文件发布模式下是自包含的不需要服务器预装 .NET整个服务就像一个普通桌面程序一样启动和退出。你可以在命令行里执行MyApi.exe --urls http://0.0.0.0:5000来指定端口也可以让服务读取配置文件。它不关心是谁把它拉起来的只要进程活着它就能服务请求。上面的描述和部署方式也说明了为什么这种结构适合做 Demo你不需要先搭好 IIS 再写代码代码写完就能脱离所有托管平台独立运行验证成本低试错路径短。3. 用 WebApplication 搭一个自托管 WebApi核心代码与参数说明3.1 最小项目的文件清单动手之前先把项目结构理一理。我习惯创建一个空解决方案然后手动添加一个 ASP.NET Core WebApi 项目不勾选任何模板附加项。最终你需要的文件就这些文件作用Program.cs程序入口创建 Host、注册服务、配置中间件Controllers/WeatherController.cs一个示例控制器用于验证服务通不通appsettings.json运行配置包括监听地址和日志设置MyApi.csproj项目文件声明目标框架和依赖包不要从模板直接生成然后删东西那样容易留下隐藏配置。手动创建更干净你每一步都清楚自己在加什么。3.2 Program.cs 里少不了的几个配置先看一份可完整运行的最小代码。下面这个Program.cs是解耦 WebApi 最朴实、最直接的写法var builder WebApplication.CreateBuilder(args); // 1. 明确使用 Kestrel不加载 IIS 集成模块 builder.WebHost.ConfigureKestrel(options { // 允许的最大请求体大小默认 30MB设为 null 表示不限制 options.Limits.MaxRequestBodySize null; // 连接超时时间单位秒默认 240 options.Limits.KeepAliveTimeout TimeSpan.FromSeconds(120); }); // 2. 从 appsettings.json 读取 urls也可以直接用 UseUrls builder.WebHost.UseUrls(http://0.0.0.0:5000); // 3. 注册控制器和 Swagger可选Demo 阶段建议加 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app builder.Build(); // 4. 中间件管道 if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.MapControllers(); app.Run();这段代码里有几个关键点需要展开。第一ConfigureKestrel里的KeepAliveTimeout和MaxRequestBodySize是日常最容易踩坑的两个参数。长连接场景下如果KeepAliveTimeout设得太短客户端频繁重连日志里全是断开记录上传文件场景下如果MaxRequestBodySize不放开POST 大文件时会直接返回 413。第二UseUrls(http://0.0.0.0:5000)表示监听本机所有可用网卡的 5000 端口。如果只需要本机访问写http://localhost:5000即可但要注意写成localhost时局域网内其他机器无法访问你的服务这在联调时会浪费大量时间排查防火墙结果发现是绑定地址的问题。第三app.Run()是阻塞调用。它会一直运行直到收到进程终止信号。这也是自托管和 IIS 最大的区别——退出机制完全由操作系统和自己的代码控制IIS 的回收机制不再介入。3.3 添加一个可验证的控制器光有入口没有出口没法确认服务真的活了。加一个最简单的控制器用来做一些基础自检using Microsoft.AspNetCore.Mvc; namespace MyApi.Controllers; [ApiController] [Route(api/[controller])] public class HealthController : ControllerBase { [HttpGet] public IActionResult Check() { var now DateTime.Now; var machineName Environment.MachineName; return Ok(new { status ok, time now.ToString(yyyy-MM-dd HH:mm:ss), machine machineName }); } }这个控制器的意义在于它返回了机器名和时间当你部署到多台服务器后面做负载均衡时能快速确认当前请求落在哪台机器上。而且它不依赖数据库、不依赖第三方服务是最干净的验证点。如果返回的都是 Kestrel 给出的默认响应头没有 IIS 的 Server 头就说明请求确实直接到达了你的服务。3.4 本地启动和验证命令代码写完后在项目目录下打开终端执行dotnet run --project MyApi.csproj --urls http://localhost:5000启动后你会看到控制台输出监听地址。然后另开一个终端验证curl http://localhost:5000/api/Health如果返回了一段 JSON说明自托管服务已经跑通了。这条命令也是后面排查问题时的第一个检查点先确认进程本身能起来再谈网关和代理的配置。4. 把服务注册成 Windows 服务彻底摆脱 IIS 的运维负担4.1 为什么 Windows 服务是解耦后的第一站很多开发者查了不少资料把服务解耦后不知道该放在哪。最常见的选择有两个一个是用 NSSM 把 exe 包装成 Windows 服务另一个是直接在 .NET 里集成WindowsServiceLifetime。前者适合已经编译好的程序后者适合你自己维护代码。我更推荐后者因为只用 NuGet 包代码里加两行配置就完事不需要额外安装工具。而且由代码自己控制生命周期比外层包装要稳定得多日志和退出事件的归属也更清楚。4.2 改造 Program.cs 支持服务模式在刚才的Program.cs基础上增加对 Windows 服务的支持using Microsoft.Extensions.Hosting.WindowsServices; var builder WebApplication.CreateBuilder(args); // 关键让应用感知 Windows 服务生命周期 builder.Host.UseWindowsService(options { options.ServiceName MyApiService; }); builder.WebHost.ConfigureKestrel(options { options.Limits.MaxRequestBodySize null; options.Limits.KeepAliveTimeout TimeSpan.FromSeconds(120); }); builder.WebHost.UseUrls(http://0.0.0.0:5000); builder.Services.AddControllers(); var app builder.Build(); app.UseAuthorization(); app.MapControllers(); app.Run();UseWindowsService是微软官方的扩展方法它会做三件事让日志输出到 Windows 事件日志、在系统停止服务时优雅关闭 Kestrel、把服务的启动状态和 SCM 绑定在一起。发布时选择“框架依赖”还是“自包含”这个决定很关键。如果服务器上已经装了对应版本的 .NET Runtime那部署一个 nn 几 MB 的文件就够了如果服务器是干净的那你得发布自包含版本目录会大一些但完全不需要预装运行环境。我一般偏向自包含省去和服务器管理员解释“为什么要装运行库”的沟通成本。4.3 安装服务的三条命令发布完成后用管理员身份打开 PowerShell执行sc.exe create MyApiService binPath C:\apps\myapi\MyApi.exe start auto sc.exe start MyApiService sc.exe query MyApiService这里有个非常容易翻车的细节sc.exe create的binPath后面必须紧跟一个空格再写路径而且路径中如果有空格必须整体用反斜杠转义引号。格式如下sc.exe create MyApiService binPath C:\apps\myapi\MyApi.exe如果少写了空格sc.exe会报参数不正确。这个点卡了很多人。另外服务启动后如果马上退出先看 Windows 事件日志路径在事件查看器 → Windows 日志 → 应用程序来源是MyApiService。4.4 服务模式下日志去哪了控制台模式下日志直接打在黑窗口里谁都能看到。服务模式下标准输出被系统吞掉了所以你必须在代码里配置日志输出目标和格式。下面是一个可复用的配置builder.Logging.ClearProviders(); builder.Logging.AddConsole(); builder.Logging.AddEventLog(options { options.LogName MyApi; options.SourceName MyApiService; options.Filter (category, level) level LogLevel.Warning; });这里AddEventLog的SourceName如果和 Windows 服务名不一致日志可能写不进去。事件日志不是用来记录业务流水的大容量存储它只适合记录启动、异常、崩溃这类关键节点业务日志请写到文件或专门的日志系统。5. 自托管服务的常见问题排查五个翻车现场复盘5.1 启动秒退控制台都看不到内容现象双击 exe 或dotnet run后窗口一闪而过服务根本没有起来。原因最常见的是端口被占用其次是配置文件里的 JSON 格式错误第三是缺少运行时。解决先改端口试一次dotnet run --urls http://localhost:5001。如果还是秒退把所有catch异常输出到日志文件不要依赖控制台。事件日志里也会有一条 .NET Runtime 的记录来源类似.NET RuntimeID 1026 通常带异常堆栈。先把这条日志拿到问题定位就快了。5.2 局域网其他机器访问不了现象本机curl一切正常换一台机器就是连接超时。原因UseUrls绑定了localhost或者 Windows 防火墙拦截了端口。解决把UseUrls改成http://0.0.0.0:5000然后在防火墙入站规则里放行 5000 端口。这一步我不止一次看到有人改完代码忘了放行防火墙折腾了半小时才发现是系统层面的拦截。5.3 服务注册成功但启动失败现象sc.exe start返回错误 1053或者服务状态一直是“正在启动”。原因UseUrls指向的端口被其他进程占用或者日志目录没有写权限。解决先用netstat -ano | findstr :5000看谁占着端口如果被占换一个端口如果没被占确认运行服务的账号是否有权限写日志文件。服务默认以 LocalSystem 运行权限一般没问题但如果你改成特定域账号就得检查目录权限。5.4 修改代码后发布服务还是旧逻辑现象明明重编了代码也覆盖发布了文件但服务行为还是旧的。原因Windows 服务启动时会加载 exe 所在目录的文件如果你覆盖发布的是另一个目录或者服务没完全停止就拷文件会导致文件被占用。解决先sc.exe stop MyApiService确认状态为 Stopped再覆盖文件再sc.exe start MyApiService。不要跳过停止步骤我之前因为图省事直接覆盖结果 exe 文件被锁定发布失败还白白多花了几分钟查问题。5.5 Kestrel 的 5000 端口和 IIS 的 80 端口冲突现象服务器上既有 IIS 站点又在跑 Kestrel外网请求偶尔出现 502。原因IIS 默认侦听 80 端口Kestrel 如果也绑了 80端口冲突时 IIS 会优先占住Kestrel 可能只在内网可以访问。解决Kestrel 监听一个非 80 端口然后用 IIS 或 Nginx 反向代理转发。但如果你追求彻底解耦干脆在服务器上停掉 IIS 网站只留 Kestrel。这个选择没有绝对的对错取决于你还有没有其他站点依赖 IIS。6. 从 Demo 到可用把服务搬到 Linux 上再做一次验证如果你的目标不只是解耦 IIS而是彻底摆脱 Windows 服务器那最后一步就是把 Demo 跑在 Linux 上。这个过程并不困难但有几个细节值得快速验证一下。发布命令和项目文件里要加一行RuntimeIdentifier或使用-r linux-x64参数dotnet publish -c Release -r linux-x64 --self-contained true -o ./publish把publish目录拷贝到 Linux 服务器后直接执行./MyApi即可运行。如果你要固定端口运行前设置环境变量或者加--urls参数。日志输出到控制台配合 systemd 管理服务。这里我不展开 systemd 配置因为上文所有验证逻辑在 Linux 上完全一致。真正需要注意的是路径分隔符号和配置文件读法。appsettings.json里不要使用硬编码的C:\xxx路径用相对路径或环境变量替代这样在 Windows 和 Linux 上都能跑。这个是很多跨平台部署翻车的根源我自己也吃过亏。我现在的习惯是写任何 WebApi Demo 都先用 Kestrel 自托管跑通再考虑要不要挂代理。这样项目从一开始就是可移植的后续加 Docker 支持、加 CI/CD 都顺理成章。希望这篇笔记能让你少走一些弯路祝顺利。本文还有配套的精品资源点击获取