Skip to main contentSkip to navigation
[email protected]
Client AreaSupport
Hosting Mammoth
HostingMammothYour Data, Our Responsibility
Home
Solutions
Hosting Services
Store
Pricing
About
Blog
API
Contact

Stay Ahead of the Curve

Get the latest insights on cybersecurity, AI innovations, and enterprise data solutions delivered to your inbox.

Hosting Mammoth
HostingMammothEnterprise Solutions

Enterprise-grade data solutions. Hosting, recovery, cybersecurity, and AI-powered services for businesses worldwide.

[email protected]
Sun - Fri, 9:00am - 5:00pm

Services

  • Cloud Hosting
  • Data Recovery
  • Cybersecurity
  • Legal Support
  • MSP Services
  • Web Development
  • AI Services
  • Free Server Migration

Hosting

  • VPS Hosting (NVMe SSD)
  • VDS Hosting (NVMe)
  • Storage VPS (High SSD)
  • GPU Servers
  • Managed Services
  • Cloud Firewall
  • Load Balancer
  • One-Click Apps
  • n8n Hosting
  • Object Storage
  • FAQ

Company

  • Store
  • Pricing
  • About Us
  • Locations
  • Blog
  • Testimonials
  • Contact
  • Affiliate Program
  • White-Label
  • Terms of Service
  • Privacy Policy
  • Browser Cookies
  • SLA

Support

  • Client Area
  • Submit Ticket
  • Knowledge Base
  • Server Status
  • API Documentation

© 2026 Hosting Mammoth. All rights reserved.

Knowledge Base
Getting StartedAccount ManagementVPS HostingGPU ServersStorage VPSCloud FirewallLoad BalancerServer ManagementBilling & PaymentsSupport & TicketsAffiliate ProgramReseller ProgramMarketplace & Appsn8n HostingManaged ServicesServer MigrationAPI & DevelopersSecurityTroubleshootingGlossaryInstall Guides
  1. Home
  2. /
  3. Support
  4. /
  5. Install Guides
  6. /
  7. How To Install Dotnet Ubuntu
GUIDEInstall Guides

How to Install .NET 8 on Ubuntu 24.04 VPS: ASP.NET Core Production Deploy

