Deploy ASP.NET Core lên VPS Linux
Cách deploy .NET lên Linux: cài .NET 10 trên Ubuntu 24.04, publish, chạy Kestrel bằng systemd sau nginx với SSL, làm tay hoặc bằng ZoPanel.

Ý chính
- Trên Ubuntu 24.04, .NET 10 và .NET 8 có sẵn trong kho gói của Ubuntu; kho của Microsoft không còn gói .NET cho bản này.
- .NET 8 và .NET 9 đều hết hỗ trợ ngày 10/11/2026, nên dự án deploy mới hãy nhắm .NET 10 LTS.
- Kestrel nên lắng nghe trên 127.0.0.1 phía sau nginx, và bật forwarded headers để app biết request là https và IP thật của khách.
- Hãy lưu key ring của Data Protection ra ngoài thư mục release, nếu không mỗi lần khởi động lại người dùng sẽ bị đăng xuất.
Để deploy .NET lên Linux, bạn publish ứng dụng bằng dotnet publish -c Release, cài ASP.NET Core runtime trên server (hoặc publish dạng self-contained), chạy app bằng systemd với Kestrel chỉ lắng nghe trên 127.0.0.1, rồi đặt nginx phía trước để lo tên miền và SSL. Hai chỗ hay làm người mới vấp: middleware forwarded headers, để app biết request gốc là HTTPS, và nơi lưu key Data Protection cố định, để khởi động lại app không đăng xuất toàn bộ người dùng. Trên Ubuntu 24.04, làm tay toàn bộ mất chưa tới một giờ.
Bài này làm từng bước trên Ubuntu 24.04, sau đó làm lại cùng việc đó bằng ZoPanel, và cuối cùng so sánh với Windows/IIS và Azure App Service.
Tháng 10/2026 nên deploy phiên bản .NET nào?
Hãy dùng .NET 10. Theo chính sách hỗ trợ .NET của Microsoft, bản LTS được hỗ trợ 3 năm, bản STS 2 năm, và mốc thời gian hiện tại là:
| Phiên bản | Loại | Phát hành | Hết hỗ trợ |
|---|---|---|---|
| .NET 10 | LTS | 11/11/2025 | 14/11/2028 |
| .NET 9 | STS | 12/11/2024 | 10/11/2026 |
| .NET 8 | LTS | 14/11/2023 | 10/11/2026 |
Tại thời điểm viết (tháng 10/2026), .NET 8 và .NET 9 chỉ còn chưa đầy một tháng nhận bản vá. Nếu app vẫn đang nhắm net8.0 hay net9.0, hãy lên kế hoạch chuyển sang net10.0 ngay; với đa số web app, việc này là sửa <TargetFramework> và cập nhật các gói NuGet.
Cần chuẩn bị gì?
- Một VPS Ubuntu 24.04 có quyền sudo. Một API ASP.NET Core nhỏ chạy được với 1 GB RAM; nếu build ngay trên server thì cần nhiều hơn.
- Một tên miền đã trỏ bản ghi A về IP server. Xem bài trỏ tên miền về VPS.
- Phần bảo mật cơ bản làm trước: SSH key, tường lửa, cập nhật tự động. Bài bảo mật VPS Ubuntu có danh sách đầy đủ.
Cách deploy .NET lên Linux thủ công
Bước 1: Cài ASP.NET Core runtime
Theo hướng dẫn cài .NET trên Ubuntu của Microsoft, trên Ubuntu 24.04 thì .NET 10 và .NET 8 có trong kho gói mặc định của Ubuntu, .NET 9 nằm ở kho ppa:dotnet/backports, còn kho gói của Microsoft không còn chứa gói .NET cho Ubuntu. Vì vậy cài .NET 10 không cần thêm kho nào:
sudo apt-get update
sudo apt-get install -y aspnetcore-runtime-10.0
dotnet --list-runtimes
Gói aspnetcore-runtime-10.0 gồm cả .NET runtime lẫn ASP.NET Core. Nếu bạn build luôn trên server thì cài dotnet-sdk-10.0; SDK đã kèm runtime. Đừng trộn gói từ kho Ubuntu với gói từ kho khác: Microsoft ghi rõ đây là nguyên nhân hay gặp của lỗi lẫn gói.
Bước 2: Publish ứng dụng
Chạy lệnh này trên máy bạn hoặc trong CI:
dotnet publish -c Release -o ./publish
Kết quả là bản build framework-dependent: chỉ có DLL của bạn, không kèm runtime. Cách còn lại là bản self-contained, mang theo runtime riêng:
dotnet publish -c Release -r linux-x64 --self-contained -o ./publish
| Framework-dependent | Self-contained | |
|---|---|---|
| Runtime trên server | Cần (aspnetcore-runtime-10.0) |
Không cần |
| Dung lượng | Nhỏ (chỉ code của bạn) | Lớn hơn: kèm runtime |
| Vá bảo mật runtime | apt upgrade rồi khởi động lại |
Build lại và deploy lại app |
| Lệnh chạy | dotnet MyApp.dll |
./MyApp |
| Kiến trúc | Bất kỳ (một bản build cho cả x64 và ARM64) | Mỗi RID một bản (linux-x64, linux-arm64) |
Hướng dẫn host trên Linux với nginx của Microsoft dùng framework-dependent, bài này cũng vậy: bản cập nhật bảo mật của Ubuntu sẽ vá runtime cho mọi app cùng lúc. App self-contained vẫn cần các thư viện hệ thống như libicu74 và libssl3t64, có liệt kê trong hướng dẫn cài đặt.
Bước 3: Đưa lên server dưới một user riêng
Đừng chạy app bằng root. Tạo user hệ thống, thư mục releases và một thư mục dữ liệu riêng:
sudo adduser --system --group --home /srv/myapp myapp
sudo mkdir -p /srv/myapp/releases /var/lib/myapp/keys
sudo chown -R myapp:myapp /srv/myapp /var/lib/myapp
sudo chmod 700 /var/lib/myapp/keys
Từ máy bạn, đẩy mỗi bản build vào một thư mục riêng và trỏ symlink current vào đó:
REL=$(date +%Y%m%d%H%M%S)
rsync -a ./publish/ deploy@server:/tmp/myapp-$REL/
ssh deploy@server "sudo mv /tmp/myapp-$REL /srv/myapp/releases/$REL && \
sudo chown -R myapp:myapp /srv/myapp/releases/$REL && \
sudo ln -sfn /srv/myapp/releases/$REL /srv/myapp/current"
Bước 4: Cho app biết nó đứng sau proxy và lưu key cố định
Sửa hai chỗ trong Program.cs. Thứ nhất, nginx giải mã SSL rồi gửi HTTP thường vào Kestrel, nên app phải đọc X-Forwarded-Proto và X-Forwarded-For. Microsoft hướng dẫn gọi UseForwardedHeaders trước các middleware khác; nếu không truyền tùy chọn thì không header nào được chuyển tiếp. Proxy trên loopback như 127.0.0.1 được tin cậy mặc định.
Thứ hai, key ring của Data Protection dùng để ký cookie đăng nhập và token chống CSRF. Khi không rơi vào nơi lưu mặc định nào, key chỉ nằm trong bộ nhớ, và hướng dẫn của Microsoft cảnh báo rằng khi đó mỗi lần khởi động lại, cookie đăng nhập và token CSRF đều mất hiệu lực. Hãy lưu key ra đĩa, ngoài thư mục release:
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/var/lib/myapp/keys"))
.SetApplicationName("myapp");
var app = builder.Build();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
// ... các middleware và endpoint còn lại
app.Run();
Microsoft lưu ý: khi bạn tự chỉ thư mục lưu key, cơ chế tự mã hóa key khi lưu sẽ bị tắt. Đó là lý do thư mục trên được chmod 700 và thuộc về user của app.
Bước 5: Chạy Kestrel bằng systemd
Để bí mật trong một file chỉ root đọc được. ASP.NET Core đọc biến môi trường làm cấu hình, dùng __ thay cho ::
sudo tee /etc/myapp.env >/dev/null <<'EOF'
ConnectionStrings__Default=Server=127.0.0.1;Database=myapp;User=myapp;Password=change-me
EOF
sudo chmod 600 /etc/myapp.env
Tạo /etc/systemd/system/myapp.service, dựa trên file unit trong hướng dẫn của Microsoft:
[Unit]
Description=My ASP.NET Core app
After=network.target
[Service]
User=myapp
Group=myapp
WorkingDirectory=/srv/myapp/current
ExecStart=/usr/bin/dotnet /srv/myapp/current/MyApp.dll
Restart=always
RestartSec=10
KillSignal=SIGTERM
TimeoutStopSec=90
SyslogIdentifier=myapp
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://127.0.0.1:5000
Environment=DOTNET_NOLOGO=true
EnvironmentFile=/etc/myapp.env
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ReadWritePaths=/var/lib/myapp
[Install]
WantedBy=multi-user.target
Vài dòng cần chú ý:
ASPNETCORE_URLS=http://127.0.0.1:5000cho Kestrel chỉ nghe ở localhost. Hãy bỏUseUrls(...)hoặcKestrel:Endpointstrongappsettings.json, vì chúng sẽ ghi đè giá trị này.KillSignal=SIGTERMlà mặc định của systemd, và hướng dẫn hiện tại của Microsoft khuyên giữ nguyên: .NET chờSIGTERMđể tắt êm, và Microsoft giờ khuyên không dùngSIGINTnhư các bản hướng dẫn cũ.ASPNETCORE_ENVIRONMENT=Productionsẽ nạpappsettings.Production.json. Tên file trên Linux phân biệt hoa thường.
Nếu muốn, thêm gói Microsoft.Extensions.Hosting.Systemd và gọi builder.Host.UseSystemd();. Khi đó app báo cho systemd biết lúc nó thực sự đã chạy xong và đã dừng; theo tài liệu UseSystemd, cần đặt Type=notify trong unit. Chỉ thêm Type=notify khi có lời gọi đó, nếu không systemd sẽ chờ một thông báo không bao giờ tới.
Khởi động:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
journalctl -u myapp -f
curl -I http://127.0.0.1:5000
Bước 6: Đặt nginx phía trước
sudo apt-get install -y nginx
Tạo /etc/nginx/sites-available/myapp, chỉnh từ ví dụ của Microsoft (khối map giúp WebSocket, SignalR và Blazor Server hoạt động):
map $http_connection $connection_upgrade {
"~*Upgrade" $http_connection;
default keep-alive;
}
server {
listen 80;
server_name example.com www.example.com;
location / {
proxy_pass http://127.0.0.1:5000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
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;
}
}
Luôn ghi rõ server_name; hướng dẫn của Microsoft coi việc thiếu nó là một rủi ro bảo mật. Bật site và kiểm tra cấu hình trước khi reload:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
Bước 7: SSL và tường lửa
sudo apt-get install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo ufw allow OpenSSH
sudo ufw allow 'Nginx Full'
sudo ufw enable
Certbot tự sửa cấu hình nginx và đặt lịch gia hạn chứng chỉ. Cổng 5000 vẫn đóng với internet. Chi tiết hơn trong bài SSL miễn phí với Let's Encrypt.
Cập nhật app thế nào?
Đẩy bản build mới vào một thư mục release mới, chuyển symlink current rồi khởi động lại:
sudo ln -sfn /srv/myapp/releases/20261021120000 /srv/myapp/current
sudo systemctl restart myapp
Rollback cũng là lệnh đó với thư mục cũ hơn. Có hai điểm cần biết: trong lúc khởi động lại sẽ có một khoảng ngắn nginx trả lỗi 502, và sau khi apt upgrade vá runtime, Microsoft khuyên khởi động lại các app. Muốn cập nhật không gián đoạn thì cần hai instance và health check trước khi chuyển traffic, việc mà công cụ deploy hoặc panel sẽ làm thay bạn. Bài tự động deploy từ Git hướng dẫn deploy mỗi lần push.
Cách deploy .NET lên Linux bằng ZoPanel
ZoPanel build app .NET từ mã nguồn ngay trên server và chạy nó như một service của tài khoản hosting. Theo tài liệu .NET:
- Cài SDK: quản trị viên mở Runtimes và cài .NET 10.0, 9.0 hoặc 8.0 trên thẻ .NET SDK. Các phiên bản cài song song được. Trên Ubuntu 24.04, 8.0 và 10.0 lấy từ gói của Ubuntu, 9.0 từ kho backports.
- Tạo website: Websites → New website → Git deploy, nhập tên miền và repository. Xem Git deploy.
- Kiểm tra nhận diện: file
.csprojhoặc.slnđược nhận là ASP.NET Core (tìm project sâu tối đa hai cấp thư mục). Lệnh đề xuất làdotnet restore,dotnet publish 'MyApp.csproj' -c Release -o out --no-restorevàASPNETCORE_URLS=http://127.0.0.1:$PORT dotnet out/'MyApp.dll'. Phiên bản SDK đi theo<TargetFramework>, trừ khi repository cóglobal.jsonriêng. - Thêm cấu hình trong Environment variables, ví dụ
ConnectionStrings__Default. Giá trị được lưu trong file chỉ root đọc được và truyền qua systemd, không bao giờ nằm trên dòng lệnh. - Deploy. Bản mới khởi động ở slot đang rảnh và chỉ nhận traffic khi trả lời Health check path với mã dưới 500 trong vòng 90 giây. Trong lúc đó bản cũ vẫn phục vụ. Rollback giữ 5 bản release gần nhất.
Bạn vẫn cần thêm UseForwardedHeaders như ở bước 4, vì nginx gửi X-Forwarded-For và X-Forwarded-Proto. Mỗi release là một thư mục mới, nên hãy để key Data Protection và file upload trong một mục Persistent paths hoặc trong database. Đừng viết cứng cổng: PORT khác nhau giữa hai slot blue/green.
Cần biết trước khi chọn: build chạy trên server, trong giới hạn RAM của tài khoản, nên với gói nhỏ hãy đặt DOTNET_gcServer=0 hoặc DOTNET_GCHeapHardLimit như tài liệu hướng dẫn. Trên Debian arm64 chỉ có .NET 10, vì Microsoft chỉ phát hành gói .NET 8 và 9 cho Debian trên amd64. Và ZoPanel là panel Linux: không chạy IIS hay app .NET Framework 4.x.
Lựa chọn khác: Windows/IIS và Azure App Service
| Cách | Hợp với | Đánh đổi |
|---|---|---|
| VPS Linux làm tay | Toàn quyền kiểm soát, chi phí thấp, hiểu rõ hệ thống | Bạn tự lo cập nhật, release và giám sát |
| VPS Linux với ZoPanel | Nhiều app hoặc nhiều khách hàng trên một server, Git deploy, rollback | Chỉ Linux; build dùng RAM của server |
| Windows Server với IIS | App .NET Framework, Windows Authentication, đội đã quen Windows | Bản quyền Windows Server; phải quản trị IIS |
| Azure App Service | Nền tảng được quản lý, có deployment slot và scale | Chi phí tăng theo gói; ít quyền kiểm soát máy chủ |
Host trên IIS qua ASP.NET Core Module là cách trưởng thành, được hỗ trợ đầy đủ, và là lựa chọn duy nhất ở đây nếu bạn còn code .NET Framework. Azure App Service bỏ hẳn việc bảo trì server và tự chia sẻ key ring Data Protection giữa các instance.
Lỗi thường gặp
- 502 Bad Gateway: app không chạy hoặc nghe ở chỗ khác, thường là
localhost:5000mặc định hoặc URL trongappsettings.json. Kiểm trajournalctl -u myappvàss -ltnp. - Redirect vòng lặp hoặc link
http://sau khi bật SSL: thiếuUseForwardedHeadershoặc gọi nó sau các middleware khác. - Người dùng bị đăng xuất sau mỗi lần deploy: key Data Protection nằm trong bộ nhớ hoặc trong thư mục release.
The application to execute does not exist: tên DLL trongExecStartsai, thường do project đặt<AssemblyName>.
Kết luận
Deploy .NET lên Linux gói gọn trong: một runtime còn hỗ trợ (.NET 10 từ kho gói của Ubuntu), bản build đã publish, systemd, nginx với forwarded headers, key ring được lưu cố định và SSL. Làm tay một lần giúp bạn hiểu từng thành phần. Khi đã có nhiều app, hoặc muốn release có health check và rollback một cú click mà không phải viết script, hãy thử gói Free của ZoPanel: 3 tài khoản hosting, 10 website, 10 database, không giới hạn thời gian, có sẵn app .NET và Git deploy. Quy trình tương tự cho các ngôn ngữ khác có trong bài deploy Node.js và deploy Python.
Câu hỏi thường gặp
ASP.NET Core có chạy production trên Linux được không?
Được. ASP.NET Core là đa nền tảng, và Microsoft có tài liệu, có hỗ trợ việc host trên Linux sau nginx với systemd. Kestrel phục vụ app, còn nginx lo SSL, tên miền và giảm tải file tĩnh.
Có cần cài SDK trên server không?
Không, trừ khi bạn build trên đó. App framework-dependent chỉ cần aspnetcore-runtime-10.0; app self-contained thì không cần cả runtime. Các panel build từ Git như ZoPanel thì cài SDK, vì chúng chạy dotnet publish ngay trên server.
Dự án mới nên dùng .NET 8 hay .NET 10?
.NET 10. Cả .NET 8 và .NET 9 đều hết hỗ trợ ngày 10/11/2026, còn .NET 10 LTS được hỗ trợ đến 14/11/2028. Sau ngày hết hỗ trợ sẽ không còn bản vá bảo mật nào.
Mở Kestrel thẳng ra internet có an toàn không?
Kestrel có thể phục vụ traffic trực tiếp, nhưng trên VPS thì đơn giản hơn nếu để nó ở 127.0.0.1 và cho nginx lo SSL và nhiều tên miền trên cổng 80 và 443. Khi đó việc gia hạn chứng chỉ không bao giờ đụng tới app.
Vì sao người dùng bị đăng xuất mỗi lần khởi động lại app?
Vì key Data Protection dùng để mã hóa cookie đăng nhập không được lưu lại. Hãy lưu bằng PersistKeysToFileSystem vào một thư mục ngoài thư mục release, chỉ user của app được ghi, và đặt SetApplicationName cố định cho app.
Đọc tiếp

Deploy tự động từ GitHub lên VPS: webhook, Actions, không downtime
Ba cách deploy tự động từ GitHub lên VPS: GitHub Actions qua SSH, webhook, hoặc Git deploy của panel, kèm script release, health check và rollback.

Deploy Laravel lên VPS: queue, scheduler và cập nhật không gián đoạn
Cách deploy Laravel lên VPS Ubuntu 24.04: PHP-FPM, nginx, .env, queue worker, scheduler và release không downtime, làm tay hoặc với ZoPanel.

Deploy Next.js lên VPS, không cần Vercel
Cách deploy Next.js lên VPS: output standalone, systemd, nginx cache /_next/static, SSL miễn phí, cập nhật không gián đoạn và những gì bạn mất so với Vercel.