.NET 10 Linux Nginx systemd SSL

From Localhost to the Internet: Deploying a .NET API to a Linux VPS

AL
Azim Litanga
· February 15, 2025 · ⏱ 10 min read
Series: Logic Dynamics Web Development Projects #01 — Part 2 of 3

Localhost is comfortable. It's fast, it's safe, and it never goes down. But it has one fatal flaw — nobody else can access it.

If you want your app to be real, it needs to live on a real server. That means learning Linux, Nginx, systemd, and SSL certificates — topics many .NET developers avoid because they feel like "DevOps stuff." But here's the truth: deploying your own app is one of the most confidence-building things you can do as a developer. DNS, HTTP, ports, processes — all abstract concepts until you configure them yourself.

This article covers exactly how I deployed my .NET 10 Chat API to a Ubuntu 24 VPS — from a zip file to a live HTTPS endpoint.

The target architecture

User's device │ │ HTTPS (port 443) ▼ Nginx (reverse proxy + SSL termination) │ │ HTTP (port 5246, internal only) ▼ ASP.NET Core API (running as systemd service) │ ▼ SQLite database (local file)

Nginx sits in front of everything. It handles SSL, accepts public traffic, and forwards requests to the .NET API running locally. The API never exposes itself directly to the internet — Nginx is the only public face.

Step 1 — Publish the API

First, publish the API as a self-contained release build:

cd ChatApp.API
dotnet publish -c Release -o ./publish

Then zip it for easy upload:

# PowerShell
Compress-Archive -Path ./publish/* -DestinationPath ./chatapp-api.zip

Step 2 — Upload to the server

scp ./chatapp-api.zip root@YOUR_VPS_IP:/var/www/chatapp-api/
⚠️ Gotcha The destination directory must exist before scp runs, or it fails with a cryptic error. Create it first: ssh root@YOUR_VPS_IP && mkdir -p /var/www/chatapp-api

Step 3 — Install .NET 10 on Ubuntu

Ubuntu's package manager doesn't always have the latest .NET version. Use the official install script:

wget https://dot.net/v1/dotnet-install.sh
chmod +x dotnet-install.sh
./dotnet-install.sh --version latest

Then add .NET to the PATH — and here's the critical detail most tutorials miss. The order matters:

export DOTNET_ROOT=$HOME/.dotnet
export PATH=$HOME/.dotnet:$HOME/.dotnet/tools:$PATH

Notice .dotnet comes before the rest of $PATH. If the system has .NET 8 installed globally and you put your .NET 10 path at the end, the system version wins. PATH is first-match wins.

Step 4 — Extract and test

cd /var/www/chatapp-api
unzip chatapp-api.zip
dotnet ChatApp.API.dll --urls "http://0.0.0.0:5246"
info: Microsoft.Hosting.Lifetime[14] Now listening on: http://0.0.0.0:5246 info: Microsoft.Hosting.Lifetime[0] Application started. info: Microsoft.Hosting.Lifetime[0] Hosting environment: Production

When you see that output, the hard part is done.

Step 5 — systemd service

Running dotnet manually works — until you close the terminal. For a real server, you need the process to run forever, survive reboots, and restart automatically if it crashes. That's what systemd is for.

nano /etc/systemd/system/chatapp-api.service
[Unit]
Description=Logic Dynamics Chat API
After=network.target

[Service]
WorkingDirectory=/var/www/chatapp-api
ExecStart=/root/.dotnet/dotnet ChatApp.API.dll --urls "http://0.0.0.0:5246"
Restart=always
RestartSec=10
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_ROOT=/root/.dotnet
Environment=PATH=/root/.dotnet:/root/.dotnet/tools:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin

[Install]
WantedBy=multi-user.target
💡 Why hardcode PATH in the service file? Unlike an SSH session where ~/.bashrc is sourced, systemd services start with a minimal environment. Without explicitly setting PATH, systemd can't find dotnet at all.
systemctl daemon-reload
systemctl enable chatapp-api   # auto-start on boot
systemctl start chatapp-api
systemctl status chatapp-api
● chatapp-api.service - Logic Dynamics Chat API Active: active (running) since Sun 2026-06-14 18:12:07 UTC Main PID: 2092547 (dotnet) Now listening on: http://0.0.0.0:5246

Step 6 — Nginx reverse proxy

server {
    listen 80;
    server_name chat-api.logicdynamics.net;

    location / {
        proxy_pass http://localhost:5246;
        proxy_http_version 1.1;

        # Critical for SignalR WebSocket upgrade
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";

        proxy_set_header Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}
⚠️ SignalR critical headers The Upgrade and Connection headers are non-negotiable for SignalR. Without them, Nginx won't forward the WebSocket upgrade handshake and SignalR falls back to long polling — or fails entirely.
ln -s /etc/nginx/sites-available/chatapp-api /etc/nginx/sites-enabled/
nginx -t        # test config — never skip this
systemctl reload nginx

Step 7 — DNS record

On Hostinger, add a simple A record pointing your subdomain to your VPS IP with a TTL of 300 seconds. Changes propagate in about 5 minutes.

Step 8 — SSL certificate

Free SSL via Let's Encrypt. Certbot handles everything — certificate request, Nginx config update, and automatic renewal:

apt install certbot python3-certbot-nginx -y
certbot --nginx -d chat-api.logicdynamics.net
Successfully received certificate. Congratulations! You have successfully enabled HTTPS on https://chat-api.logicdynamics.net

Certbot automatically updates the Nginx config to listen on port 443 and redirect HTTP to HTTPS. It also sets up a cron job to renew the certificate before it expires.

The update workflow

When you push code changes, the full deployment process takes under two minutes:

# 1. Publish locally
dotnet publish -c Release -o ./publish
Compress-Archive -Path ./publish/* -DestinationPath ./chatapp-api.zip

# 2. Upload
scp ./chatapp-api.zip root@YOUR_VPS_IP:/var/www/chatapp-api/

# 3. On the server
ssh root@YOUR_VPS_IP
cd /var/www/chatapp-api
unzip -o chatapp-api.zip   # -o flag overwrites existing files
systemctl restart chatapp-api

What I learned

The full source code including deployment config is at github.com/LogicDynamics/ChatApp.

← Previous
How I Built a Real-Time Chat App in .NET 10 with SignalR & MAUI
Next →
Crafting a Logic Dynamics Branded UI in .NET MAUI from Scratch
AL
Azim Litanga

Founder of LogicDynamics — .NET engineer, builder, and creator. Passionate about Blazor, Web APIs, AI, and shipping real software. Based in Oklahoma City.