From Azure to One VPS: How LLMs Made Migrating My Whole Side-Project Estate a No-Brainer | Victor A
Post
Cancel
` and `<br>-->
For years my side projects lived on Azure. App Services here, a Static Web App there, a managed SQL database, a sprinkle of Functions, some blob storage, a Communication Services resource for email. Each one made sense the day I created it. Added up over a dozen-plus projects, it became a sprawling estate with a monthly bill that kept quietly creeping — and a mental tax every time I opened the portal and saw resource groups I half-remembered.
This month I moved all of it onto a single VPS. Every API, every static site, every Blazor app, the databases, the object storage, even the transactional email. Azure now shows zero resource groups .
A year ago I wouldn’t have attempted this. It would have been weeks of fiddly, repetitive, error-prone work. But with an LLM coding agent doing the heavy lifting, it turned into something I could knock out in focused sessions between actual product work. That shift is the real story here, so let me walk through what I did, what it cost (and saved), the honest downsides, and why I think this is a no-brainer move for anyone bootstrapping.
The setup
The destination is deliberately boring: one solidly-specced VPS, plus a few free or near-free services stitched around it.
The box runs everything as plain systemd services — .NET APIs, Node/Express static frontends, a couple of Blazor WebAssembly apps, an Azure Functions app re-hosted with the Functions runtime, a containerized screenshot service, and a local SQL Server instance alongside a handful of SQLite databases.
Cloudflare Tunnel handles all ingress. This is the part that changed my life. No open ports, no nginx TLS juggling, no Let’s Encrypt cron jobs, no per-app certificate dance. The tunnel dials out from the box to Cloudflare, and every public hostname is a proxied CNAME pointing at the tunnel. SSL is automatic and free.
Cloudflare DNS for every domain — also free.
Object storage moved to Cloudflare R2 (S3-compatible, no egress fees), with a public bucket bound to a CDN subdomain for images and thumbnails.
Email moved to Amazon SES — pennies per thousand messages, and a single shared module so every app sends through the same path.
Backups : a weekly full-machine snapshot plus nightly database dumps shipped off-box to cheap object storage.
The whole public surface is fronted by Cloudflare, so I also get caching, DDoS protection, and analytics thrown in without lifting a finger.
What actually got migrated
The variety is the point — this wasn’t one app, it was a zoo:
Several .NET APIs (some framework-dependent, some self-contained), each now a systemd unit on its own loopback port.
A pile of static and SSR frontends — marketing sites, job boards, a music app, a couple of niche tools — served by tiny Express processes.
Two Blazor WebAssembly apps that were on Static Web Apps.
An Azure Functions app (the multi-LLM comparison backend for one of my products) re-hosted with the Functions Core Tools runtime — same code, just self-hosted.
Databases : Azure SQL → a local SQL Server; several SQLite files pulled down and dropped into place.
CI/CD : every repo’s GitHub Actions workflow rewritten from “deploy to Azure” to “rsync to the box and restart the service.”
Each migration followed the same rhythm: build, copy, write a systemd unit, add a tunnel ingress rule, flip the DNS record, delete the Azure resource. Once you’ve done it twice, it’s a template. And templates are exactly what an LLM agent is brilliant at applying — fast, consistently, and without getting bored on the fifteenth repetition.
Why now: the LLM difference
Here’s the honest truth: the individual steps in a migration like this aren’t hard. They’re just numerous, repetitive, and unforgiving of small mistakes. A wrong port, a stale DNS record, a forgotten environment variable, a build that targets the wrong runtime — any one of them costs you twenty minutes of head-scratching, and there are hundreds of them across a dozen apps.
An LLM coding agent collapses that. I could say “migrate the next frontend the same way we did the last one,” and it would inspect the project, figure out the build, write the deploy server, wire up the service, and run the smoke tests — surfacing only the genuinely novel decisions for me to make. The work shifted from typing to deciding. I stayed in the architect’s seat; the agent did the plumbing.
It also caught and remembered the gotchas so I didn’t keep re-stepping on them — things like a static-host’s hostname binding silently blocking a DNS cutover until the old resource is deleted, or a dev-tool reverse proxy quietly dropping query strings, or an OAuth provider needing its redirect URI updated to the new hostname. Those are the kinds of papercuts that turn a clean afternoon into a frustrating week. Having them spotted, fixed, and noted as we went is what made the whole thing feel...