23 min read

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?
  • Why Deploy ASP.NET Core on a Linux VPS?
  • Prerequisites
  • Step 1: Update System Packages
  • Step 2: Remove Conflicting Ubuntu dotnet Packages
  • Step 3: Add the Microsoft Package Feed
  • Step 4: Install .NET 8 SDK or Runtime
  • Step 5: Verify the Installation
  • Step 6: Publish Your ASP.NET Core App
  • Step 7: Deploy the App to /var/www
  • Step 8: Create a systemd Service
  • Step 9: Configure Nginx as a Reverse Proxy
  • Step 10: Entity Framework Core Migrations
  • Configuration: appsettings and Environment Variables
  • Upgrading .NET
  • Troubleshooting
  • FAQ
  • Next Steps
  • 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 myapp and you are done.
    • Container-ready — the official mcr.microsoft.com/dotnet/aspnet:8.0 image 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.
    For typical ASP.NET Core workloads — a REST API backing a React frontend, a Blazor Server dashboard, a SignalR chat backend — a 4 vCPU / 8 GB VPS running Ubuntu 24.04 comfortably handles thousands of concurrent requests.

    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:

    bash
    ssh root@your-server-ip

    Step 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.

    bash
    sudo apt update && sudo apt upgrade -y

    If the kernel was updated, reboot before continuing:

    bash
    sudo reboot

    Then 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:

    bash
    sudo apt remove --purge -y 'dotnet' 'aspnetcore' 'netstandard*'
    sudo apt autoremove -y

    If 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:

    bash
    which dotnet
    ls /usr/share/dotnet 2>/dev/null
    ls /usr/lib/dotnet 2>/dev/null

    If any of those return paths, remove them:

    bash
    sudo rm -rf /usr/share/dotnet /usr/lib/dotnet

    Step 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:

    bash
    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.deb

    Expected output:

    text
    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:

    bash
    sudo apt update

    You 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:

    bash
    sudo tee /etc/apt/preferences.d/99microsoft-dotnet.pref > /dev/null <<'EOF'
    Package: dotnet aspnetcore netstandard*
    Pin: origin "packages.microsoft.com"
    Pin-Priority: 1001
    EOF

    A 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

    PackageInstallsSizeUse On
    dotnet-sdk-8.0Compiler, dotnet CLI, MSBuild, NuGet, both runtimes~700 MBDev machines, CI/CD runners, servers where you build the app
    aspnetcore-runtime-8.0ASP.NET Core + base .NET Runtime (no compiler)~160 MBProduction web servers running ASP.NET Core apps
    dotnet-runtime-8.0Bare .NET Runtime only (no ASP.NET Core)~90 MBConsole apps, background workers, gRPC-only services without ASP.NET Core middleware
    For production, install 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)

    bash
    sudo apt install -y aspnetcore-runtime-8.0

    Install the SDK (Dev / Build Server)

    bash
    sudo apt install -y dotnet-sdk-8.0

    Install the Bare Runtime (Console Apps Only)

    bash
    sudo apt install -y dotnet-runtime-8.0

    Expected output (SDK install):

    text
    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:

    bash
    dotnet --info

    Expected output (SDK install, abbreviated):

    text
    .NET SDK:
     Version:           8.0.404
     Commit:            ...
     Workload version:  8.0.400-manifests.xxxxxxxx
     MSBuild version:   17.11.x

    Runtime 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:

    bash
    dotnet --list-runtimes

    Expected output:

    text
    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:

    bash
    export PATH=$PATH:/usr/share/dotnet

    Step 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:

    bash
    dotnet publish -c Release --self-contained false -o ./publish

    Flag-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 from apt upgrade instead of requiring a rebuild.
    • -o ./publish — output directory.
    If you are deploying to a server where you cannot (or do not want to) install the runtime globally — for example, a shared host — switch to --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:

    bash
    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:

    bash
    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:

    bash
    sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
    sudo chown -R myapp:myapp /var/www/myapp

    Sanity-check that the app starts before wiring up systemd. Change to the directory and run it directly:

    bash
    cd /var/www/myapp
    sudo -u myapp dotnet MyApp.dll --urls http://127.0.0.1:5000

    Expected output:

    text
    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/myapp

    From a second SSH session, confirm the app responds:

    bash
    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:

    bash
    sudo mkdir -p /var/lib/myapp/dp-keys
    sudo chown -R myapp:myapp /var/lib/myapp
    sudo chmod 700 /var/lib/myapp/dp-keys

    Then register it in your Program.cs:

    csharp
    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:

    bash
    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 to 0.0.0.0 on 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 load appsettings.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/myapp where they might leak via a static file misconfiguration.
    • Restart=always — restart the process if it exits for any reason. Combined with RestartSec=10 you 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/bin or overwrite /etc.
    Create the env file:

    bash
    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.env

    Note the double-underscore __ — ASP.NET Core's configuration binder translates ConnectionStrings__DefaultConnection into the nested JSON key ConnectionStrings:DefaultConnection.

    Enable and start the service:

    bash
    sudo systemctl daemon-reload
    sudo systemctl enable --now myapp.service

    Check status:

    bash
    sudo systemctl status myapp.service

    Expected output:

    text
    ● 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.dll

    Tail the logs:

    bash
    sudo journalctl -u myapp.service -f

    Step 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:

    bash
    sudo apt install -y nginx

    Create the site config:

    bash
    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:

    bash
    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:

    csharp
    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():

    csharp
    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:

    bash
    dotnet ef migrations bundle --self-contained -r linux-x64 -o ./efbundle

    Copy efbundle to the server and run it during deploys:

    bash
    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:

    bash
    cd /srv/src/MyApp
    sudo -u myapp dotnet ef database update

    Use 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=Production
  • User secrets (dev only — never shipped)
  • Environment variables (highest priority on production)
  • The recommended production layout:

    appsettings.json (checked into git):

    json
    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft.AspNetCore": "Warning"
        }
      },
      "AllowedHosts": "*"
    }

    appsettings.Production.json (checked into git, non-secret production overrides):

    json
    {
      "Logging": {
        "LogLevel": {
          "Default": "Warning"
        }
      },
      "Features": {
        "EnableExperimentalEndpoints": false
      }
    }

    Secrets — pushed via the systemd EnvironmentFile from Step 8:

    text
    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:

    csharp
    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:

    bash
    sudo apt update
    sudo apt upgrade -y aspnetcore-runtime-8.0
    sudo systemctl restart myapp.service

    When the next LTS lands (.NET 10 in November 2025, followed by its own patch stream), install it side-by-side:

    bash
    sudo apt install -y aspnetcore-runtime-10.0

    Both 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:

    bash
    dotnet --list-runtimes

    Once you have migrated off .NET 8 entirely, you can remove it:

    bash
    sudo apt remove --purge -y aspnetcore-runtime-8.0 dotnet-runtime-8.0

    Troubleshooting

    ProblemCauseSolution
    HTTP Error 500.30 - ASP.NET Core app failed to startRuntime mismatch, missing dependency, or exception in Program.csCheck 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 foundWrong runtime installed or Ubuntu's dotnet-host is shadowing Microsoft'sRun 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 useAnother process (old instance, another app) holds port 5000Find it: sudo lsof -i :5000. Stop it, or change ASPNETCORE_URLS in the systemd unit to a different port.
    Nginx returns 502 Bad GatewayKestrel is not listening on the port Nginx is proxying toCheck 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 restartData Protection keys directory is ephemeral or missing SetApplicationNameConfigure 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 readsUse the absolute path /usr/bin/dotnet in the systemd ExecStart line (this is already in the template above).
    Blazor Server / SignalR disconnects every 60sNginx proxy_read_timeout too short, or missing WebSocket Upgrade headersSet 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 specifiedKestrel is trying to bind to an HTTPS URL without a certRemove https:// from ASPNETCORE_URLS. Let Nginx handle TLS; Kestrel stays on plain HTTP loopback.
    Wrong Request.Scheme (always http) behind Nginx with TLSMissing forwarded headers middlewareRegister UseForwardedHeaders() in Program.cs as shown in Step 9.
    Permission denied writing to logs or keys directorysystemd ProtectSystem=strict blocks writes outside ReadWritePathsAdd 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:

    bash
    # Live tail
    sudo journalctl -u myapp.service -f

    Last 200 lines

    sudo journalctl -u myapp.service -n 200 --no-pager

    Since last boot

    sudo journalctl -u myapp.service -b

    Filter to errors only

    sudo journalctl -u myapp.service -p err

    If 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 /healthz and scrape it from Uptime Kuma or Prometheus for alerting on downtime and latency.
    • Ship to containers — the official mcr.microsoft.com/dotnet/aspnet:8.0 base 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 restart on 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.

    Was this article helpful?

    ← Back to Install GuidesBrowse all categories →

    Still have questions?

    Contact Support →Submit a Ticket