干这行久了就会碰到各种“页面看起来正常,但一部署就翻车”的场景。最近在帮客户做国产化环境迁移,目标机器是一台银河麒麟V10服务器,任务是在上面把.NET Core环境装好,再把Web网站跑起来。听起来很简单?等真正动起手来,你会发现“麒麟”并不是某一个单一的发行版,它既有基于Debian体系的,也有基于CentOS体系的,CPU架构从x86到飞腾、鲲鹏都有,稍不留神就会在第一步选错安装包。
这篇文章就把我这次在麒麟服务器上安装.NET Core环境并发布Web网站的完整过程写下来。内容覆盖场景选型、离线安装、发布部署、systemd托管、反向代理,以及我在现场遇到的一堆坑。如果你也在做国产化交付、内网部署,或者只是想把.NET Web应用挪到Linux服务器上,这份记录应该能帮你少走很多弯路。
1. 动手前先把底层情况摸清楚
1.1 为什么要在麒麟服务器上跑.NET Core
首先要搞清楚一个现实:很多政企单位、传统行业客户的服务器环境已经切换到了国产操作系统。而麒麟(银河麒麟、中标麒麟)是目前最常见的国产服务器系统之一。可问题是,业务系统不可能一夜之间全部重写,很多存量应用是用C#、.NET技术栈开发的Web系统,这时候就必须在麒麟上把.NET Core运行时跑起来。
另外,.NET Core从3.1开始就是跨平台的,不只是可以在Windows上跑,Linux、macOS都可以。我们平时开发用的ASP.NET Core Web应用,发布之后就是一堆dll文件加上一个可执行文件,只要目标Linux服务器上有正确的.NET运行时,就能像跑Java的jar包一样把它跑起来。这个特性在国产化迁移里特别管用,不用重写业务,只需要把发布的文件装到新环境就能用。
1.2 先查版本和CPU架构,这一步错后面全错
我见过太多人一上来就直接下载安装包,结果解压出来执行时报“Exec format error”,搞了半天才发现是CPU架构不匹配。麒麟V10服务器版在真实环境里跑得非常杂,x86的Intel、AMD,还有海光、兆芯,ARM的飞腾、鲲鹏也都有。不同CPU对应不同的.NET运行时包,一旦下错,连启动都启动不了。
上服务器第一件事就是执行这两条命令:
hostnamectl uname -mhostnamectl能看到操作系统版本,uname -m看CPU架构,x86_64就是x86 64位,aarch64就是ARM 64位。拿我这次部署的机器来说,系统显示是“Kylin V10”,架构是aarch64,那就意味着所有安装包都要选linux-arm64版本,而不是linux-x64版本。
还要顺便确认一下系统默认的软件包管理工具。麒麟V10有两大分支,基于Ubuntu/Debian的用apt,基于CentOS/RHEL的用yum或dnf。这个会影响到后面安装依赖库时用什么命令,也影响到能不能直接用yum装一些基础组件。
1.3 服务器端装SDK还是装Runtime,别搞混了
很多刚接触Linux部署的同学会把Windows上的经验带进来,在服务器上把完整的.NET SDK一装,甚至去找“Hosting Bundle”。这个习惯在Windows上问题不大,但在Linux上建议分清场景:
- 如果服务器负责跑应用,不需要在本机编译代码,那么装Runtime就够了;
- 如果需要在服务器上执行dotnet build、发布操作,或者跑一些需要SDK的命令,那就装完整SDK;
- “Hosting Bundle”是Windows上ASP.NET Core托管IIS用的概念,Linux上对应的其实是ASP.NET Core Runtime。
我这次因为有现场调试需求,也需要偶尔在服务器上重新发布一下,所以选了完整SDK。如果你们公司的发布流程是“开发机上编译好再传包”,生产服务器完全可以只装ASP.NET Core Runtime,体积小很多,占用更少,安全面也更小。
| 安装包类型 | 用途 | 适用场景 |
|---|---|---|
| .NET SDK | 包含编译器、CLI、运行时 | 需要在本机编译、发布、调试 |
| .NET Runtime | 仅运行命令行的dll | 只跑控制台程序或后台任务 |
| ASP.NET Core Runtime | 运行Web应用所需 | 服务器跑Web网站时选它 |
2. 在麒麟服务器上安装.NET Core运行时
2.1 离线环境怎么准备安装包
客户的内网服务器通常没有外网,或者外网卡在安全策略后面,基本没法直接使用微软的在线安装脚本。最稳妥的办法是提前在一台能访问外网的机器上把安装包下载好,用U盘或内网传输工具拷到服务器上。
下载安装包时认准两个地方:
- 微软官方 .NET 下载页面;
- dotnet.microsoft.com 上对应的发行版本页面。
文件命名里会区分版本、架构、运行时类型。比如我这次下载的文件名类似:
dotnet-sdk-6.0.428-linux-arm64.tar.gz如果你只要运行时,对应的就是:
aspnetcore-runtime-6.0.36-linux-arm64.tar.gz注意这里有个比较容易踩的坑:ASP.NET Core Runtime和.NET Runtime是两回事。如果你发布的网站是ASP.NET Core MVC或者Web API,只装一个纯.NET Runtime是不够的,启动时会直接报缺少程序集。所以生产服务器上至少装ASP.NET Core Runtime,或者干脆像我一样装完整SDK,一步到位省心。
2.2 解压安装和环境变量配置
麒麟系统上没什么特殊的安装器,微软官方提供的tar.gz包就是绿色版,解压后就能用。我习惯把全部.NET相关文件放到一个统一目录下,比如/usr/local/dotnet,方便以后升级和管理。
把安装包上传到服务器后,执行以下几步:
sudo mkdir -p /usr/local/dotnet sudo tar zxf dotnet-sdk-6.0.428-linux-arm64.tar.gz -C /usr/local/dotnet sudo ln -s /usr/local/dotnet/dotnet /usr/bin/dotnet最后一步做软链接,是为了任何用户状态下都能直接敲dotnet命令,不需要每次手动添加PATH。
如果你不想做软链接,也可以在 /etc/profile 里追加环境变量:
export PATH=$PATH:/usr/local/dotnet改完记得执行 source /etc/profile 让配置生效。
2.3 验证安装是否成功
命里安装完第一时间验证:
dotnet --info dotnet --list-runtimesdotnet --info会输出完整的运行时和SDK信息。如果能看到类似:
.NET SDKs installed: 6.0.428 [/usr/local/dotnet/sdk] .NET runtimes installed: Microsoft.AspNetCore.App 6.0.36 [/usr/local/dotnet/shared] Microsoft.NETCore.App 6.0.36 [/usr/local/dotnet/shared]说明环境基本就绪。这里也要提醒一下:有些麒麟服务器为了精简,可能缺少一些底层依赖库,比如ICU库、OpenSSL、libstdc++等。如果启动时报找不到libicu或libssl,就需要用apt或yum把依赖补上:
# Debian/Ubuntu系的麒麟 sudo apt-get update sudo apt-get install -y libicu-dev libssl-dev # CentOS系的麒麟 sudo yum install -y icu openssl2.4 一个容易被忽略的坑:权限和目录规划
我曾经在一台服务器上遇到dotnet命令可以执行,但发布的应用目录没有读权限,结果systemd里启动一直失败的现象。排查了很久,最后才发现是目录属主不对。
建议把网站目录规划成独立用户,例如创建一个www用户,专门用于运行Web应用。目录结构可以参考:
/var/www/mycms/ # 存放发布文件 /var/log/mycms/ # 存放日志创建用户和授权:
sudo useradd -r -s /sbin/nologin www sudo mkdir -p /var/www/mycms sudo chown -R www:www /var/www/mycms这么做的好处是应用以最小权限运行,即便被攻破也不能直接影响系统其他目录。很多老手部署时经常跳过这步,直接拿root用户跑应用,现场确实方便了,但安全上非常不值得提倡。
3. 发布Web网站并把文件部署到服务器
3.1 开发机上执行发布命令
如果是开发机发布,我建议用命令行发布,而不是直接复制项目文件夹。VS的“发布”功能本质和命令行一样,但命令行更透明,能看到每个参数。
在项目根目录执行:
dotnet publish -c Release -o /tmp/publish如果项目引用了运行时标识,可以在命令里指定:
dotnet publish -c Release -r linux-arm64 --self-contained false -o /tmp/publish这里有两个参数需要注意:
- -r 指定目标运行时,如果服务器是ARM就写linux-arm64,是x86就写linux-x64;
- --self-contained 为true时,会把整套.NET运行时一起打进去,服务器上不需要预装Runtime;为false时,服务器上必须手动装对应的运行时。
依赖框架和自带运行时没有绝对好坏。自带运行时部署更简单,但发布体积大几十MB,每次更新都要重新上传整个包;依赖框架则要求服务器环境保持一致。
3.2 上传发布文件到麒麟服务器
发布完成之后,/tmp/publish下面就是整个Web应用的“成品”:一堆dll、json配置文件、wwwroot静态资源,可能还有一个可执行文件。把这些文件整体传到服务器上:
scp -r /tmp/publish user@server-ip:/var/www/mycms/在内网环境没有scp可用时,也可以打包后通过U盘拷贝:
tar czf myapp.tar.gz -C /tmp publish到服务器端解压:
sudo mkdir -p /var/www/mycms sudo tar zxf myapp.tar.gz -C /var/www/mycms --strip-components=1 sudo chown -R www:www /var/www/mycms3.3 先手动运行一次,排查问题再托管
很多新手喜欢直接把服务配好systemd再启动,结果一启动就失败,日志也看不出所以然。我的习惯是先手动跑一下,确认应用本身没问题,再交给systemd托管。
先看一下程序集名称。通常发布目录下的dll名称和项目名称一致。
sudo -u www ASPNETCORE_ENVIRONMENT=Production dotnet /var/www/mycms/MyCms.dll如果看到类似:
info: Microsoft.Hosting.Lifetime[14] Now listening on: http://localhost:5000 info: Microsoft.Hosting.Lifetime[0] Application started. Press Ctrl+C to shut down.说明应用起来没问题。这时在本地执行:
curl http://localhost:5000能看到页面代码,就表示Web服务已经正常响应。
如果没反应,先检查端口占用情况:
ss -tlnp | grep 5000也可能是应用启动后崩了,需要看控制台输出的异常信息。这一步手动运行的价值就在这里,错误信息直接打印在终端上,不用去翻日志。
3.4 用systemd把网站托管起来,开机自启
手动运行只能证明应用能跑,但服务器一重启或者SSH一断开,网站就没了。生产环境肯定要用systemd托管。
编辑服务文件:
sudo vim /etc/systemd/system/mycms.service内容如下:
[Unit] Description=My .NET Core Web Application After=network.target [Service] Type=simple User=www Group=www WorkingDirectory=/var/www/mycms ExecStart=/usr/local/dotnet/dotnet /var/www/mycms/MyCms.dll Restart=always RestartSec=10 KillSignal=SIGINT SyslogIdentifier=mycms Environment=ASPNETCORE_ENVIRONMENT=Production Environment=ASPNETCORE_URLS=http://0.0.0.0:5000 [Install] WantedBy=multi-user.target这里重点解释几个关键参数:
- User/Group:以www身份运行,不是root;
- WorkingDirectory:工作目录必须指向发布目录,很多配置读取、日志写入都以这个目录为基准;
- Restart=always:进程崩溃后自动重启,这个是生产环境必须的;
- ASPNETCORE_URLS:默认Kestrel监听的是localhost:5000,外网没法直接访问,所以这里设置成0.0.0.0:5000,让所有网卡都能访问;
- KillSignal=SIGINT:ASP.NET Core有优雅停机处理,收到SIGINT会先处理完正在进行的请求再退出,比直接SIGKILL安全。
配置好之后执行:
sudo systemctl daemon-reload sudo systemctl enable mycms.service sudo systemctl start mycms.service sudo systemctl status mycms.service看到Active: active (running)就说明托管成功。以后查看日志用:
journalctl -u mycms.service -f3.5 配置Nginx反向代理,一个服务器部署多个Web项目
如果直接让Kestrel监听80端口,也不是不能用,但Kestrel对静态文件、压缩、并发连接的优化不如成熟Web服务器,而且一个服务器上往往要跑多个Web项目,全都占着80端口显然不行。
常规做法是Nginx监听80端口,按域名把请求转发给不同的Kestrel端口。比如项目A跑在5000,项目B跑在5001。
安装Nginx:
# Debian系 sudo apt-get install -y nginx # CentOS系 sudo yum install -y nginx编辑站点配置:
sudo vim /etc/nginx/conf.d/mycms.conf配置内容:
server { listen 80; server_name your.domain.com; location / { proxy_pass http://127.0.0.1:5000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }改完后重载Nginx:
sudo nginx -t sudo systemctl reload nginx如果同一台服务器部署多个Web项目,就复制这个配置文件,改server_name和proxy_pass端口即可。这种方式比直接给每个端口开放防火墙更安全,外部用户只看到80端口,不会暴露内部端口。
4. 现场排障记录:这些问题最容易踩
4.1 下载错架构的安装包,服务启动不了
有一次我帮同事排查,服务器是飞腾ARM架构,但同事下载的是x64安装包。解压、配置PATH都正常,执行dotnet --list-runtimes也能看到输出,但一运行Web应用就报:
Exec format error后来用 file 检查了dotnet二进制文件:
file /usr/local/dotnet/dotnet果然显示是x86-64的ELF文件,而系统内核却是aarch64。解决办法也很粗暴,重新下载linux-arm64版本即可。所以前面反复强调uname -m检查架构是有原因的。
4.2 应用能启动,但外网访问不了
这种情况八成是Kestrel只绑定了localhost。手动运行时如果没设置ASPNETCORE_URLS,默认的监听地址就是localhost:5000,外部访问自然被拒绝。
解决方式有两个:
- 启动命令里带环境变量:ASPNETCORE_URLS=http://0.0.0.0:5000 dotnet xxx.dll
- 在appsettings.json里添加:
"Urls": "http://0.0.0.0:5000"另外还要检查防火墙。麒麟V10很多自带firewalld:
sudo firewall-cmd --list-all sudo firewall-cmd --add-port=5000/tcp --permanent sudo firewall-cmd --reload如果是内部部署,也可以临时关闭防火墙验证一下是不是这个原因,但生产环境不建议直接关闭防火墙。
4.3 提示缺少ICU或OpenSSL依赖
在精简安装的麒麟服务器上,安装完.NET之后执行dotnet命令,可能直接报找不到libicu或libssl。
这类报错信息比较直接,比如:
error while loading shared libraries: libicuuc.so.66可以通过下面的命令确认哪些库缺失:
ldd /usr/local/dotnet/dotnet | grep "not found"然后根据缺什么补什么:
sudo apt-get install -y libicu66 # 或 sudo yum install -y libicu有些新版本的系统里包名不同,可以用install命令后再用ldd确认一遍,直到没有not found为止。
4.4 systemd启动失败,但手动运行正常
这个现象特别迷惑人。手动执行dotnet xxx.dll时一切正常,但一配好systemd启动就失败,journalctl里显示Permission denied。
我遇到这种情况几乎都是因为目录权限。比如/var/www/mycms目录的所有者是root,而Systemd里指定的User=www没有读取权限。马上用chown授权:
sudo chown -R www:www /var/www/mycms还有一种是site目录挂载在特殊文件系统上(比如NFS),systemd默认启动顺序在挂载之前,导致启动时找不到目录。排查方法是在systemd服务文件里加上:
After=remote-fs.target或者直接把挂载写到fstab里确保开机先挂载。这类问题在现场非常典型。
4.5 80端口被占用,或者权限不够
很多人都想在80端口直接跑Kestrel,但有两个现实问题:
- 80端口可能被Nginx或者其他Web服务占用了;
- Linux下1024以下端口需要root权限,用www用户跑就会报权限不足。
最快确认端口占用情况:
ss -tlnp | grep :80如果确认被Nginx占用,那刚才配置反向代理的方案就是正解,让Nginx去监听80端口,Kestrel继续跑在高位端口上。这样既有性能,又绕开了权限问题,还方便多站点共用一个公网入口。
4.6 打开页面出现乱码或者静态资源404
乱码问题大概率是系统没有对应的中文字体,网页上的中文内容显示成方块。安装字体包可以解决:
sudo apt-get install -y fonts-wqy-zenhei fonts-wqy-microhei静态资源404则要检查发布目录下的wwwroot文件夹是否完整。有时候用scp或U盘拷贝发布包时漏掉了wwwroot,页面HTML能返回,但CSS、JS全部加载不了。直接看一下:
ls -l /var/www/mycms/wwwroot如果这个目录不存在,就得从开发机重新发布一份完整的包传上来。
5. 可能遇到的其他奇怪问题
实际部署不总是一帆风顺,这类奇怪问题也值得记录一下。
有些Web应用在页面里注册Service Worker时,会看到类似:
加载web视图时出错: error: could not register service worker: invalidstateerror这个和服务器环境关系不大,通常是浏览器安全策略或HTTPS证书的问题。检查一下站点是否启用了HTTPS,Service Worker要求在安全上下文下注册,如果只是http且没有证书,部分浏览器就会报这个。要么配好HTTPS证书,要么在开发环境临时把Service Worker注册逻辑屏蔽掉。
还有一次部署debug版的Web应用,页面提示需要认证,认证地址后面带了一个奇怪的token。后来发现是应用自身有一套Dsh认证机制,和.NET环境没关系,但第一次遇到确实容易让人误以为是环境装错了。遇到认证相关提示时,先看看是不是应用代码或配置里包含token认证逻辑,别盲目去重装环境。
另外提醒一下,有的麒麟系统默认开启了SELinux。如果Nginx反代之后始终504或者连不上后端,可以临时执行:
getenforce如果返回Enforcing,可以先用setenforce 0临时关闭测试。确认是SELinux拦截之后,再根据服务类型配置对应的布尔值或上下文策略。不要为了图省事长期关闭SELinux,否则系统安全性会明显下降。
还有的时候,服务器本身时间不对,会导致HTTPS证书校验失败。用date看一下系统时间,如果偏差太大,安装一下chrony或者ntp并同步时间:
sudo apt-get install -y chrony sudo systemctl enable --now chrony这种情况多见于刚装好的银河麒麟服务器,时间还停留在初始安装时的时间。
最后说一件容易被忽视的事:不要在晚上加班部署时才想起来新建用户和授权。整个过程中最花时间的是应用内部的配置和依赖问题,而不是.NET环境本身。提前规划目录、用户、端口和数据库连接串,现场真正安装的时间其实只要十几分钟。我在同型号的多台麒麟服务器上做过批量部署,流程固定之后,每台机器从解压到测试通过,基本在15分钟以内。
平常做完一个项目,我会顺手把用到的安装包版本、依赖库清单、发布配置、systemd服务文件一起存到本地一个脚本仓库里。下次遇到相似项目,只需要改项目名和端口就能直接复用,省下来的时间非常可观。