How to Install .NET 8 on Ubuntu 24.04 VPS — ASP.NET Core Production Deploy
.NET 8 is Microsoft's current long-term support (LTS) release of the cross-platform .NET runtime, supported through November 2026. Running ASP.NET Core on a Linux VPS instead of Windows Server eliminates licensing costs, cuts memory overhead, and puts you on the same deployment primitives the rest of the cloud-native world already uses: systemd, Nginx, and container-friendly single-file publishes. This guide walks you through a complete install-to-production deploy on Ubuntu 24.04, from pulling the correct Microsoft package feed to serving a live ASP.NET Core app behind Nginx with TLS and data protection keys configured correctly.
Skip the setup? Our CloudCore Starter VPS comes with Ubuntu 24.04 pre-provisioned and ready for a dotnet publish deploy in under five minutes.Table of Contents
What is .NET 8?
.NET 8 is the unified runtime and SDK for C#, F#, and VB.NET applications. It is the successor to .NET 7 and the current LTS release, meaning Microsoft will ship security patches and bug fixes through November 2026. On top of .NET 8 sits ASP.NET Core, the high-performance web framework that powers minimal APIs, Blazor Server and WebAssembly apps, SignalR real-time services, and gRPC endpoints.
The runtime is fully open source, cross-platform, and produces binaries roughly on par with Go or Rust for HTTP throughput. Kestrel, the in-process web server baked into ASP.NET Core, routinely tops the TechEmpower benchmarks for JSON serialization and plaintext response workloads. Native AOT compilation, introduced in .NET 7 and expanded in .NET 8, shrinks container images to tens of megabytes and drops cold-start times to milliseconds — ideal for serverless and containerized deployments.
On Linux, .NET 8 ships as a set of deb packages from Microsoft's own apt feed. You can install the full SDK (for building and publishing code), the ASP.NET Core Runtime (for running web apps), or the bare .NET Runtime (for console apps and services that do not need ASP.NET Core). This guide covers all three and explains when to pick each.
Why Deploy ASP.NET Core on a Linux VPS?
Running .NET on Linux has been officially supported since .NET Core 1.0 in 2016, and in 2026 it is the mainstream deployment target for new ASP.NET Core projects. Compared to Windows Server, a Linux VPS gives you:
- No Windows Server CAL costs — a standard Ubuntu VPS has zero licensing overhead. Windows Server Datacenter editions add EUR 40+/month to the same hardware.
- Lower memory footprint — a minimal Ubuntu 24.04 install idles under 200 MB. A Windows Server 2022 Core install needs 1.5-2 GB just to run the OS.
- Native systemd integration — no IIS, no app pools, no recycling timers.
systemctl restart myappand you are done. - Container-ready — the official
mcr.microsoft.com/dotnet/aspnet:8.0image is 215 MB and runs anywhere Docker runs. - Same toolchain as the rest of your stack — Nginx, certbot, UFW, fail2ban, and journald work identically whether you are hosting Node.js, Python, or C#.
- Predictable, flat-rate pricing — a VPS costs the same whether your app serves 10 requests or 10 million. No per-core licensing, no Software Assurance renewals.
Prerequisites
Before you begin, make sure you have:
- A VPS running Ubuntu 24.04 LTS with root or sudo access
- SSH access to your server
- At least 2 GB of RAM (4 GB+ recommended for builds on the server)
- At least 5 GB of free disk space for the SDK plus your app
- A domain name pointing at your server's public IP (required for the Nginx + TLS section)
Recommended Plan: CloudCore Starter>
For most ASP.NET Core APIs and small-to-medium Blazor apps, the CloudCore Starter plan is the right sizing:>
- 4 vCPU cores
- 8 GB RAM
- 75 GB NVMe SSD
- Unmetered bandwidth>
If you are running Entity Framework migrations against a local PostgreSQL or SQL Server instance on the same box, 8 GB gives you comfortable headroom for both the runtime and the database.
Connect to your server via SSH:
ssh root@your-server-ipStep 1: Update System Packages
Refresh the package index and upgrade installed packages so the Microsoft feed's dependency resolution does not trip over stale libc or libssl versions.
sudo apt update && sudo apt upgrade -yIf the kernel was updated, reboot before continuing:
sudo rebootThen reconnect over SSH.
Step 2: Remove Conflicting Ubuntu dotnet Packages
Ubuntu 24.04 ships its own dotnet-host, dotnet-runtime-8.0, and aspnetcore-runtime-8.0 packages in the universe repository. These packages are community-built, often behind on patches, and — most importantly — they conflict with the Microsoft-published packages in ways that cause confusing Framework 'Microsoft.NETCore.App', version '8.0.x' was not found errors at runtime.
Always use the Microsoft feed for production. Remove any Ubuntu-shipped .NET packages first:
sudo apt remove --purge -y 'dotnet' 'aspnetcore' 'netstandard*'
sudo apt autoremove -yIf the command reports "Unable to locate package" for all of them, you have a clean slate — nothing is installed. Continue to the next step.
Check for any leftover files that could shadow the Microsoft install:
which dotnet
ls /usr/share/dotnet 2>/dev/null
ls /usr/lib/dotnet 2>/dev/nullIf any of those return paths, remove them:
sudo rm -rf /usr/share/dotnet /usr/lib/dotnetStep 3: Add the Microsoft Package Feed
Microsoft publishes a tiny bootstrap package called packages-microsoft-prod that registers their apt feed and GPG key for you. Install it from the Microsoft download site (not from Ubuntu's repo) to make sure you get the latest feed definition.
Download and install the bootstrap package:
wget https://packages.microsoft.com/config/ubuntu/24.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
sudo dpkg -i packages-microsoft-prod.deb
rm packages-microsoft-prod.debExpected output:
Selecting previously unselected package packages-microsoft-prod.
(Reading database ... 45678 files and directories currently installed.)
Preparing to unpack packages-microsoft-prod.deb ...
Unpacking packages-microsoft-prod (1.1-ubuntu24.04.1) ...
Setting up packages-microsoft-prod (1.1-ubuntu24.04.1) ...This writes /etc/apt/sources.list.d/microsoft-prod.list and imports Microsoft's GPG key to /etc/apt/trusted.gpg.d/microsoft.gpg. Refresh the package index so apt sees the new feed:
sudo apt updateYou should now see packages.microsoft.com in the Hit: lines of the output.
Pin the Microsoft Feed (Optional but Recommended)
To prevent apt from accidentally preferring Ubuntu's universe packages over Microsoft's, pin the Microsoft feed at a higher priority:
sudo tee /etc/apt/preferences.d/99microsoft-dotnet.pref > /dev/null <<'EOF'
Package: dotnet aspnetcore netstandard*
Pin: origin "packages.microsoft.com"
Pin-Priority: 1001
EOFA priority above 1000 forces apt to prefer this origin even over the currently installed version, which makes upgrades deterministic.
Step 4: Install .NET 8 SDK or Runtime
Microsoft ships three separate packages. Picking the right one is important because installing the wrong one on a production server wastes disk space (the SDK is ~700 MB) and expands your attack surface unnecessarily.
SDK vs Runtime vs ASP.NET Core Runtime
| Package | Installs | Size | Use On |
|---|---|---|---|
dotnet-sdk-8.0 | Compiler, dotnet CLI, MSBuild, NuGet, both runtimes | ~700 MB | Dev machines, CI/CD runners, servers where you build the app |
aspnetcore-runtime-8.0 | ASP.NET Core + base .NET Runtime (no compiler) | ~160 MB | Production web servers running ASP.NET Core apps |
dotnet-runtime-8.0 | Bare .NET Runtime only (no ASP.NET Core) | ~90 MB | Console apps, background workers, gRPC-only services without ASP.NET Core middleware |
aspnetcore-runtime-8.0. You do not need the SDK on the production server if you build on a separate CI machine or on your laptop and ship the published output.For a dev server where you build on the box, install dotnet-sdk-8.0 — it includes both runtimes automatically.
Install the ASP.NET Core Runtime (Production)
sudo apt install -y aspnetcore-runtime-8.0Install the SDK (Dev / Build Server)
sudo apt install -y dotnet-sdk-8.0Install the Bare Runtime (Console Apps Only)
sudo apt install -y dotnet-runtime-8.0Expected output (SDK install):
The following NEW packages will be installed:
aspnetcore-runtime-8.0 aspnetcore-targeting-pack-8.0 dotnet-apphost-pack-8.0
dotnet-host-8.0 dotnet-hostfxr-8.0 dotnet-runtime-8.0 dotnet-sdk-8.0
dotnet-targeting-pack-8.0 dotnet-templates-8.0 netstandard-targeting-pack-2.1-8.0
0 upgraded, 10 newly installed, 0 to remove and 0 not upgraded.
Need to get 184 MB of archives.Step 5: Verify the Installation
Confirm the runtime and SDK are registered correctly:
dotnet --infoExpected output (SDK install, abbreviated):
.NET SDK: Version: 8.0.404 Commit: ... Workload version: 8.0.400-manifests.xxxxxxxx MSBuild version: 17.11.xRuntime Environment: OS Name: ubuntu OS Version: 24.04 OS Platform: Linux RID: linux-x64 Base Path: /usr/share/dotnet/sdk/8.0.404/
.NET workloads installed: There are no installed workloads to display.
Host: Version: 8.0.11 Architecture: x64 Commit: ...
.NET SDKs installed: 8.0.404 [/usr/share/dotnet/sdk]
.NET runtimes installed: Microsoft.AspNetCore.App 8.0.11 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App] Microsoft.NETCore.App 8.0.11 [/usr/share/dotnet/shared/Microsoft.NETCore.App]
List just the runtimes if you installed only aspnetcore-runtime-8.0:
dotnet --list-runtimesExpected output:
Microsoft.AspNetCore.App 8.0.11 [/usr/share/dotnet/shared/Microsoft.AspNetCore.App]
Microsoft.NETCore.App 8.0.11 [/usr/share/dotnet/shared/Microsoft.NETCore.App]If you see Command 'dotnet' not found, the shell PATH has not picked up /usr/share/dotnet. Open a new SSH session or run:
export PATH=$PATH:/usr/share/dotnetStep 6: Publish Your ASP.NET Core App
On your development machine (or on the VPS if you installed the SDK), publish the app for Linux. The dotnet publish command produces a self-contained directory ready to run on the target server.
The recommended production command is:
dotnet publish -c Release --self-contained false -o ./publishFlag-by-flag:
-c Release— builds with optimizations enabled and without the debug symbols you do not want on production boxes.--self-contained false— produces a framework-dependent publish that relies on the ASP.NET Core runtime already installed on the server. The output is ~10 MB instead of ~90 MB and gets runtime security patches fromapt upgradeinstead of requiring a rebuild.-o ./publish— output directory.
--self-contained true -r linux-x64 to bundle the runtime into the publish output. This is significantly larger and loses the security-update benefit, so only use it when necessary.Transfer the Publish Output to the Server
From your workstation:
rsync -avz ./publish/ admin@your-server-ip:/tmp/myapp/Or from a CI pipeline, upload the tarball as a release artifact and curl it on the server.
Step 7: Deploy the App to /var/www
On the VPS, install the published files under /var/www/myapp, which is the conventional location for web application payloads on Debian-family systems.
Create the directory and copy the files:
sudo mkdir -p /var/www/myapp
sudo cp -r /tmp/myapp/* /var/www/myapp/Create a dedicated system user to run the app — never run ASP.NET Core as root:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
sudo chown -R myapp:myapp /var/www/myappSanity-check that the app starts before wiring up systemd. Change to the directory and run it directly:
cd /var/www/myapp
sudo -u myapp dotnet MyApp.dll --urls http://127.0.0.1:5000Expected output:
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://127.0.0.1:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: /var/www/myappFrom a second SSH session, confirm the app responds:
curl http://127.0.0.1:5000/If you get a response, stop the foreground process with Ctrl+C and move on to the systemd unit.
Data Protection Keys Directory
ASP.NET Core's Data Protection API (used for authentication cookies, antiforgery tokens, and anything calling IDataProtectionProvider) needs a persistent directory for its keyring. If you skip this, every app restart invalidates all existing cookies and logs every user out.
Create a keys directory owned by the app user:
sudo mkdir -p /var/lib/myapp/dp-keys
sudo chown -R myapp:myapp /var/lib/myapp
sudo chmod 700 /var/lib/myapp/dp-keysThen register it in your Program.cs:
builder.Services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/var/lib/myapp/dp-keys"))
.SetApplicationName("MyApp");The SetApplicationName call is important if you plan to scale out to multiple servers sharing the same keyring — it must match across all replicas.
Step 8: Create a systemd Service
systemd is Ubuntu's service manager. It will start your app at boot, restart it on crash, collect logs to journald, and handle graceful shutdown.
Create the unit file:
sudo tee /etc/systemd/system/myapp.service > /dev/null <<'EOF' [Unit] Description=MyApp ASP.NET Core application After=network.target[Service] WorkingDirectory=/var/www/myapp ExecStart=/usr/bin/dotnet /var/www/myapp/MyApp.dll Restart=always RestartSec=10 KillSignal=SIGINT SyslogIdentifier=myapp User=myapp Group=myapp EnvironmentFile=/etc/myapp/myapp.env Environment=ASPNETCORE_ENVIRONMENT=Production Environment=ASPNETCORE_URLS=http://127.0.0.1:5000 Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false Environment=DOTNET_CLI_HOME=/tmp
Security hardening
NoNewPrivileges=true PrivateTmp=true ProtectSystem=strict ProtectHome=true ReadWritePaths=/var/lib/myapp /var/log/myapp ProtectKernelTunables=true ProtectControlGroups=true
[Install] WantedBy=multi-user.target EOF
Key pieces:
ASPNETCORE_URLS=http://127.0.0.1:5000— binds Kestrel to loopback only. All external traffic arrives via Nginx on ports 80/443 and is proxied to 5000. Never bind Kestrel directly to0.0.0.0on a public interface in production — Nginx gives you TLS termination, request buffering, and slowloris protection that Kestrel does not.ASPNETCORE_ENVIRONMENT=Production— tells ASP.NET Core to loadappsettings.Production.json, disable the developer exception page, and enable response compression defaults.EnvironmentFile=/etc/myapp/myapp.env— pulls secrets (connection strings, API keys, JWT signing keys) from an env file outside the publish directory. This keeps credentials out of your git history and out of/var/www/myappwhere they might leak via a static file misconfiguration.Restart=always— restart the process if it exits for any reason. Combined withRestartSec=10you get bounded restart loops that will not hammer the CPU on a tight crash loop.ProtectSystem=strict+ReadWritePaths— the process cannot write anywhere except the directories you explicitly allow. Even if the app is compromised, an attacker cannot drop files into/usr/binor overwrite/etc.
sudo mkdir -p /etc/myapp
sudo tee /etc/myapp/myapp.env > /dev/null <<'EOF'
ConnectionStrings__DefaultConnection=Host=127.0.0.1;Database=myapp;Username=myapp;Password=change-me
Jwt__SigningKey=replace-with-32-byte-random-string
EOF
sudo chown root:myapp /etc/myapp/myapp.env
sudo chmod 640 /etc/myapp/myapp.envNote the double-underscore __ — ASP.NET Core's configuration binder translates ConnectionStrings__DefaultConnection into the nested JSON key ConnectionStrings:DefaultConnection.
Enable and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.serviceCheck status:
sudo systemctl status myapp.serviceExpected output:
● myapp.service - MyApp ASP.NET Core application
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-04-16 10:15:00 UTC; 5s ago
Main PID: 5678 (dotnet)
Tasks: 18 (limit: 9420)
Memory: 78.4M
CPU: 1.102s
CGroup: /system.slice/myapp.service
└─5678 /usr/bin/dotnet /var/www/myapp/MyApp.dllTail the logs:
sudo journalctl -u myapp.service -fStep 9: Configure Nginx as a Reverse Proxy
Nginx fronts Kestrel so you get TLS termination, HTTP/2, static file caching, WebSocket upgrade handling, and correct client IP forwarding. See our companion guide on installing Nginx on Ubuntu 24.04 if you do not already have it installed.
Install Nginx:
sudo apt install -y nginxCreate the site config:
sudo tee /etc/nginx/sites-available/myapp > /dev/null <<'EOF'WebSocket upgrade mapping — required for SignalR and Blazor Server
map $http_upgrade $connection_upgrade { default upgrade; '' close; }server { listen 80; server_name myapp.example.com;
# Certbot will replace this with a redirect to 443 after cert issuance location / { proxy_pass http://127.0.0.1:5000; proxy_http_version 1.1;
# WebSocket support (SignalR, Blazor Server) proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade;
# Forward real client information to ASP.NET Core 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-Host $host;
# Long timeouts for SignalR / streaming proxy_read_timeout 600s; proxy_send_timeout 600s; proxy_buffering off;
# Max upload size (adjust for your app) client_max_body_size 50m; } } EOF
Enable the site and obtain a TLS certificate:
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/ sudo nginx -t sudo systemctl reload nginx
sudo apt install -y certbot python3-certbot-nginx sudo certbot --nginx -d myapp.example.com
Certbot auto-rewrites the server block to listen on 443 with the certificate and key paths, and adds a 301 redirect from port 80.
Tell ASP.NET Core to Trust the Proxy
For Request.Scheme and HttpContext.Connection.RemoteIpAddress to reflect the real client instead of 127.0.0.1, ASP.NET Core needs forwarded headers middleware. In Program.cs:
using Microsoft.AspNetCore.HttpOverrides;builder.Services.Configure<ForwardedHeadersOptions>(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; // Trust the loopback proxy options.KnownNetworks.Clear(); options.KnownProxies.Clear(); options.KnownProxies.Add(System.Net.IPAddress.Parse("127.0.0.1")); });
var app = builder.Build(); app.UseForwardedHeaders();
Without this, app.UseHttpsRedirection() will loop forever because Kestrel only sees http:// from the proxy.
Step 10: Entity Framework Core Migrations
Most ASP.NET Core apps ship with Entity Framework Core migrations. Run them on the server as part of your deploy.
Option A: Run Migrations at Startup (Simple)
In Program.cs, before app.Run():
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
}Simple, but runs on every replica in a scaled deployment and can race if you have more than one instance starting at once.
Option B: Ship dotnet ef Bundles
From your dev machine, bundle migrations into a standalone executable:
dotnet ef migrations bundle --self-contained -r linux-x64 -o ./efbundleCopy efbundle to the server and run it during deploys:
sudo -u myapp /var/www/myapp/efbundle --connection "Host=127.0.0.1;Database=myapp;Username=myapp;Password=..."This works even if the production server does not have the SDK installed.
Option C: dotnet ef database update (Dev Server Only)
If the SDK is installed and you have the project source on the server:
cd /srv/src/MyApp
sudo -u myapp dotnet ef database updateUse this only on dev/staging, never on production, because it requires the full source tree and build output on the box.
Configuration: appsettings and Environment Variables
ASP.NET Core reads configuration from multiple sources, in this order (later sources override earlier ones):
appsettings.json (committed to source control, non-secret defaults)appsettings.{Environment}.json — appsettings.Production.json when ASPNETCORE_ENVIRONMENT=ProductionThe recommended production layout:
appsettings.json (checked into git):
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}appsettings.Production.json (checked into git, non-secret production overrides):
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"Features": {
"EnableExperimentalEndpoints": false
}
}Secrets — pushed via the systemd EnvironmentFile from Step 8:
ConnectionStrings__DefaultConnection=...
Jwt__SigningKey=...
Stripe__SecretKey=sk_live_...To verify what the app is reading in production, expose a diagnostic endpoint guarded by an admin API key, or use the IConfiguration logger extension temporarily:
app.Logger.LogInformation("Loaded config: {Config}", builder.Configuration.GetDebugView());Remove this before shipping to production — it dumps every config value including secrets.
Upgrading .NET
When .NET 8 patches ship (monthly on Patch Tuesday), upgrade with:
sudo apt update
sudo apt upgrade -y aspnetcore-runtime-8.0
sudo systemctl restart myapp.serviceWhen the next LTS lands (.NET 10 in November 2025, followed by its own patch stream), install it side-by-side:
sudo apt install -y aspnetcore-runtime-10.0Both runtimes coexist under /usr/share/dotnet/shared/. Your app still runs on whichever TargetFramework it was published against. To migrate, update <TargetFramework>net10.0</TargetFramework> in the .csproj, republish, and redeploy. No server config changes required.
To see all installed runtimes:
dotnet --list-runtimesOnce you have migrated off .NET 8 entirely, you can remove it:
sudo apt remove --purge -y aspnetcore-runtime-8.0 dotnet-runtime-8.0Troubleshooting
| Problem | Cause | Solution |
|---|---|---|
HTTP Error 500.30 - ASP.NET Core app failed to start | Runtime mismatch, missing dependency, or exception in Program.cs | Check sudo journalctl -u myapp.service -n 200 --no-pager. Most common cause is a missing connection string or DI registration throwing in Startup. |
Framework 'Microsoft.AspNetCore.App', version '8.0.x' was not found | Wrong runtime installed or Ubuntu's dotnet-host is shadowing Microsoft's | Run dotnet --list-runtimes. If empty, install aspnetcore-runtime-8.0. If it shows an older version, the app was published against a newer patch — upgrade with sudo apt upgrade aspnetcore-runtime-8.0. |
Failed to bind to address http://127.0.0.1:5000: address already in use | Another process (old instance, another app) holds port 5000 | Find it: sudo lsof -i :5000. Stop it, or change ASPNETCORE_URLS in the systemd unit to a different port. |
| Nginx returns 502 Bad Gateway | Kestrel is not listening on the port Nginx is proxying to | Check sudo systemctl status myapp.service. Verify ASPNETCORE_URLS matches the proxy_pass URL. Run curl http://127.0.0.1:5000/ on the server to confirm Kestrel responds. |
| App logs users out on every restart | Data Protection keys directory is ephemeral or missing SetApplicationName | Configure PersistKeysToFileSystem(new DirectoryInfo("/var/lib/myapp/dp-keys")) and ensure systemd's ReadWritePaths includes that path. |
dotnet command works as root but not as myapp user | /usr/share/dotnet not in the myapp user's PATH, or systemd's ProtectSystem blocks reads | Use the absolute path /usr/bin/dotnet in the systemd ExecStart line (this is already in the template above). |
| Blazor Server / SignalR disconnects every 60s | Nginx proxy_read_timeout too short, or missing WebSocket Upgrade headers | Set proxy_read_timeout 600s; and include the Upgrade/Connection headers as shown in the Nginx config. |
Unable to configure HTTPS endpoint. No server certificate was specified | Kestrel is trying to bind to an HTTPS URL without a cert | Remove https:// from ASPNETCORE_URLS. Let Nginx handle TLS; Kestrel stays on plain HTTP loopback. |
Wrong Request.Scheme (always http) behind Nginx with TLS | Missing forwarded headers middleware | Register UseForwardedHeaders() in Program.cs as shown in Step 9. |
Permission denied writing to logs or keys directory | systemd ProtectSystem=strict blocks writes outside ReadWritePaths | Add the path to ReadWritePaths= in the unit file and systemctl daemon-reload && systemctl restart myapp. |
Viewing Logs
The primary debug tool is the service's journald stream:
# Live tail
sudo journalctl -u myapp.service -fLast 200 lines
sudo journalctl -u myapp.service -n 200 --no-pagerSince last boot
sudo journalctl -u myapp.service -bFilter to errors only
sudo journalctl -u myapp.service -p errIf your app also writes to a file via Serilog or NLog, check /var/log/myapp/ (make sure that path is in ReadWritePaths in the unit file).
For a deeper dive on managing services, see our guide on creating and managing systemd services on Ubuntu.
FAQ
Should I use the SDK or just the ASP.NET Core Runtime in production?
Install only the ASP.NET Core Runtime (aspnetcore-runtime-8.0) on production boxes. The SDK is ~5x larger, includes the compiler, and brings in NuGet plus MSBuild — none of which you need to run a published app. Build on your laptop or in CI, dotnet publish -c Release --self-contained false, and ship the output. This also means your production server has a smaller attack surface and faster apt upgrade cycles.
Framework-dependent vs self-contained publish — which do I want?
Framework-dependent (--self-contained false) is the right default for VPS deploys. The output is ~10 MB, the runtime is shared across all apps on the box, and security patches arrive via apt upgrade. Self-contained (--self-contained true -r linux-x64) bundles the runtime into the publish output, producing a ~90 MB directory that runs without any runtime installed on the target. Use self-contained only when you cannot install the runtime globally — for example, shipping to a container base image without .NET pre-installed, or targeting a shared host with no apt access. The downside is that CVEs in the runtime require you to rebuild and redeploy instead of apt upgrade && systemctl restart.
Why run Kestrel behind Nginx instead of exposing it directly?
Kestrel is a fine edge server for inter-service traffic, but on a public-facing port it is missing features Nginx gives you for free: TLS termination (Kestrel can do it, but certificate management and renewal are awkward compared to certbot), HTTP/2 and HTTP/3 offload, static file serving with caching, slowloris and request buffering protection, and rate limiting. Nginx also lets you serve a maintenance page during deploys without touching the .NET app. The overhead of the extra hop on localhost is sub-millisecond.
Can I run multiple ASP.NET Core apps on the same VPS?
Yes, and it is the typical setup. Give each app its own systemd unit, its own port (5000, 5001, 5002, ...), its own user, its own /var/www/<app> directory, and its own Nginx server block with a distinct server_name. Certbot can issue a single certificate covering multiple subdomains with -d app1.example.com -d app2.example.com. A 4 vCPU / 8 GB VPS comfortably runs 5-10 modest ASP.NET Core apps this way.
How do I deploy new versions without dropping requests?
The simplest zero-downtime approach uses Nginx upstream failover: run two instances of your app (old and new) on different ports, flip the Nginx proxy_pass to the new one, reload Nginx (systemctl reload nginx — not restart), then stop the old instance. For more sophisticated blue-green deploys, put both upstream servers in an upstream { } block with backup and max_fails directives. For full containerized rollouts, see our companion guides on Docker Compose and k3s.
What's the memory footprint of a typical ASP.NET Core API?
An idle ASP.NET Core 8 minimal API on Linux uses approximately 40-60 MB of resident memory. Under moderate load (100 req/s JSON endpoints hitting PostgreSQL), expect 120-200 MB. Large Blazor Server apps with many concurrent circuits can climb to 500 MB+. On an 8 GB VPS you have plenty of headroom for the runtime, a PostgreSQL instance, and Nginx with room to spare.
Next Steps
With .NET 8 installed, your ASP.NET Core app publishing cleanly, and Nginx fronting Kestrel with TLS, you have a production-ready stack. Build on it:
- Add PostgreSQL or SQL Server for persistence — most ASP.NET Core apps pair with PostgreSQL via Npgsql. Run it on the same VPS for small workloads or move it to a dedicated DB server as you scale.
- Set up health checks and monitoring — ASP.NET Core ships
Microsoft.Extensions.Diagnostics.HealthChecks. Expose/healthzand scrape it from Uptime Kuma or Prometheus for alerting on downtime and latency.
- Ship to containers — the official
mcr.microsoft.com/dotnet/aspnet:8.0base image plus a multi-stage Dockerfile produces a 215 MB production image. Pair with Docker Compose for multi-service deployments.
- Add a CI/CD pipeline — install Gitea with Actions runners, or point GitHub Actions at your VPS via SSH deploy keys, to automate
dotnet publish+rsync+systemctl restarton every push to main.
- Read the official docs — the ASP.NET Core documentation and .NET on Linux docs are excellent and kept current.
Ready to Deploy Your ASP.NET Core App?>
The CloudCore Starter VPS gives you 4 vCPU, 8 GB RAM, and 75 GB NVMe on Ubuntu 24.04 — the exact spec this guide targets. Provision in 60 seconds and follow the steps above to ship your first production .NET 8 deploy.>
- Ubuntu 24.04 LTS pre-installed
- Full root access for Microsoft feed + systemd
- Unmetered bandwidth for build artifact uploads
- Snapshots for safe upgrade rollbacks>
Launch a CloudCore Starter VPS and deploy .NET 8 today.