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
- Linux is just another tool. There are only about 15 commands you need for basic VPS management. Once you've done it once, it becomes muscle memory.
- systemd is your friend.
journalctl -u chatapp-api -f gives you live logs — essential for debugging production issues.
- Nginx is a proxy powerhouse. One instance can front multiple APIs, handle SSL for all of them, and add rate limiting and caching.
- SSL is free and easy. There's no excuse for HTTP in 2026. Certbot makes it a two-minute job.
The full source code including deployment config is at github.com/LogicDynamics/ChatApp.