直接说结论:Ubuntu 22.04 上装 .NET 没有想象中那么绕,但确实有几个坑值得提前避开。我最近刚在一台全新安装的 22.04 服务器上从零配好 .NET 8 环境,中间踩了微软源签名失效、运行时和 SDK 混淆这类问题,顺手把整个过程整理成这篇实操记录。不管你是在本机做开发,还是要在服务器上部署 ASP.NET Core 应用,这篇东西都能直接照着抄。
先说清楚一个概念:.NET SDK和.NET Runtime不是一回事。SDK 包含编译器、MSBuild、dotnet CLI 等一系列开发工具,适合在本机写代码、编译项目;Runtime 只负责运行已经编译好的程序。如果你只是部署别人写好的应用,装 runtime 就够了,没必要多占那几百 MB。但如果你连dotnet build都要跑,那就装 SDK。搞混这两者,是我见过最常见的安装翻车原因。
1. 准备工作:先搞清楚系统状态和版本选择
1.1 确认 Ubuntu 版本和架构
动手之前先确认系统的具体版本和 CPU 架构。命令很简单:
lsb_release -a uname -mlsb_release -a会显示发行版信息,确认是 22.04.x 而不是 20.04 或 24.04。不同版本的源配置路径略有差异,硬套会报错。uname -m显示x86_64就代表 amd64 架构,这是绝大多数服务器的类型。如果是aarch64(ARM 架构,比如树莓派或某些云厂商的 ARM 实例),后面装包时要选 arm64 版本,不能直接照抄 amd64 的命令。
我在某台 ARM 架构的实例上踩过一次坑:直接用 amd64 的安装脚本,装完以后运行dotnet --info直接报"command not found"。因为源配置里写的架构不匹配,包管理器根本找不到对应的软件包。所以第一步别省,看清楚架构再往下走。
1.2 .NET 版本该怎么选
现在(以当前时间点为准)主流的选择是 .NET 8,它是 LTS(长期支持)版本,微软会维护三年左右。.NET 9 属于 STS(短期支持),生命周期只有 18 个月,适合想尝鲜的开发者,不适合需要稳定环境的服务器。.NET 6 虽然还在支持期内,但已经进入了最后阶段,新项目不建议再用了。
打个比方:LTS 就像买房,合同期长、稳定;STS 就像租房,到期了就得搬,不搬就没维护。生产环境我劝你老老实实选 LTS,除非项目有明确需求非用某个 STS 特性不可。
# 查看当前系统是否已有 .NET 相关包 dpkg -l | grep -E 'dotnet|aspnet'这一条命令能帮你快速排查系统里是否已经装过 .NET。如果有残留版本,建议先清干净再装新的,避免版本冲突。
1.3 安装基础依赖
.NET 在 Ubuntu 上需要一些底层依赖库,主要是 OpenSSL、ICU(国际化组件)、curl 等。绝大多数全新安装的 Ubuntu 22.04 都自带这些,但保险起见先跑一遍更新:
sudo apt update sudo apt install -y curl ca-certificates libstdc++6 libicu70libicu70在 22.04 上默认叫这个名字,20.04 上则是libicu66,用错版本会报找不到依赖的错误。如果你已经安装了更高版本的 libicu,apt 会自动处理依赖关系,不需要手动指定。
提示:如果系统里有残留的旧版 .NET,建议先执行
sudo apt remove --purge dotnet-* aspnetcore-*清理干净,再开始新安装。我见过两台机器因为新旧版本混合导致dotnet --list-sdks显示正常但编译时行为异常的情况,排查半天最后发现是全局环境变量在捣乱。
2. 通过 APT 源安装:最省心的官方路线
2.1 配置微软软件源
微软官方提供了 Ubuntu 的 APT 源,这是我最推荐的安装方式。好处是后续可以用sudo apt update && sudo apt upgrade直接升级 .NET,不用手动下载安装包。
先把微软的包签名密钥和源地址加进去。Ubuntu 22.04 用的代号是jammy,这一点必须写对。
# 下载并安装微软的 GPG 密钥 wget https://packages.microsoft.com/keys/microsoft.asc sudo mv microsoft.asc /etc/apt/trusted.gpg.d/microsoft.asc # 配置 APT 源 sudo sh -c 'echo "deb [arch=amd64 signed-by=/etc/apt/trusted.gpg.d/microsoft.asc] https://packages.microsoft.com/ubuntu/22.04/prod jammy main" > /etc/apt/sources.list.d/microsoft-prod.list'如果你用的是 ARM 架构,把上面源地址里的arch=amd64改成arch=arm64。
这一步我想多说两句。网上很多老教程用的是curl https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor ...这种方式,会把密钥转成二进制格式再放进去。我实测在 22.04 上直接保存.asc文本格式也能正常工作,因为新版的 apt 支持直接读取 ASCII armor 格式的密钥。两种方式都行,但建议保持一致性,别混用。
2.2 安装 SDK 或 Runtime
源配好之后,先刷新包列表,然后安装:
sudo apt update然后选择一种:
# 安装完整 SDK(开发机用) sudo apt install -y dotnet-sdk-8.0 # 只装 .NET Runtime(部署服务器用) sudo apt install -y dotnet-runtime-8.0 # 只装 ASP.NET Core Runtime(跑 Web 应用用) sudo apt install -y aspnetcore-runtime-8.0这里有一个常见的误区:跑 ASP.NET Core 应用时不能只装dotnet-runtime-8.0。普通 runtime 不包含 ASP.NET Core 的组件,你装完以后运行 web 项目会报错说缺少Microsoft.AspNetCore.App框架引用。必须装aspnetcore-runtime-8.0才行。
SDK 自带完整的运行时,所以装了 SDK 就不用再单独装 runtime,反过来不行。
2.3 验证安装结果
安装完成后,验证一下:
dotnet --version dotnet --list-sdks dotnet --list-runtimes正常输出应该能看到类似8.0.x的版本号。如果三个命令都报 command not found,大概率是 PATH 没配上。但用 apt 安装一般不会出现这个问题,除非你之前手动改过~/.bashrc里的 PATH。
再做一个实战测试,创建一个临时项目跑一下:
mkdir /tmp/dotnet-test && cd /tmp/dotnet-test dotnet new console -n HelloDotnet cd HelloDotnet dotnet run看到终端打印出 "Hello, World!" 就说明环境没问题。这一步虽然简单,但能验证编译链路、运行时加载、依赖解析是否完整,比单纯看dotnet --version靠谱得多。
3. 通过官方脚本安装:适合多版本共存的场景
3.1 使用 dotnet-install 脚本
除了 APT 源,微软还提供了一个安装脚本,适合需要安装指定版本、或想多版本并存的场景。这个脚本的路径是公开的:
# 下载安装脚本 wget https://dot.net/v1/dotnet-install.sh chmod +x dotnet-install.sh # 安装最新的 .NET 8 SDK ./dotnet-install.sh --channel 8.0脚本默认安装到$HOME/.dotnet,不会写入系统目录,也不需要 sudo。这有个好处:不会污染全局环境,卸载时直接删目录就行。但也有个坑:脚本装的 .NET 不在系统 PATH 里,需要手动添加:
echo 'export PATH=$HOME/.dotnet:$PATH' >> ~/.bashrc source ~/.bashrc3.2 为什么有时候脚本比 APT 更合适
我个人的经验是:如果你是开发者,且要在同一台机器上切换 .NET 6 和 .NET 8 两个版本来验证兼容性,脚本方案比 APT 省心得多。APT 源每次 update 都会把微软源里的所有包刷一遍,版本多了之后偶尔会出现依赖锁定的问题。而脚本方案是完全独立安装,互不干扰。
你可以这样同时装两个版本:
./dotnet-install.sh --channel 6.0 ./dotnet-install.sh --channel 8.0然后通过dotnet --list-sdks查看,会同时列出两个 SDK。运行时选择方面,在项目目录下放一个global.json文件即可指定用哪个版本:
{ "sdk": { "version": "8.0.100", "rollForward": "latestFeature" } }rollForward指定了版本匹配的策略。latestFeature表示允许向上匹配同大版本内的最新功能版本;如果想严格锁定精确版本,可以设成disable。这个文件在 CI/CD 构建时特别有用,能保证构建环境的 SDK 版本完全一致。
注意:手动脚本安装的版本不会出现在 APT 的包管理记录里,以后升级时需要手动再跑一次脚本。如果安装到系统级目录(用
--install-dir /usr/share/dotnet),卸载时直接删目录,但我不建议这么做,容易把系统 PATH 搞乱。
3.3 直接下载二进制包
如果你连脚本也不想跑,也可以直接从微软官网下载 tar.gz 压缩包手动解压:
mkdir -p $HOME/dotnet tar zxf dotnet-sdk-8.0.100-linux-x64.tar.gz -C $HOME/dotnet export PATH=$HOME/dotnet:$PATH dotnet --version这种方式的优点是完全可控,想放哪放哪,尤其适合做离线部署(在内网环境里不能访问外网时)。缺点是升级也得手动下载解压,而且要注意下载的版本号要和官方发布页面对应上,复制错链接是常事。
4. 部署 ASP.NET Core 应用时的环境配置
4.1 设置环境变量
装完 .NET 只是第一步,真正把应用跑起来还需要一些环境配置。ASP.NET Core 默认会监听localhost:5000,但在服务器上你一般得让它监听0.0.0.0才能被外部访问。有两种方式:
# 方式一:运行前设置环境变量 export ASPNETCORE_URLS=http://0.0.0.0:5000 dotnet YourApp.dll # 方式二:启动时指定 dotnet YourApp.dll --urls "http://0.0.0.0:5000"如果你的应用里配置了applicationhost.json或者 launchSettings.json,这个值可能被覆盖。部署到 Linux 上时,建议在 appsettings.json 里显式设置"Urls": "http://0.0.0.0:5000",或者干脆在 systemd 服务文件里通过Environment=ASPNETCORE_URLS=http://0.0.0.0:5000来指定。
我实际部署某项目时遇到过一个问题:在开发机上跑dotnet run一切正常,但发布到服务器后怎么都访问不到。排查了一圈才发现是 launchSettings.json 里写死了 localhost 监听地址,发布出来的 DLL 仍然在读这个配置。后来把发布目录里的 launchSettings 删掉,问题就解决了。
4.2 HTTPS 开发证书
本地开发如果用到 HTTPS,需要先设置开发证书:
dotnet dev-certs https dotnet dev-certs https --trust在 Ubuntu 上,--trust需要手动将证书导入系统信任区。它会提示你输入 sudo 密码。如果在服务器上做 HTTPS 部署,不建议用 dev-certs,而是用反向代理(比如 Nginx)来做 TLS 终止,或者申请正规的 CA 证书。
4.3 用 systemd 把应用变成后台服务
在生成环境服务器上,推荐用 systemd 托管 .NET 应用,这样系统重启时应用会自动拉起,crash 了也会自动重启。创建一个 service 文件:
sudo nano /etc/systemd/system/yourapp.service内容如下:
[Unit] Description=My .NET Web Application [Service] WorkingDirectory=/var/www/yourapp ExecStart=/usr/bin/dotnet /var/www/yourapp/YourApp.dll Restart=always RestartSec=10 KillSignal=SIGINT Environment=ASPNETCORE_ENVIRONMENT=Production Environment=ASPNETCORE_URLS=http://0.0.0.0:5000 [Install] WantedBy=multi-user.target然后执行:
sudo systemctl daemon-reload sudo systemctl enable yourapp.service sudo systemctl start yourapp.service这里KillSignal=SIGINT很重要。.NET 应用收到 SIGINT 时能优雅关机,执行挂起请求的收尾工作(比如保存状态、清理连接池)。如果用默认的 SIGTERM,有时会导致进程直接退出,某些数据库事务来不及回滚。
查看日志用:
sudo journalctl -u yourapp.service -f4.4 内存与 GC 配置
在服务器上部署 .NET 应用,内存占用经常是运维关注的重点。默认情况下 .NET 的 GC 是服务器模式,会根据机器物理内存自动调节堆大小。在内存有限的 VPS(比如 1GB 或 2GB)上,可以考虑用ServerGC和GCConserveMemory配置:
export DOTNET_gcServer=1 export DOTNET_GCConserveMemory=9或者在 csproj 里设置:
<PropertyGroup> <ServerGarbageCollection>true</ServerGarbageCollection> <ConserveMemory>9</ConserveMemory> </PropertyGroup>但这里要提醒一点:不建议在容器环境里盲目调 GC 参数。我见过有团队在 Docker 里把 ServerGC 关掉,结果应用在小并发下频繁 GC,CPU 飙到 80%。先跑起来看监控数据,再决定要不要调,比听网上建议瞎改强。
5. 常见问题与排查技巧实录
5.1 证书验证失败:无法从 packages.microsoft.com 获取源
这是搭 APT 源时最常见的问题,报错信息一般长这样:
W: GPG error: https://packages.microsoft.com/ubuntu/22.04/prod jammy InRelease: The following signatures couldn't be verified because the public key is not available出现这个问题的原因在于/etc/apt/trusted.gpg.d/microsoft.asc没有成功下载,或者权限不对。我碰到过一次是 wget 下载时网络中断,生成的文件是空文件,apt 读不到密钥自然就报错。
解法很简单:重新下载密钥,确认文件非空:
sudo rm -f /etc/apt/trusted.gpg.d/microsoft.asc wget -q https://packages.microsoft.com/keys/microsoft.asc ls -l microsoft.asc # 确认文件大小不是 0 sudo mv microsoft.asc /etc/apt/trusted.gpg.d/microsoft.asc sudo apt update5.2 安装了 SDK 但 dotnet 命令找不到
这个问题通常在手动解压安装或者脚本安装后出现。原因很简单:PATH 没配置好。
echo $PATH看看输出里有没有 dotnet 所在目录。如果没有,参照前面第 3 节的方法添加 PATH。如果你用的是 snap 安装方式,命令路径不一样,建议直接卸载 snap 版,改用 apt 或脚本方式。
还有一个隐蔽的问题:如果你是通过sudo apt install dotnet-sdk-8.0装的,但dotnet命令位于/usr/bin/dotnet里,普通用户执行没问题,但用 sudo 执行时如果 sudo 环境没保留用户 PATH,也可能报 command not found。这种情况可以用sudo -i或直接sudo /usr/bin/dotnet验证。
5.3 运行应用时报缺 Microsoft.AspNetCore.App
It was not possible to find any compatible framework version The framework 'Microsoft.AspNetCore.App', version '8.0.0' was not found.这个报错十有八九是只装了dotnet-runtime-8.0没装aspnetcore-runtime-8.0。不需要重新安装 SDK,执行:
sudo apt install -y aspnetcore-runtime-8.0然后验证:
dotnet --list-runtimes应该能看到Microsoft.AspNetCore.App 8.0.x这一行。如果运行环境里同时有多个 .NET 大版本(比如 6 和 8),发布时把目标框架编错也会出现类似问题。用dotnet publish -f net8.0指定目标框架可以避免。
5.4 多版本冲突:解决 .NET 6 和 .NET 8 共存
我见过不少开发者用 apt 先装了 .NET 6,再装 .NET 8,结果用dotnet --list-sdks看到两个版本都在,但dotnet run总是跑在低版本上。这是因为没有指定启动版本。
解决方案就是前面提到的global.json。每个项目根目录放一个,Git 提交到代码库以后,所有开发者和 CI 服务器都会自动用相同版本构建,这个习惯值得从第一天开始养成。
dotnet new globaljson --sdk-version 8.0.100 --roll-forward latestFeature5.5 环境变量 LD_LIBRARY_PATH 的坑
这个问题比较隐蔽。如果你从源码编译过某些原生依赖(比如 openssl),系统里的LD_LIBRARY_PATH被改写,.NET 可能加载不了 ICU 或者 SSL 库,报错为:
Process terminated. Couldn't find a valid ICU package installed on the system.有一些教程建议装libicu后设置export DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1。我得提醒一句:这个变量能绕过问题,但等于关闭了全球化功能。如果你的应用需要处理日期格式化、Unicode 排序、时区转换,关掉这个会引发诡异行为,比如日期格式错乱、大小写转换异常。正确做法是保证libicu70已安装,并清除掉LD_LIBRARY_PATH里的无效项。
5.6 磁盘空间不足
SDK 完整安装后占用约 500MB 到 1GB 空间。如果服务器的/分区比较小,装完可能会看到No space left on device。
df -h如果确实空间紧张,可以只装 ASP.NET Core Runtime(大约 80MB),用本地开发机发布好自包含应用,再拷贝到服务器上运行。自包含发布方式能把 .NET 运行时打进发布包里,服务器上完全不需要装 .NET,之前见过一些嵌入式场景没有外网权限,就把整个发布目录打包传上去直接用。
5.7 官方易踩坑速查表
| 现象 | 原因 | 解决办法 |
|---|---|---|
| apt update 时报密钥错误 | 微软 GPG 密钥缺失或损坏 | 重新下载microsoft.asc到 trusted.gpg.d |
| dotnet 命令找不到 | PATH 未配置 | export PATH 或重开终端 |
| 运行 Web 应用报缺 ASP.NET Core | 只装了 runtime | 安装aspnetcore-runtime-8.0 |
| 编译时 SDK 版本不对 | 无 global.json | 项目中添加 global.json |
| 服务启动后被 kill | OOM 或 systemd 配置不对 | 查看 journalctl 日志,检查 Restart 参数 |
| 访问外部 API 报 SSL 错误 | 缺 ca-certificates | sudo apt install -y ca-certificates |
6. 离线安装与自包含发布的补充方案
在一些内网环境、银行/企业服务器(不连外网)里装 .NET,标准安装流程完全不可行。这时有两种替代方案,都值得记录。
6.1 离线安装包方式
在有网络的环境下载好 .deb 包,然后拷贝到内网机器上安装。微软源里的包可以直接用apt download抓取:
apt download dotnet-sdk-8.0 aspnetcore-runtime-8.0会得到几个.deb文件,传到内网机器后执行:
sudo dpkg -i *.deb注意依赖关系:.deb不能自动拉取依赖,所以还需要把libicu70等依赖也一并下载。如果内网机器用的是离线镜像源,也可以用apt install在线安装依赖,只对 .NET 包做 dpkg 安装。
6.2 自包含发布
对于应用开发者来说,自包含发布是最省心的一种部署方式。发布命令:
dotnet publish -c Release -r linux-x64 --self-contained true /p:PublishSingleFile=true这一条命令会生成一个包含运行时和应用的目录(或单文件),拷贝到任何 x64 Linux 服务器上都能直接运行。目标服务器完全不需要装 .NET。
linux-x64对应 x86_64 架构,linux-arm64对应 ARM 架构。发布平台必须和目标机器架构一致,不能用 x64 的发布包跑在 arm64 上,反过来也一样。
自包含的代价是体积:单文件大概在 60-80MB,而框架依赖发布只有 2-10MB。但有的时候这个体积换来的省心程度是值得的。
我在某个离线项目里把整套 .NET 8 应用做成自包含单文件,拷贝到目标机器上直接./app就起来。目标机器连libicu都不用装,只要内核版本不太旧就行。
7. 我踩过且不想让你再踩的几个坑
最后分享三个特别容易忽略的细节,都是真金白银的教训。
第一个是时区问题。.NET 在 Linux 上获取本地时间依赖 ICU 和 tzdata。如果目标机器的/etc/localtime没配好,DateTime.Now会返回 UTC 时间。部署完应用最好看一眼日志里的时间戳,如果和本地时间差了 8 个小时,先查系统时区而不是改代码:
timedatectl set-timezone Asia/Shanghai第二个是运行账号权限。不要图省事用 root 跑 .NET 服务。我见过有团队用 root 跑 Web 应用,结果应用某个接口存在路径穿越漏洞,黑客直接读了 /etc/shadow。正确做法是创建专用账号,只给应用目录的读写权限:
sudo useradd -m -d /var/www/yourapp yourappuser sudo chown -R yourappuser:yourappuser /var/www/yourapp然后在 systemd 服务文件的[Service]段加上User=yourappuser和Group=yourappuser。
第三个是诊断工具。装完 .NET 以后,dotnet-counters、dotnet-dump这些全局工具建议顺手装上。线上应用 CPU飙到 100%、内存只增不减的时候,有没有这些工具决定了你要排查一天还是一小时:
dotnet tool install --global dotnet-counters dotnet tool install --global dotnet-dump等真正出事时你会感谢此刻装的工具。举个真实例子,某应用的托管服务内存持续上涨,用dotnet-counters monitor --process-id <pid>盯着gc-heap-size和working-set两个计数器,十分钟就定位到是某静态集合无限增长导致的,这比靠猜靠谱太多。
Ubuntu 22.04 装 .NET 这件事本身不难,难的是装完之后让它稳定地跑起来。希望这篇记录能帮你少走一段弯路。如果安装过程中遇到什么奇怪的报错,欢迎对照上面的排查表逐条过一遍,大概率能找到答案。