Post

From Azure to One VPS: How LLMs Made Migrating My Whole Side-Project Estate a No-Brainer

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 light.

The economics

This is the part that matters when you’re bootstrapping and every euro is runway.

On Azure I was paying for multiple App Service plans, a managed SQL database, storage, and assorted small line items — the kind of bill that’s “not that much” per service but very much something in aggregate, every single month, forever. Consolidating onto one VPS replaced most of that recurring spend with a single predictable monthly cost — and that one box has more CPU and RAM than the sum of the tiers it replaced. My side projects are faster now, not slower, because they’re not throttled by entry-level cloud SKUs.

Add in the free tiers around it — Cloudflare DNS, Tunnel, CDN, and SSL all cost nothing; R2 has no egress fees; SES is effectively free at my volume — and the math stops being close. It’s lopsided. I cut recurring cost meaningfully and got better performance. When you’re funding your own runway, that combination is rare enough to take seriously.

The honest con: single point of failure

I’m not going to pretend this is free of trade-offs. The obvious one: it’s a single box. If it goes down, everything goes down together. On Azure, a problem with one service was contained to that service.

So I mitigate rather than ignore it:

  • Weekly full-machine snapshots — a bad day means restoring an image, not rebuilding from memory.
  • Nightly off-box database backups with retention, so the data survives even if the box doesn’t.
  • Infrastructure-as-muscle-memory: because every service is a systemd unit and every deploy is a scripted GitHub Action, standing the whole thing back up on a fresh box is a known, repeatable procedure — not an archaeology project.
  • Uptime monitoring on every public hostname so I hear about problems before my users do.

For a portfolio of side projects and an early-stage startup, “one well-backed-up box that I fully control” is a perfectly reasonable risk posture. I’m optimizing for cost, speed, and ownership while I find product-market fit — not for five-nines on day one. When a project earns the right to high availability, it can graduate to something more elaborate. Most never need to.

Why this is a no-brainer while bootstrapping

Put it together and the case writes itself:

  • Lower, predictable cost instead of a creeping multi-service bill.
  • More performance per dollar than entry-level managed tiers.
  • Full control — I own the box, the runtime, the deploy path, and the data. No surprise deprecations, no vendor-specific lock-in, no portal spelunking.
  • Free, best-in-class edges — Cloudflare for DNS/TLS/CDN/tunnel, R2 for storage, SES for email — so “self-hosted” doesn’t mean “exposed and fragile.”
  • A weekend of effort, not a quarter — because the LLM does the repetitive 90% and you do the thoughtful 10%.

The single-point-of-failure caveat is real, but it’s a managed risk, not a dealbreaker — and the snapshot-plus-backup safety net is cheap.

If you’ve got a graveyard of cloud resources quietly draining your runway, this is genuinely a great moment to reclaim it. The tooling — both the infrastructure side (tunnels, S3-compatible storage, managed DNS) and the AI side (a coding agent that can actually do the migration with you) — has never been this good. Big, scary, repetitive infra migrations have quietly become a thing one person can do in their spare time.

I took back control of my costs, my performance, and my stack. The portal’s empty now. It feels great.

Building in public while bootstrapping. If you’re weighing a similar move and want to compare notes, I’m around.

This post is licensed under CC BY 4.0 by the author.
SpaceX Launch Countdowns Starship Flight 12 Countdown