ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

麒麟服务器部署.NET Core全流程:从环境识别到Nginx反代实战

麒麟服务器部署.NET Core全流程:从环境识别到Nginx反代实战 搞麒麟服务器上部署 .NET Core 这件事说实话第一次干的时候我也没底。系统官网、网上教程东一榔头西一棒子真正能从头到尾跑通的少。很多教程默认你用的是 Ubuntu但麒麟系统哪怕底层再像 Ubuntu实际命令行、依赖库、权限控制、包管理仓库这些细节还是会坑你一把。这篇文章不整虚的把我从零开始部署 .NET Core Web 网站从 6.0 到 8.0到银河麒麟 V10 服务器的完整过程梳理下来包括遇到过的报错、排查思路、最终配置直接给你一份可以照着敲命令的实战笔记。适合谁看一是有国产化服务器部署需求的开发或运维同学二是单位要求上麒麟但自己对 Linux 生态没那么熟的朋友。只要你的服务器能联网或者哪怕完全离线这篇都能帮你把流程跑通。1. 麒麟服务器环境的版本识别桌面版和服务器版别搞混1.1 银河麒麟V10的版本差异银河麒麟 V10 这个版本号下面其实分了好几套体系。最常见的是基于 Ubuntu 内核的桌面版和基于 CentOS/RHEL 内核的服务器版。这两套系统的包管理命令、系统目录结构、默认防火墙工具都不一样。我之前接过一个项目客户说是银河麒麟 V10 服务器结果我拿 Ubuntu 那套 apt 命令去装包直接提示 command not found。后来一查系统实际是 CentOS 底子的服务器版得用 yum 或者 dnf。所以拿到机器第一件事就是要确认自己手上到底是什么版本。命令也简单# 查看发行版信息 cat /etc/os-release # 查看内核版本 uname -a # 查看系统架构 uname -m银河麒麟 V10 的 /etc/os-release 里一般会显示 Kylin Linux Advanced Server release V10 或者类似的名称。如果是这种你就按 CentOS 系的思路走。如果是 Kylin Desktop再配合 apt 源那就是 Debian 系。这里还有个小坑有些麒麟系统是飞腾、鲲鹏这类 ARM 架构的 CPU有些是 x86_64 的海光、兆芯 CPU。这两种架构在下载 .NET 运行时的时候必须严格区分下载错了装不上装上了跑不起来都是血泪教训。1.2 部署前需要摸底的系统信息清单建议部署前先把下面这些信息记录清楚后面所有配置都会用到项目确认命令部署影响系统版本cat /etc/os-release决定用 yum 还是 apt架构uname -m决定下载 x64 还是 arm64 运行时CPU 核数nproc调并发参数参考内存free -h决定是否要开 swap防止 OOM磁盘空间df -h发布文件和日志空间预留网络策略curl -I https://dotnet.microsoft.com 或 ping 测试判断在线安装还是离线部署当时我手头那台机器内存只有 2G部署 .NET 8 的应用以后没开 swap 的情况下跑起来特别吃力内存一高就被操作系统杀掉进程。后来加了 2G swap 才稳定。另外再说一点如果在生产环境部署建议先检查一下当前系统有没有启用 SELinux。麒麟服务器版默认可能是强制模式这会影响 Nginx 反代 .NET 进程时的权限。# 查看 SELinux 状态 getenforce如果是 Enforcing后面配 Nginx 的时候会麻烦一些。但好多教程压根不提这个我那次 502 Bad Gateway 查了半天最后定位到是 SELinux 拦截了 Nginx 去访问本机高位端口的 socket。这个咱们后面专门展开。2. 下载与安装 .NET Core Runtime架构选对是第一步2.1 Runtime、SDK、Hosting Bundle 到底该装哪个这是新手最容易迷糊的地方。简单说SDK用来在服务器上编译项目的包含编译器、MSBuild 等全套工具。生产环境如果只是运行发布好的程序装 SDK 属于杀鸡用牛刀还白白占盘。Runtime只负责运行已经编译好的 DLL体积小生产环境够用。Hosting Bundle是给 IIS 用的托管包在 Linux 上对应的是 ASP.NET Core Module不过在纯 Kestrel 部署场景下我们只需要 ASP.NET Core Runtime 就够了。如果你在服务器上既想跑发布后的程序偶尔还想用 dotnet 命令执行一些小工具那就装 ASP.NET Core Runtime 8.0.x 或 6.0.x。它是带 ASP.NET Core 支持的最小运行时集。以 .NET 8 为例微软官方下载页提供的 Linux 包有这些类型aspnetcore-runtime-8.0.x-linux-x64.tar.gz aspnetcore-runtime-8.0.x-linux-arm64.tar.gz dotnet-runtime-8.0.x-linux-x64.tar.gz dotnet-sdk-8.0.x-linux-x64.tar.gz我们用的就是 aspnetcore-runtime 那个包因为它包含了 Web 应用所需的组件。纯 console 程序才用得上 dotnet-runtime。2.2 在线安装配置微软源适用于能联网的麒麟如果你的麒麟服务器能正常访问外网最简单的方式是直接用微软提供的 apt/yum 源。CentOS 系的麒麟服务器版先用 rpm 导入微软的源sudo rpm -Uvh https://packages.microsoft.com/config/centos/8/packages-microsoft-prod.rpm但要注意麒麟服务器版的仓库地址可能跟 CentOS 8 有差异有时候 rpm 包导入不成功。我遇到的情况是 rpm 导入报了个依赖错误后来干脆绕过这个方式直接下载 tar.gz 包解压。如果用的是基于 Ubuntu 的麒麟桌面版倒是可以这样配源wget https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb sudo dpkg -i packages-microsoft-prod.deb sudo apt update sudo apt install -y aspnetcore-runtime-8.0但这个方式有个前提packages.microsoft.com在你的网络环境下能通。国产化环境里经常有这种情况公网仓库被白名单限制死了这时候就直接用离线部署更省心。2.3 离线安装解压即用是最省心的方式离线和内网环境我强烈推荐用 tar.gz 包这也是我最终采用的方案。操作不复杂关键点是把解压目录统一管理好。先把包下载到一台能联网的机器上然后传到目标服务器。我这里以 .NET 8.0 的 Linux x64 版本为例# 1. 创建目录 sudo mkdir -p /usr/local/dotnet # 2. 解压到指定目录 sudo tar zxf aspnetcore-runtime-8.0.x-linux-x64.tar.gz -C /usr/local/dotnet # 3. 创建软链接方便各用户使用 sudo ln -s /usr/local/dotnet/dotnet /usr/bin/dotnet # 4. 验证版本 dotnet --info这里有几个细节点tar 解压的时候包内文件默认会带一定的属主信息建议解压完以后执行chown -R root:root /usr/local/dotnet统一属主免得后面 systemd 启动服务时报权限错误。软链接/usr/bin/dotnet这一步很关键。虽然你也可以把/usr/local/dotnet加进 PATH 变量但生产环境多用户登录的情况下全局软链接最不容易出错。还有一个坑是DOTNET_ROOT环境变量。有些程序在运行的时候会去读这个变量不设的话可能报找不到 runtime之类的错。建议在/etc/profile.d/dotnet.sh里写入export DOTNET_ROOT/usr/local/dotnet export PATH$PATH:/usr/local/dotnet然后source /etc/profile.d/dotnet.sh使其生效。这一步做完了dotnet --info才能在任何用户下直接调用。2.4 用 dotnet-install.sh 脚本的补充方案除了手动解压微软也提供了官方安装脚本dotnet-install.sh。这个脚本适合你想在用户目录下安装对应版本运行时的情况而且能指定--runtime aspnetcore --version 8.0直接装 ASP.NET Core Runtime。wget https://dot.net/v1/dotnet-install.sh chmod x dotnet-install.sh ./dotnet-install.sh --runtime aspnetcore --version 8.0.0 --install-dir /usr/local/dotnet这个脚本会帮你去下载对应版本的 tar 包并解压本质上跟我手动操作一样但对很多不喜欢跟压缩包打交道的人来说一条命令更省事。不过需要注意这个脚本存在一个逻辑陷阱如果服务器设置了 http_proxy脚本下载失败后不会自动重试容易卡住。离线环境下不建议用这条方案直接拷包解压最稳妥。3. 安装后的环境验证三个命令确认基础可用3.1 验证 Runtime 是否正常装完以后不要急着发布网站先跑几个基础命令确认环境是ok的。dotnet --list-runtimes dotnet --info看到类似这样的输出就说明 Runtime 装好了Microsoft.AspNetCore.App 8.0.5 [/usr/local/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 8.0.5 [/usr/local/dotnet/shared/Microsoft.NETCore.App]这里你需要注意两个组件是否同时存在。ASP.NET Core 应用同时依赖Microsoft.NETCore.App和Microsoft.AspNetCore.App缺一个都不行。我见过一份部署记录只安装了 dotnet-runtime 包结果启动 Web 项目时报It was not possible to find any compatible framework version The framework Microsoft.AspNetCore.App, version 8.0.0 was not found.所以再次强调Web 网站项目必须装aspnetcore-runtime而不是那个基础的dotnet-runtime。3.2 依赖库缺失的坑libicu 和 opensslLinux 的 dotnet 运行还需要 ICU 库和 OpenSSL。麒麟系统一般都会有 OpenSSL但 libicu 不一定齐全。有一次我在一台最小化安装的麒麟服务器上执行dotnet --version直接报了一堆 LD 错误大概长这样error while loading shared libraries: libicuuc.so.XX: cannot open shared object file解决方案很简单CentOS 系的安装 libicu 包。麒麟桌面版可以 apt 装sudo apt install -y libicu-devCentOS 系的服务器版sudo yum install -y libicu还有一个更轻量的绕行方案直接把 .NET 设置为 invariant 模式让程序不使用全球化功能。在环境变量里加上export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT1但我不建议在中文网站场景下这么做因为排序、日期格式化、区域设置都可能受影响。除非你那个程序对全球化完全不敏感否则还是老老实实装 ICU。3.3 把 Hello World 先跑起来环境确认没问题之后顺手验证一下运行能力创建一个临时控制台程序测试mkdir /tmp/dotnet-test cd /tmp/dotnet-test dotnet new console dotnet run如果输出 Hello World那 Runtime 基本上没问题了。如果是带 Web 功能的验证可以用dotnet new web创建最小 Web 项目dotnet new web dotnet run --urls http://0.0.0.0:5000然后在另一台机器上访问http://服务器IP:5000能看到页面就说明 Kestrel 已经正常监听了。这一步能提前排除很多问题别急着把正式项目传上去先让环境自证清白。4. 发布 Web 应用从开发机到服务器的文件搬运4.1 发布方式框架依赖还是自包含开发机上发布 .NET Core Web 应用时你有两个方向可选。框架依赖发布framework-dependent体积小只有你项目编译出来的 DLL 和静态文件运行依赖服务器上已经装好的 ASP.NET Core Runtime。dotnet publish -c Release -o ./publish自包含发布self-contained会把整个 .NET 运行时都打进去服务器上可以不装 Runtime。发布命令里要指定 RIDdotnet publish -c Release -r linux-x64 --self-contained true -o ./publish自包含发布的好处是跟服务器环境彻底解耦不管服务器上的运行时什么版本都不影响。坏处是发布目录很大可能几百 MB而且每次更新应用都要重新传大量文件。生产环境我更推荐框架依赖发布。理由很简单既然你已经在麒麟服务器上装了 Runtime就没必要折腾自包含。除非你需要在多个版本的 Runtime 之间切换或者服务器不允许你装额外软件那才考虑自包含。4.2 发布时别忘了指定目标运行时如果你在用 .NET 8 或更高版本跨平台发布时建议显式指定 RID避免生成通用 RID 的兼容包dotnet publish -c Release -r linux-x64 --self-contained false -o ./publish指定了-r linux-x64后生成的 DLL 在目标机器上不会去探测其他平台的依赖启动也更快一点。不过要注意框架依赖发布时需要确保目标机器上安装的 ASP.NET Core Runtime 版本不低于你编译时使用的 TargetFramework 版本。比如你项目目标是 net8.0服务器上就要有 8.0.x 的 runtime8.0.0 也不行小版本最好高于等于编译时使用的 SDK 版本。这里还有个大坑如果你的开发机是 Windows发布时命令行里指定的-r linux-x64不会自动转换路径分隔符相关配置个别 NuGet 包里的 native 文件比如某些 CLR 扩展库可能会带 win-x64 的 RID 策略导致发布出来的目录里混入了 Windows 版的 dll。最稳妥的做法是开发机上就使用dotnet publish -r linux-x64之后检查publish/目录下有没有.dll和.so混在一起的不合理现象。4.3 上传到服务器scp 或 WinSCP文件发布好以后上传工具可以选择 scp 命令行也可以用图形化的 WinSCP、FileZilla 等。scp -r ./publish/* root服务器IP:/data/wwwroot/myapp/如果你对 scp 不熟或者觉得慢也可以用 rsync它对增量传输支持得更好rsync -avz ./publish/ root服务器IP:/data/wwwroot/myapp/上传完成后检查一下目录属主。建议专门建一个非 root 用户来运行应用比如 www 用户避免用 root 直接跑 web 服务这也是基本的运维安全习惯。sudo useradd -r -s /sbin/nologin www sudo chown -R www:www /data/wwwroot/myapp目录规划上我习惯把网站文件、日志、临时文件都分开/data/wwwroot/myapp/ # 程序发布目录 /data/logs/myapp/ # 应用日志 /tmp/myapp/ # 临时目录可选后面 systemd 服务文件里需要用到这些路径先定下来比较好。5. systemd 服务托管让网站脱离终端独立运行5.1 为什么不用 nohup 直接跑第一次部署的人可能想着直接dotnet MyApp.dll然后 nohup 放后台这种做法在测试环境临时验证没问题但生产环境非常不建议。因为 nohup 启动的进程不受系统服务管理崩溃了不会自动拉起也不能设置开机自启日志管理也很随意。正确的做法是把它封装成一个 systemd 服务。你只需要写一个.service文件然后 systemd 就能在开机时自动启动、崩溃时自动重启、统一收集 stdout/stderr 日志。5.2 写一个能直接用的 service 文件用 vim 创建服务文件sudo vim /etc/systemd/system/myapp.service我常用的配置长这样[Unit] DescriptionMy ASP.NET Core Web Application Afternetwork.target [Service] Typesimple Userwww Groupwww WorkingDirectory/data/wwwroot/myapp ExecStart/usr/local/dotnet/dotnet /data/wwwroot/myapp/MyApp.dll Restarton-failure RestartSec10 EnvironmentASPNETCORE_ENVIRONMENTProduction EnvironmentDOTNET_ROOT/usr/local/dotnet EnvironmentASPNETCORE_URLShttp://0.0.0.0:5000 SyslogIdentifiermyapp [Install] WantedBymulti-user.target逐行解释几个关键项Typesimple表示 ExecStart 启动的进程是主进程不需要 fork这是最常用的设置。Restarton-failure非正常退出时自动重启注意别设成 always否则手动 stop 之后还会被拉起来。RestartSec10挂了 10 秒后重试避免不断重启把 CPU 打满。ASPNETCORE_URLShttp://0.0.0.0:5000让 Kestrel 监听所有网卡 5000 端口。如果你服务器有内网 IP 和外网 IP也可以直接指定http://内网IP:5000更安全。SyslogIdentifiermyappjournald 日志索引器方便用journalctl -u myapp查日志。5.3 服务启动、开机自启与日志查看写完后依次执行命令# 重新加载 systemd 配置 sudo systemctl daemon-reload # 启动服务 sudo systemctl start myapp # 查看运行状态 sudo systemctl status myapp # 设置开机自启 sudo systemctl enable myapp # 查看最近日志 sudo journalctl -u myapp -n 50 --no-pager如果状态显示 running 且没有任何红色报错基本就成了。如果显示 failed先看日志sudo journalctl -u myapp -f日志里最常见的问题就是路径配错、权限不足、端口被占用。权限不足的典型错误是Failed to bind to address http://0.0.0.0:5000: address already in use. Unhandled exception. System.UnauthorizedAccessException: Access to the path /data/wwwroot/myapp is denied.第一个报错看端口第二个报错查属主。还记得前面chown -R www:www /data/wwwroot/myapp那步吗你没做这一步的话www 用户根本没权限读程序文件服务必挂。还有一个容易忽略的点如果程序里写了写文件的逻辑比如上传图片、生成日志文件那么/data/wwwroot/myapp下面有对应子目录时也要给 www 用户写权限。别到时候查半天不知道为啥一直 500。6. Nginx 反向代理配置让网站通过 80/443 对外提供服务6.1 为什么要把 80 端口交给 NginxKestrel 本身是可以直接对外监听 HTTP 的但生产环境里我们往往需要同时部署多个站点、要做静态文件压缩、要配 HTTPS、要统一日志这些功能 Nginx 处理效率更高。所以常规架构是用户请求 - Nginx (80/443) - Kestrel (127.0.0.1:5000) - .NET Core 应用这个架构下Kestrel 只需要监听内网回环地址 127.0.0.1Nginx 监听公网端口。这样暴露在外部的是 Nginx即使 Kestrel 出了什么安全问题攻击面也小一些。6.2 Nginx 安装与基础配置先安装 Nginx。CentOS 系的麒麟服务器版sudo yum install -y nginxUbuntu 系的麒麟桌面版sudo apt install -y nginx装完以后在/etc/nginx/conf.d/目录下新建一个配置文件myapp.confserver { listen 80; server_name your_domain_or_ip; # 转发到 Kestrel location / { proxy_pass http://127.0.0.1:5000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection keep-alive; proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # 静态资源直接走 Nginx可选 location /static/ { alias /data/wwwroot/myapp/wwwroot/; expires 7d; } }配置里proxy_set_header Connection keep-alive;很重要因为 .NET Core 的 Kestrel 默认长连接如果这里不设置可能会导致 WebSocket 或者某些流式响应异常。配好后检查语法并重载sudo nginx -t sudo systemctl reload nginx这时在浏览器访问http://你的服务器IP应该能通过 Nginx 看到网站了。6.3 SELinux 开启时的权限问题这一步是我踩过最深的一个坑。在麒麟服务器版的默认环境下SELinux 可能是 Enforcing 模式。Nginx 启动没问题但访问的时候一直 502 Bad GatewayNginx 错误日志里能看到connect() failed (13: Permission denied) while connecting to upstream原因就是 SELinux 默认禁止 Nginx 去访问宿主机上非标准端口5000的 socket。网上很多人让直接关 SELinuxsudo setenforce 0这样确实能解决但生产环境关 SELinux 等于自己去掉了系统一层安全防护。我更推荐用 semanage 命令把 5000 端口标记为 httpd 可以访问的类型sudo yum install -y policycoreutils-python-utils sudo semanage port -a -t http_port_t -p tcp 5000这样 Nginx 就可以代理到 5000 端口了SELinux 依然保持 Enforcing。如果你开发测试的环境里sudo setenforce 0只是临时放宽重启之后又恢复了。要永久修改就要改/etc/selinux/config文件。个人还是建议不要永久关 SELinux除非服务器在纯内网隔离环境里。6.4 防火墙放行端口麒麟系统默认开启 firewalld 的话80/443 端口是需要放行的sudo firewall-cmd --permanent --add-port80/tcp sudo firewall-cmd --permanent --add-port443/tcp sudo firewall-cmd --reload如果用的是 ufwsudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw reload另外要注意云服务器上面的安全组策略。有时候你本地怎么测都通浏览器一访问就超时多半是云厂商控制台的安全组没有放行对应端口。这个跟系统防火墙没有关系很容易被忽略。7. 部署完成后的验证与日常维护别让网站悄悄挂掉7.1 完整验证流程部署完成后我一般按这个顺序验证本机 curl 测试 Kestrel 是否在监听curl http://127.0.0.1:5000/health本机 curl 测试 Nginx 是否转发正常curl http://127.0.0.1/浏览器访问公网 IP 或域名确认页面加载正常。查看 systemd 服务状态确认没有异常重启记录systemctl status myapp确认开机自启已生效systemctl is-enabled myapp我一直建议项目里至少加一个健康检查的 Controller 或者 Minimal API 接口。不需要复杂逻辑就返回一个 JSON 状态码。这样排查挂没挂、探活、接入监控系统都非常方便。7.2 日志轮转与磁盘空间.NET Core 应用如果自己写日志很容易把磁盘塞满。推荐在项目里用 Serilog配置 RollingFile 按天切割。服务器层面也可以配置 logrotate 管理 Nginx 日志。如果你发现网站突然变慢或者直接挂掉第一件事查磁盘df -h我就是有次 /var 分区被 journal 日志占满整个系统卡到无法 ssh最后只能重启进入单用户模式清理日志。生产环境务必装一个磁盘监控工具挂载点剩余空间低于 10% 就要告警。7.3 进程没挂但网站 500 的排查思路有时候 systemctl status 显示服务正常运行但页面 500。这种时候先看应用自身日志sudo journalctl -u myapp --since 30 minutes ago --no-pager如果是数据库连接超时检查数据库服务是否正常、连接字符串指向的 IP/端口是否通。如果是 redis 连接失败看看 redis 是否还需要密码验证。还有一种比较隐蔽的情况Kestrel 端口监听了 IPv6 的::1但 Nginx 里 proxy_pass 写的是http://127.0.0.1:5000两边不一致导致 502。排查方式用这两个命令对比ss -tlnp | grep 5000 curl -v http://[::1]:5000/health7.4 更新发布的正确姿势发布新版本时最怕的就是发布过程中文件正在被占用导致失败。因为 systemd 服务还在运行旧版本的 DLL 可能被进程锁定。我建议更新的标准流程是# 1. 停止服务 sudo systemctl stop myapp # 2. 备份当前版本可选 sudo mv /data/wwwroot/myapp /data/wwwroot/myapp_bak_日期 # 3. 上传新版本到目录 # 用 rsync 上传或者直接解压覆盖 # 4. 修改属主 sudo chown -R www:www /data/wwwroot/myapp # 5. 启动服务 sudo systemctl start myapp # 6. 验证页面 curl http://127.0.0.1:5000/health千万不要在大白天直接覆盖 DLL除非你确定系统里没有文件锁否则很容易让站点挂到一半。用 systemd 的 stop 再 start比 kill 进程再 nohup 重建要干净利落得多。8. 部署过程中常见坑位总结我把踩过的都列出来现象可能原因排查方向dotnet: command not found环境变量未生效或软链接没建ls -l /usr/bin/dotnetecho $PATHIt was not possible to find any compatible framework version只装了 runtime 没装 aspnetcore runtimedotnet --list-runtimes检查libicu 相关报错ICU 库缺失安装 libicu 或设置 invariantFailed to bind address already in use端口被其他进程占用ss -tlnp看对应端口Access to the path is denied目录属主不对ls -ld检查chown 给 wwwNginx 502 Bad GatewaySELinux 开端口未被标记semanage 添加端口Nginx 502 Bad GatewaySELinux 关Kestrel 没启动或端口不对curl 127.0.0.1:5000外网无法访问但本机 curl 正常云安全组/防火墙未放行检查安全组规则和 firewalld进程 OOM 被杀内存不足free -h加 swap国产化服务器上跑 .NET Core 或者说 .NET 本身总体体验是越往后越顺畅的。从 .NET Core 3.1 开始Linux 支持就已经很成熟到了 .NET 6/8跨平台的坑明显少了很多。大部分问题反而出在系统层面版本识别错误、架构不匹配、SELinux 策略、防火墙规则这些属于运维知识不是 .NET 本身的问题。另外有个小技巧想分享给经常跟国产化系统打交道的人如果你们手上有多台不同架构的服务器建议把 dotnet runtime 的 tar 包、nginx 的 rpm 包、常用的依赖库都提前下载好放到一个内网文件服务器上做一个简单的小仓库。这样后面再遇到一台新机器整个环境搭建过程可以压缩到十分钟以内。我第一次弄这个环境折腾了两个小时后面重复操作基本就是复制粘贴。最后再给一条建议部署完顺手把dotnet --info、systemctl status myapp、nginx -t的完整输出存成一个文本文件跟部署文档放一起。下次排查问题或者换人接手的时候这些记录能省掉一大半的猜谜时间。
返回列表