AR
alx richards
all posts
2026.01.21infra6 min read

Deploying .NET on a budget VPS

A practical walkthrough of running a .NET Web API on a $6/month VPS with Nginx, systemd, and a GitHub Actions deploy pipeline.

Not every project needs Kubernetes. Not every API needs a managed cloud. Sometimes you have a .NET Web API, a $6 VPS, and an afternoon.

Here's the setup I've landed on after a few iterations.

The stack

  • Hetzner CX11 — 1 vCPU, 2 GB RAM, 20 GB SSD, €4.15/month. Enough for a handful of low-traffic APIs.
  • Ubuntu 24.04
  • Nginx as reverse proxy
  • .NET 8 runtime (not SDK — runtime only on the server)
  • systemd to manage the process
  • GitHub Actions for deploys

Build on CI, copy to server

The key decision: don't install the .NET SDK on the server. Build a self-contained binary on CI, copy it over with scp, restart the service. The server doesn't need to know anything about .NET internals.

# .github/workflows/deploy.yml
- name: Publish
  run: dotnet publish -c Release -r linux-x64 --self-contained false -o ./publish

- name: Copy to server
  run: scp -r ./publish/* user@your-server:/opt/myapp/

The --self-contained false flag keeps the binary small because the .NET runtime is already installed on the server. If you don't want to install the runtime at all, use --self-contained true — the binary will be larger but has zero server dependencies.

systemd service

Create /etc/systemd/system/myapp.service:

[Unit]
Description=My .NET API
After=network.target

[Service]
WorkingDirectory=/opt/myapp
ExecStart=/opt/myapp/MyApp
Restart=always
RestartSec=10
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=ASPNETCORE_URLS=http://localhost:5000

[Install]
WantedBy=multi-user.target

Then:

systemctl enable myapp
systemctl start myapp

Logs with journalctl -u myapp -f.

Nginx config

server {
    listen 80;
    server_name api.yourdomain.com;

    location / {
        proxy_pass http://localhost:5000;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection keep-alive;
        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;
    }
}

Then certbot --nginx for HTTPS. Takes about 90 seconds.

The deploy step

After the scp, the CI job SSHs in and restarts:

ssh user@your-server "systemctl restart myapp"

Total deploy time from push to live: about 45 seconds.

What this doesn't give you

Zero-downtime deploys. The systemctl restart has a brief gap. For a personal project or internal tool, this is fine. If you need zero-downtime, look at a blue-green setup or just accept that managed PaaS is worth the cost at that point.

For side projects though, this setup has been running for eight months without touching it. That's the real benchmark.

Alexander Richards2026.01.21