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.