Alibaba Cloud Linux上部署.NET 8 Web应用:systemd+Nginx完整实战

发布时间:2026/10/3 5:54:25
Alibaba Cloud Linux上部署.NET 8 Web应用:systemd+Nginx完整实战
1. 为什么我把.NET 8装在这台Alibaba Cloud Linux上而不是用Windows服务器或容器先交代一下背景。我手上有一台阿拉云ECS实例系统镜像是Alibaba Cloud Linux 3.2104之前一直跑MySQL和Nginx这类的常规服务。最近要上一个.NET 8的ASP.NET Core Web API项目最初其实纠结过两条路一条是换成Windows Server然后用IIS托管另一条是把应用打成镜像丢到Docker里运行。最后我的选择是直接在Alibaba Cloud Linux原生环境上用systemd加上Nginx把.NET 8跑起来这套方案跑到现在非常稳定。为什么这么选核心在于.NET Core早就实现了跨平台ASP.NET Core应用在Linux上的性能表现甚至优于Windows/IIS的组合资源占用也更干净。Alibaba Cloud Linux作为阿里云自研的操作系统兼容RHEL/CentOS生态安装微软的.NET仓库没有障碍安全更新可以走dnf一条龙搞定。再加上云安全组配合firewalld管理端口整个链路不需要额外的商业授权费用。对我这种中小型项目而言直接裸跑反而比套一层容器更省事——少一个维护点排查问题也更直观。这套方案适合谁如果你也有一台公网服务器想跑.NET 8的Web应用或者正在纠结Linux能不能稳跑.NET项目要不要上Docker那这篇文章值得看完。我会把从零开始装SDK、发布项目、配置守护进程、挂上Nginx反向代理、套HTTPS证书的完整过程讲透每个环节不仅告诉你怎么做还会解释为什么这么做。最后一部分是排错笔记都是我实际踩过的坑可以直接当作排查手册用。2. 基础准备Alibaba Cloud Linux的底子要打牢装.NET 8之前先把系统本身收拾利索了。这一步看着基础但我见过太多人在半路卡住回过头来发现其实是最初的软件源或者内核小版本出了问题。2.1 确认系统版本与架构先登录服务器确认你手上的具体版本cat /etc/os-release uname -mAlibaba Cloud Linux 3默认是x86_64架构但如果你用的是ARM的倚天710实例那uname -m会显示aarch64。为什么这个信息重要后面装.NET镜像源时x86_64和aarch64用的是同一套微软RHEL源微软的源会根据系统架构自动匹配但万一你手动下载rpm包选错架构就直接装不上。另外ARM架构下某些依赖库的兼容性需要额外验证所以先确认架构能避免很多冤枉路。2.2 更新系统内核与基础工具接着把系统更新到当前最新状态sudo dnf update -y sudo dnf install -y wget curl vim tar rsync很多人会跳过这一步但Alibaba Cloud Linux 3的默认软件仓库里部分基础库版本偏旧直接装.NET大概率也能装上但如果触发依赖冲突处理起来比先更新一遍麻烦得多。特别是gcc、libstdc这类跟原生互操作有关的库版本太旧会直接影响某些NuGet包在Linux上的表现。我实测过先dnf update再装.NET SDK整个过程丝滑跳过后装偶尔会遇到需要手动解决依赖的情况。另外顺手装一下epel-release这样后续如果需要一些额外工具不用再折腾第三方源sudo dnf install -y epel-release2.3 时间同步与主机名设置日志排错时经常遇到一个问题容器或服务器日志时间跟本地差了8小时查问题全靠脑补时区。所以这一步千万别省sudo timedatectl set-timezone Asia/Shanghai sudo timedatectl set-ntp true sudo hostnamectl set-hostname dotnet-app-server时间同步走的是systemd-timesyncdAlibaba Cloud Linux 3默认开启。设好时区和主机名后面看journalctl日志时每一行时间都对得上排查问题的效率能高不少。3. 安装.NET 8 SDK与运行时从微软源到验证这一节是整篇文章最核心的部分。安装方式有两条路径我会把两条都讲清楚然后告诉你我推荐哪条以及为什么。3.1 路径一通过微软RHEL仓库安装推荐微软为RHEL系准备了专门的软件仓库Alibaba Cloud Linux 3兼容RHEL 9的包格式所以可以直接用sudo dnf install -y https://packages.microsoft.com/config/rhel/9/packages-microsoft-prod.rpm装完rpm之后系统会生成一个/etc/yum.repos.d/microsoft-prod.repo文件。这时候你可以直接装SDKsudo dnf install -y dotnet-sdk-8.0如果项目只是跑别人编译好的发布产物不需要本地编译那可以只装运行时体积小很多sudo dnf install -y aspnetcore-runtime-8.0SDK和Runtime的区别要搞清楚。SDK包含编译器、MSBuild、NuGet等一整套开发工具链主要用于本地build和publishRuntime只包含运行时组件服务器上跑发布产物足够了。我的建议是服务器上只装aspnetcore-runtime除非你需要在服务器上直接编译项目。装SDK会多占几百MB磁盘空间虽然不多但生产环境我倾向于精简。装完验证一下dotnet --info dotnet --list-runtimes正常输出里能看到Microsoft.AspNetCore.App 8.0.x和.NET Runtime 8.0.x说明安装成功。如果dotnet命令找不到检查一下shell是否重启过或者执行source /etc/profile重新加载环境变量。3.2 路径二dotnet-install脚本安装备用方案有些时候微软的RHEL仓库拉不下来或者你的系统版本跟仓库匹配有问题这时候可以用官方提供的安装脚本完全绕开系统包管理器curl -sSL https://dot.net/v1/dotnet-install.sh | bash -s -- --channel 8.0 --install-dir /usr/share/dotnet脚本默认装到用户目录我一般指定--install-dir放到系统目录方便所有用户使用。装完需要手动配环境变量cat /etc/profile.d/dotnet.sh EOF export PATH$PATH:/usr/share/dotnet export DOTNET_ROOT/usr/share/dotnet EOF source /etc/profile再验证dotnet --list-sdks能输出8.0.x版本就没问题。为什么还要保留这条备用路径因为生产环境我遇到过微软源临时抽风的情况某次dnf install一直报网络超时切到脚本安装十几秒就搞定。两条路都会走不是坏事。3.3 一个容易翻车的细节dnf缓存问题如果你在安装过程中碰到类似Error: Failed to synchronize cache for repo microsoft-prod的报错别慌这是dnf元数据缓存过期了sudo dnf clean all sudo dnf makecache然后重新安装。这个坑在刚配好源的时候特别常见因为微软源的元数据更新频率不算高dnf的本地缓存容易带着过期的索引去匹配版本。4. 项目发布与上传构建物、目录规划与权限设计SDK装好只是第一步真正决定项目能不能跑起来的是发布产物和服务器上的目录布局。这里分享一套我踩过几次坑之后固定的流程。4.1 发布模式选择FDD还是SCD在本地用dotnet publish发布项目时有两个选择Framework-dependent框架依赖和Self-contained自包含。两者区别是SCD会把.NET运行时一起打包进发布目录服务器上不用预装RuntimeFDD则需要服务器先装好对应的Runtime。我推荐FDD。原因有三服务器上已经装了RuntimeFDD的发布目录小上传快安全补丁更新更灵活Runtime升级后应用自动受益省磁盘一个SCD包动辄上百MBFDD通常只有几MB到几十MB。发布命令dotnet publish -c Release -o ./publish如果有多个环境配置比如测试环境和生产环境区分可以在发布前指定dotnet publish -c Release -o ./publish -p:EnvironmentNameProduction这个参数会把appsettings.Production.json一并打进发布产物同时把ASPNETCORE_ENVIRONMENT的默认值写进Web.config文件如果有生成的话。不过我们在Linux上用Nginx托管Web.config根本用不到后面会细说。4.2 上传方式rsync比scp好用发布产物准备好了接下来传到服务器。文件的传输我强烈建议用rsync而不是scp。rsync支持断点续传、增量同步、压缩传输对于一个几百MB的发布目录首次上传可能差异不大但后续更新项目时rsync只传变更部分效率天差地别。rsync -avz -e ssh -p 22 ./publish/ 用户名服务器IP:/srv/dotnetapp/注意源目录后面的/不能省它表示同步目录内容而不是目录本身。如果漏了会在目标目录下多出一层publish嵌套后面systemd配置的WorkingDirectory就得跟着改容易踩坑。4.3 目录规划与运行账户我的习惯是在/srv下建项目专属目录日志放到/var/log下数据目录如果应用需要也单独划出来sudo mkdir -p /srv/dotnetapp sudo mkdir -p /var/log/dotnetapp然后一定要创建一个普通用户来跑应用千万不要用root直接跑.NET进程。原因很直接进程被提权攻击会拿到root权限危害太大。创建一个普通的www用户sudo useradd -r -m -d /home/www -s /bin/bash www sudo chown -R www:www /srv/dotnetapp sudo chown -R www:www /var/log/dotnetapp权限设计就是这个思路运行账户只对应用目录有读写权限其他目录保持默认权限。后续App要写日志、写缓存都在它自己的目录下完成不会碰系统文件。4.4 环境变量配置别把机密写进代码配置文件里的连接字符串、密钥这类敏感信息千万别明文写进appsettings.json然后提交到Git仓库。正确做法是利用环境变量覆盖。ASP.NET Core的配置系统默认在读取appsettings.json之后会继续读取环境变量同名字段用环境变量的值覆盖配置文件。我在服务器上专门维护一个环境变量文件配合systemd使用sudo mkdir -p /etc/dotnetapp sudo vim /etc/dotnetapp/dotnetapp.env文件内容大致是ASPNETCORE_ENVIRONMENTProduction ConnectionStrings__DefaultConnectionServer127.0.0.1;Databasemyapp;User Idappuser;Password这里填写强密码注意ConnectionStrings__DefaultConnection这个双下划线写法这是ASP.NET Core的约定双下划线相当于配置层级中的冒号。systemd的EnvironmentFile会读取这个文件加载所有变量。5. 让服务“活下来”systemd守护进程与Nginx反向代理发布产物落地之后接下来是怎么让它稳定运行。我不建议裸敲dotnet MyApp.dll然后挂着因为一旦SSH断开或进程崩溃没人管。systemd就是Linux上最好的进程守护方案。5.1 为什么Nginx要在前面挡一道应用本身用Kestrel监听端口Kestrel已经具备生产级HTTP服务器能力为什么不直接用Kestrel暴露公网端口原因有几点Kestrel对静态文件处理、缓存策略、请求头解析这些边缘功能不如Nginx成熟Nginx可以统一处理HTTPS证书管理证书续期、多站点复用一套配置Nginx的proxy_pass天然支持负载均衡将来应用要横向扩容改几行配置就能把流量分流到多个Kestrel实例。简单说Kestrel负责业务Nginx负责边界。两者职责分开后续维护轻松很多。5.2 systemd服务单元文件在/etc/systemd/system/dotnetapp.service新建一个服务单元[Unit] DescriptionDotNet 8 Web Application Afternetwork.target [Service] Userwww Groupwww WorkingDirectory/srv/dotnetapp EnvironmentFile/etc/dotnetapp/dotnetapp.env ExecStart/usr/bin/dotnet /srv/dotnetapp/MyApp.dll Restartalways RestartSec10 KillSignalSIGINT SyslogIdentifierdotnetapp [Install] WantedBymulti-user.target解释几个关键配置WorkingDirectory设置应用的工作目录很多相对路径操作都依赖它EnvironmentFile把之前准备的环境变量文件加载进进程环境Restartalways进程退出就自动拉起RestartSec10表示退出后等10秒再拉起防止崩了立刻重启的循环风暴KillSignalSIGINT停止服务时给应用发SIGINT信号让ASP.NET Core优雅停机而不是直接SIGKILL干掉进程。这点对Web应用很重要能保证正在处理的请求完成后再退出。然后启动服务sudo systemctl daemon-reload sudo systemctl enable --now dotnetapp sudo systemctl status dotnetapp看状态输出如果是active (running)说明Kestrel已经在5000端口监听了。等一下端口5000是哪来的我还没配置默认URLASP.NET Core默认监听http://localhost:5000。后面Nginx就要连这个端口。5.3 Nginx反向代理配置在Alibaba Cloud Linux 3上装Nginxsudo dnf install -y nginx然后新建站点配置sudo vim /etc/nginx/conf.d/dotnetapp.confserver { 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; } }这里几个proxy_set_header字段特别关键尤其是X-Forwarded-For和X-Forwarded-Proto。如果不转发这两个头ASP.NET Core的Request.IP拿到的永远是127.0.0.1而且不会识别HTTPS请求——明明用了HTTPS应用里生成的重定向链接却还是http://开头。检查配置并重启sudo nginx -t sudo systemctl enable --now nginx sudo systemctl reload nginx5.4 应用侧必须配合的UseForwardedHeadersNginx把X-Forwarded-For这些头转发过去了但ASP.NET Core默认不会自动信任它们。你需要在Program.cs里显式启用using Microsoft.AspNetCore.HttpOverrides; var builder WebApplication.CreateBuilder(args); builder.Services.ConfigureForwardedHeadersOptions(options { options.ForwardedHeaders ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); var app builder.Build(); app.UseForwardedHeaders(); // 其他中间件这一步有没有做好表现差异非常大。我之前踩过坑Nginx配好了HTTPS但应用里用HttpRequest.Scheme判断协议生成的验证邮件链接全部是http://排查了半天才发现是UseForwardedHeaders没调。这个中间件的作用是让应用信任Nginx转发过来的X-Forwarded-Proto头从而正确识别用户实际用的协议。补充一个安全细节使用ForwardedHeaders时如果应用不只信任本机Nginx还要在ForwardedHeadersOptions里配置KnownProxies限制信任范围否则攻击者可以伪造X-Forwarded-For头做IP欺骗。标准做法是只信任来自127.0.0.1的代理默认就是这个所以保持默认即可。5.5 静态文件与缓存策略如果你的项目有大量静态资源图片、CSS、JS建议在Nginx层直接服务静态文件而不是全部交给Kestrel。在server块里加一段location /wwwroot/ { alias /srv/dotnetapp/wwwroot/; expires 30d; add_header Cache-Control public, no-transform; }这样一个图片请求到Nginx就直接返回不经过应用层负载能降不少。但注意一定要确认发布目录下确实有wwwroot文件夹而且www用户有读取权限。6. 安全线补齐HTTPS证书、防火墙与敏感配置项目已经在Linux上跑通了但这只是开始生产环境上线前还有最后一道工序把所有访问链路加密、把不用的端口锁死。这一节的内容我建议你在上线前一条一条对照着做不要省略。6.1 用certbot给域名签一张免费证书既然Nginx在前面挡着证书管理就变得很简单。用Lets Encrypt的客户端certbot一键签证书、自动配Nginxsudo dnf install -y certbot python3-certbot-nginx sudo certbot --nginx -d your-domain.com它会自动检测Nginx配置把server块的listen 80改成listen 443 ssl并配置证书路径。续期也帮你搞定certbot安装时会注册systemd timer自动续期sudo systemctl list-timers | grep certbot确认有certbot-renew.timer在运行证书到期前它会自动执行续期续完重新加载Nginx。有个细节容易忽略续期成功后Nginx并不会自动重载证书配置默认的renew hook里一般带了nginx -s reload但如果你改过Nginx配置最好自己确认一下续期日志里有没有报错。我一般习惯定期手动跑一次sudo certbot renew --dry-run看输出是否干净。6.2 firewalld端口策略Alibaba Cloud Linux 3自带firewalld默认只放行SSH端口。我部署应用时只放行80和443Kestrel的5000端口只监听本机127.0.0.1不需要对外开放sudo systemctl enable --now firewalld sudo firewall-cmd --permanent --add-servicehttp sudo firewall-cmd --permanent --add-servicehttps sudo firewall-cmd --permanent --remove-servicessh --add-rich-rulerule familyipv4 source address你公司的出口IP port port22 protocoltcp accept sudo firewall-cmd --reload最后一条把SSH的22端口限定为公司出口IP访问顺带把ssh服务默认放行移除能少一大半被扫描爆破的攻击流量。另外别忘了阿里云控制台的安全组规则。安全组是云平台层面拦一道firewalld是系统层面拦一道两层都要配。安全组里只需放行80、443和2222建议限制来源IP。6.3 连接字符串等敏感信息的最终兜底为什么前面强调用EnvironmentFile而不是把连接字符串写文件里因为.env文件我放在了/etc/dotnetapp/目录权限设成700、文件权限600只有root能看sudo chown root:root /etc/dotnetapp/dotnetapp.env sudo chmod 600 /etc/dotnetapp/dotnetapp.env这样即使服务器被入侵攻击者用www用户的权限也读不到数据库密码。这条兜底习惯极其重要尤其是未来要接第三方支付、对象存储这类有密钥的场景。6.4 顺手做一次系统安全更新上线前建议把系统补丁打满。Alibaba Cloud Linux的openssh如果有安全更新一定及时升。命令一条搞定sudo dnf update -y sudo reboot重启前确认一下systemctl enable dotnetapp已经设好这样重启后服务自动拉起不需要人工干预。7. .NET 8在云上运行的排错笔记与实测经验最后分享这段时间踩过、帮朋友排查过的一批实际问题。如果你在部署过程中遇到类似报错可以直接对照着看能省去大量翻文档的时间。7.1 高频异常定位表现象可能原因排查命令/思路访问返回502 Bad GatewayNginx连不上Kestrel端口curl http://127.0.0.1:5000看进程是否存活看systemctl status dotnetapp访问返回500 Internal Server Error应用本身崩溃或配置错误journalctl -u dotnetapp -f直接看实时日志页面能开但登录后跳回http缺少UseForwardedHeaders或X-Forwarded-Proto没配置确认Program.cs里加了app.UseForwardedHeaders()Nginx里proxy_set_header X-Forwarded-Proto $scheme日志时间慢了8小时系统时区没设成Asia/Shanghaitimedatectl set-timezone Asia/Shanghai应用启动后几秒就被杀死可能是端口冲突或者OOMss -tlnp查端口dmesgdotnet命令找不到PATH没配或者SDK没装全检查/etc/profile.d/dotnet.sh重开shell验证dotnet --infoappsettings里的配置不生效环境变量没被正确加载systemctl show dotnetapp看EnvironmentFile路径cat /etc/dotnetapp/dotnetapp.env确认键名双下划线写法7.2 最常见的定位步骤服务一挂第一件事永远是看journald日志这是systemd体系最顺手的排查入口sudo journalctl -u dotnetapp -f --since 10 minutes ago-f是follow模式有新的输出实时刷新--since按时间过滤。如果服务一直重启可以先停掉自动重启短期观察sudo systemctl stop dotnetapp sudo /usr/bin/dotnet /srv/dotnetapp/MyApp.dll手工在前台跑一次所有异常堆栈会直接打印出来比journalctl再绕一层快得多。不过记得排查完恢复systemd托管。7.3 .NET 8特有的注意点.NET 8里app.UseForwardedHeaders()必须在app.UseAuthentication()之前调用这个顺序错了OAuth回调地址会变成127.0.0.1认证流程必挂。另外.NET 8默认的Kestrel配置里ListenAnyIP的行为和旧版有一些差异如果你需要让Kestrel监听非本机地址务必在appsettings.json里显式配置{ Urls: http://127.0.0.1:5000 }我统一让Kestrel只监听127.0.0.1对外一定有Nginx转发。这样即使防火墙配错了应用也不会直接暴露到公网。7.4 性能观察工具部署完成后我习惯用dotnet-counters看一眼真实运行数据而不是猜dotnet tool install --global dotnet-counters dotnet-counters monitor --process-id PID --counters System.Runtime进程ID用pgrep -f MyApp.dll拿。重点看CPU Usage、Working Set和GC Heap Size。如果是首次体验或者压测阶段看到Working Set在200-300MB徘徊是正常的说明.NET运行时按需分配了内存不用慌。调优的话关注的是GC Heap增长的曲线持续陡峭上涨说明有内存泄漏风险配合dotnet-dump抓dump分析。7.5 一个小习惯把发布过程写成脚本我每次更新版本流程都是一样的构建、打包、上传、重启服务。手动敲三遍就会觉得烦于是写了个简单的发布脚本放在本地#!/bin/bash set -e dotnet publish -c Release -o ./publish rsync -avz --delete -e ssh -p 22 ./publish/ 用户服务器IP:/srv/dotnetapp/ ssh 用户服务器IP sudo systemctl restart dotnetapp--delete参数保证服务器上残留的旧文件被清掉避免旧DLL和新版本混在一起出现诡异行为。这个脚本现在是我整个部署流程的核心每次跑一遍30秒内完成更新。从裸系统到项目稳定运行整个过程大概一个小时其中一半时间花在等安全更新上。中间没有用到任何图形界面、也没有依赖容器就是一套标准的Linux运行栈。如果你正在规划.NET 8上云这套方案可以直接照着动手。等跑起来之后再回头看看Linux上的日志、systemd的自动拉起、Nginx的流量转发你会发现.NET应用在Linux上的运维体验完全不输传统Windows栈甚至更清爽。