ASP.NET Core部署到Ubuntu服务器

本文把一个目标框架为 net10.0 的 ASP.NET Core 应用发布到 Ubuntu 24.04 x86_64 服务器,采用框架依赖部署:服务器安装 ASP.NET Core Runtime 10,程序由独立账号运行,Nginx 负责公网入口和 HTTPS。示例项目名为 BaoleWeb,域名为 dotnet.example.com,请替换成自己的名称。

这套步骤适用于普通 HTTP 网站或 API。SignalR、WebSocket、多级负载均衡和多节点共享登录状态还需要额外配置。先在开发环境确认项目可运行,不要通过更改线上生产环境为 Development 来显示详细异常。

先处理代码里的代理信任,再发布

用户在浏览器访问的是 HTTPS,Nginx 到应用却使用本机 HTTP。应用要知道原始协议,登录回调和重定向才不会生成错误地址。在现有 Program.cs 中加入转发头设置,并把 UseForwardedHeaders放在 HTTPS 重定向和认证中间件之前。下面展示需要的顺序,保留项目原来的服务注册和业务路由:

using System.Net;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.HttpOverrides;

var builder = WebApplication.CreateBuilder(args);

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedFor
        | ForwardedHeaders.XForwardedProto;
    options.ForwardLimit = 1;
    options.KnownIPNetworks.Clear();
    options.KnownProxies.Clear();
    options.KnownProxies.Add(IPAddress.Loopback);
});

builder.Services.AddDataProtection()
    .SetApplicationName("BaoleWeb")
    .PersistKeysToFileSystem(new System.IO.DirectoryInfo("/var/lib/baole-dotnet/keys"));

// 保留项目原有的服务注册。
var app = builder.Build();
app.UseForwardedHeaders();

// 在此后保留 HTTPS 重定向、认证、授权等中间件。
app.MapGet("/healthz", () => Results.Ok(new { status = "ok" }));
// 保留项目原有的页面或 API 路由。
app.Run();

这里先清理默认列表,随后明确加入 127.0.0.1,只信任本例的同机代理,不能只复制两条 Clear 就结束。也不要额外设置 ASPNETCORE_FORWARDEDHEADERS_ENABLED=true来放开代理范围。若已有健康检查接口,保留原接口并调整后文路径,避免重复映射 /healthz。

Data Protection 这段用于本例单机的持久登录密钥。应用名称要固定,不能每次发布改一个名字;项目已经使用 Redis、数据库等共享密钥方案时,保留原有方案,不重复切换到文件存储。

在开发电脑使用 .NET 10 SDK 构建。下面命令针对 linux-x64 生成完整发布目录,服务器无需安装 SDK:

dotnet publish ./BaoleWeb.csproj -c Release -f net10.0 -r linux-x64 --self-contained false -o ./publish-linux

通过SFTP把 publish-linux内的全部文件上传到 ~/baole-dotnet-upload/。除了 DLL,还需要 deps.json、runtimeconfig.json、wwwroot 和其他发布依赖;只上传一个 DLL 常常无法启动。Linux 文件名区分大小写,路径要与代码一致。

安装运行时,建立独立服务目录

下列命令适用于 Ubuntu 24.04 x86_64。使用 Ubuntu 软件源安装 .NET 10,不额外混入其他发行版或旧教程的微软软件源:

cat /etc/os-release
uname -m
sudo apt-get update &&
sudo apt-get install aspnetcore-runtime-10.0 nginx curl nano &&
dotnet --list-runtimes

输出应同时包含 10.0.x 的 Microsoft.NETCore.App和Microsoft.AspNetCore.App。只有 dotnet-runtime 不够运行依赖 ASP.NET Core 共享框架的网站。若提示找不到软件包,先核对系统、软件源和更新结果,不使用来历不明的安装脚本。

首次部署创建系统账号并保存完整发布目录。发现同名账号或路径时先检查用途,不覆盖旧实例:

test -f "$HOME/baole-dotnet-upload/BaoleWeb.dll" &&
sudo useradd --system --user-group --home-dir /var/lib/baole-dotnet --shell /usr/sbin/nologin baole-dotnet &&
sudo install -d -m 0755 /opt/baole-dotnet/releases/release-01 &&
sudo cp -a "$HOME/baole-dotnet-upload/." /opt/baole-dotnet/releases/release-01/ &&
sudo chown -R root:root /opt/baole-dotnet/releases/release-01 &&
sudo chmod -R u=rwX,go=rX /opt/baole-dotnet/releases/release-01 &&
sudo ln -sT /opt/baole-dotnet/releases/release-01 /opt/baole-dotnet/current &&
sudo install -d -o baole-dotnet -g baole-dotnet -m 0700 /var/lib/baole-dotnet/keys &&
sudo install -d -o root -g root -m 0750 /etc/baole-dotnet &&
sudo touch /etc/baole-dotnet/app.env &&
sudo chmod 0600 /etc/baole-dotnet/app.env

