After more than 10 years running my websites and webapps on an Azure VM, I've migrated to a Linux VPS for a couple of key reasons:
Cost savings: My Azure VM was initially a free option in 2016, then a cheap option, and nowadays a not-so-cheap option; cost predictability is important to me, and a Linux VPS provides that (so far).
Simplicity: Azure infrastructure is more than just a VM: it's networking interfaces, storage accounts, network security groups, IP addresses, and more. While the fine-grain control is great, Microsoft is constantly upgrading services individually, creating constant work for me.
.NET rocks: Moving to Linux gave me a chance to migrating my .NET Framework apps to .NET 8. In doing so I've learned a ton about cross-platform compatibility in the .NET ecosystem.
Choosing a VPS Provider
This was a rollercoaster. Reducing hosting costs was my primary goal in choosing a VPS host, and any Linux VPS ($5-10/month) was going to beat a Windows Azure VM ($90/month) anyway - so what else mattered?
I also considered other key factors: DDos protection, configurable firewall, support options, minimum specs, US-based location, and more.
What I wasn't expecting: just how difficult it would be to get a US-based data center. Supposedly the AI crunch in 2026 has been causing a major shortage of cheap servers, so it was either host in Europe or wait for capacity.
In the end, I found an aggressively-priced VPS with much better specs than my Azure VM could offer in a US East data center. Huzzah!
The Setup
Moving from an IIS-based Windows Server environment to Linux is shockingly easy. I probably could have containerized my applications, but that's a future endeavour.
My new server is essentially:
- Debian
- nginx for hosting static sites and as a reverse proxy to my .NET applications
- nginx also terminates SSL connections, so all SSL & redirect logic is handled in nginx configs
- .NET web applications running Kestrel
- Certbot for managing/renewing SSL certs
There's no database layer to consider - all my .NET apps now run SQLite, which drastically simplifies deployment, ongoing management, and security.
The Process
This isn't a detailed step-by-step guide for anyone to follow - it's simply the high-level steps I took to migrate everything from a Windows to Linux. This assumes all my applications are Linux compatible (which was a separate multi-month project itself).
Step 1: provision a Linux server & install software
- install nginx
- install .NET runtimes
- harden server (SSH, firewalls, etc.)
Step 2: configure directories
- website, application, and logging directories should be set up across the usual spots in Linux (
/var,/opt, etc.)
Step 3: copy websites & applications to server
- remoting with
rsyncmakes this simple: copy all static and .NET websites to server in a repeatable way - this ends up being my (super-simple) deployment pipeline too:
- an
rsyncscript copies artifacts to the server - another
rsyncscript copies artifacts to production directories on the server
- an
Step 4: configure nginx for static websites
Static websites are simple to serve with nginx; just point a config to the right folders:
- create nginx
.conffiles for each static site under/etc/nginx/sites-available - create symlink to each under
/etc/nginx/sites-enabled - test & reload nginx service as needed
Step 5: configure .NET apps & nginx reverse proxy
My .NET applications and websites run on their built-in Kestrel server and are accessed via a reverse proxy with nginx:
- build each application for Linux (build param:
--runtime linux-x64) - deploy to server
- copy configuration & databases from old production server
- set folder & file permissions for
www-datanginx user (chmodandchownkey directories) - create nginx
.conffiles for each .NET website under/etc/nginx/sites-available- this is a reverse proxy situation; SSL will also terminate here, so the config is different than static sites (but Certbot handles that extra configuration)
- create symlink to each under
/etc/nginx/sites-enabled - configure each website to start on boot
Step 6: set up automatic SSL installation & renewal
This was the easiest part of the entire migration. Certbot is just a couple of commands to get every website running with valid Let's Encrypt certificates.
- Install and configure Certbot to handle automatic SSL certification renewal
And with those basic, high-level steps, I was up and running on a Linux server. I tested this full migration on a local VM and was able to get through it in just a couple of weeks. With a well-documented and vetted process, the final production migration took just a couple of days.
And my wallet feels better for it.