命令使用 &&连接,DLL 检查或前一步失败时,不会继续创建和复制。发布包不要带生产密码、证书私钥或开发调试配置。编辑 /etc/baole-dotnet/app.env,以以下三项作为起点:

ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://127.0.0.1:5000
AllowedHosts=dotnet.example.com

数据库等参数按项目真实配置名称追加。例如层级配置 ConnectionStrings:Main可对应环境变量 ConnectionStrings__Main,但 Main 必须与项目使用的名称一致。这个文件只给管理员读取,systemd 启动时把值交给应用;不要把它复制到网站目录。

启动服务,并核对实际监听

新建 /etc/systemd/system/baole-dotnet.service:

[Unit]
Description=Baole ASP.NET Core application
After=network.target

[Service]
Type=exec
User=baole-dotnet
Group=baole-dotnet
StateDirectory=baole-dotnet
StateDirectoryMode=0700
WorkingDirectory=/opt/baole-dotnet/current
EnvironmentFile=/etc/baole-dotnet/app.env
ExecStart=/usr/bin/dotnet /opt/baole-dotnet/current/BaoleWeb.dll
Restart=on-failure
RestartSec=5
KillSignal=SIGINT
TimeoutStopSec=30
UMask=0077
NoNewPrivileges=true
PrivateTmp=true

[Install]
WantedBy=multi-user.target
sudo systemctl daemon-reload &&
sudo systemctl enable --now baole-dotnet
sudo systemctl status baole-dotnet --no-pager
sudo journalctl -u baole-dotnet -n 80 --no-pager
sudo ss -lntp 'sport = :5000'
curl -i -H 'Host: dotnet.example.com' http://127.0.0.1:5000/healthz

监听应为 127.0.0.1:5000,不是公网的 0.0.0.0。项目如果通过代码、Kestrel 配置或启动参数另外指定了地址,可能覆盖环境变量,以实际监听为准。应用已启用 HTTPS 重定向时,本机 HTTP 可能返回 307 或 308;还要完成下方 HTTPS 入口后检查最终响应。出现 400 时核对 Host 与 AllowedHosts,不直接取消主机名限制。

接入域名,确认HTTPS不会循环跳转

为这个应用新建 /etc/nginx/sites-available/baole-dotnet,保留其他站点:

server {
    listen 80;
    server_name dotnet.example.com;

    location / {
        proxy_pass http://127.0.0.1:5000;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $remote_addr;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header Connection "";
    }
}
sudo ln -sT /etc/nginx/sites-available/baole-dotnet /etc/nginx/sites-enabled/baole-dotnet &&
sudo nginx -t &&
sudo systemctl reload nginx

配置检查成功后才重载,随后按Nginx 证书教程为实际域名启用 HTTPS,并通过安全组开放网站端口。5000 保留本机监听,无需公网放行。完成后从外部检查:

curl -i https://dotnet.example.com/healthz

示例健康接口应返回 200 和 ok,接着验证真正的页面、静态资源、登录和退出。如果 HTTPS 不断重定向,先查 UseForwardedHeaders 的执行顺序和可信代理地址;如果是 502,先查 systemd 日志与本机 5000 监听。不要为排错关闭证书验证,或把 Production 改成 Development 暴露异常详情。

更新程序时,保留业务文件和登录密钥

发布目录只保存程序,上传文件等可写内容应配置到 /var/lib/baole-dotnet下的独立子目录。本例把 Data Protection 密钥固定保存在 /var/lib/baole-dotnet/keys,并使用固定应用名称,避免更换版本目录后登录凭据无法继续解密。项目采用其他密钥存储时,以真实配置和启动日志为准。

升级时将完整新发布包放进新的 release 目录,保留旧程序和当前外部配置,安排短暂维护后停止服务、切换 current 符号链接并重新启动。不要边运行边覆盖 DLL,也不要把业务目录和密钥目录一起换掉。重启后除了接口,还要检查已有登录会话、文件读取和必要业务操作。

程序版本回退以数据库和配置仍兼容为前提;涉及数据库迁移,应先准备对应方案。密钥文件不会因为存入这个目录就自动获得磁盘加密,应限制为服务账号访问,离机备份另行加密。密钥与业务数据需要另行备份,程序发布包不能代替它们。多节点部署时,密钥共享及加密方式需要单独设计,不直接复制本例的单机假设。