Skip to content
Back to writing

There's no such thing as "just move it to a VPS"

Moving a site to a VPS sounds like an afternoon of work. Why cheap code, old systems and real infrastructure make it a much bigger job.

In this note
  1. The code got cheap
  2. The app is one piece of the website
  3. Old systems have history
  4. Infrastructure doesn't care about your framework
  5. Reference code and AI shrink the known work
  6. Vibe coding still meets reality
  7. So yes, move it to a VPS

"Just move it to a VPS" sounds like an afternoon. Take the old site, rewrite what needs rewriting, copy the data, point the domain at a new server, done. These days it even feels easier than it sounds: I usually have a reference codebase with the same architecture, a frontend stack I trust, deployment configs I've already debugged once, and an AI agent that will happily write half the code.

This year I did two of these migrations back to back. Neither took an afternoon.

The code got cheap

This is the funny part. With a decent reference project and an agent, rebuilding a client site is absurdly fast compared to a few years ago. Components, repetitive templates, adapters, API cleanup, translating old patterns into a modern stack: most of that turns into reviewing diffs. On one of those projects I went from a written spec to a database schema, an importer, and the first parsers on day one. At that point it's hard not to think the whole thing will be done by the weekend.

Then you deploy it.

The app is one piece of the website

A production website isn't a folder of code. It's DNS, certificates, redirects, mail records, caches, reverse proxies, databases, file permissions, old URLs that search engines still send people to, forgotten subdomains, cron jobs, analytics, a hosting panel's idea of configuration, and sometimes a piece of software nobody has touched in years because everyone is scared of it. None of that goes away because the new app is cleaner, and all of it has opinions about your deploy.

Here's what that looked like in practice:

  • The site worked perfectly in development and got stuck in an infinite redirect loop in the production build, but only on the root URL of the default language.
  • nginx remembered the IP of a container, so after a restart it kept sending traffic to a container that no longer existed. Hello, 502.
  • Logged-in requests to the admin panel were silently dropped, because the auth cookies were bigger than nginx's default header buffers.
  • A database connection pool of 100 per container, times four containers, against a database that allows 200 connections. Everything was fine right up until the editors actually started working.
  • The production Docker image built without complaint, then failed on every admin page because the translation files never made it into the image.

None of these are hard to fix. All of them are invisible from localhost.

Old systems have history

Legacy systems are rarely complicated on purpose. They get complicated because things happened: a domain moved, someone added a redirect five years ago, another developer reorganised the directories, a plugin quietly depended on something undocumented, a temporary workaround became permanent, and a hosting panel generated configuration nobody remembers writing. After enough of that you're not really replacing code anymore. You're reconstructing years of assumptions.

My favourite example: one of the old sites stored every language version of a page as a separate record, loosely linked together. Merging them into one record per page was the right call, and it also quietly broke about fifty URLs in one language. The only reason I caught it was that I walked every old URL against the new app before switching over.

Infrastructure doesn't care about your framework

Next.js doesn't care that your VPS has almost no memory. Docker doesn't care that the network between your server and the registry is flaky. Your CI pipeline doesn't care that production lives behind some provider-specific limitation. And your users definitely don't care that a deploy failed because an image couldn't be pulled. They care that the site is down.

As far as I can tell, most of production engineering is discovering how many assumptions your development environment was quietly making for you.

Reference code and AI shrink the known work

Having a project with the same architecture is fantastic. So are AI agents, reusable Dockerfiles, shared components, tests, and infrastructure you already understand. Together they can turn days of coding into hours. But what they shrink is the known work, and the expensive part of a migration is the unknown work: what this domain actually points to, why this redirect exists, which old URLs still get traffic, what breaks if you turn this off, where a setting is really coming from, and why something works on one server but not the other.

An agent is genuinely useful for digging into all of those. It just can't make the questions stop existing.

Vibe coding still meets reality

You can vibe-code a big part of a migration, and I do. Give an agent a reference implementation, describe the result you want, review what comes back, test it, iterate, and an absurd amount of application work disappears. But the last 20% is where it stops being abstract. There's a real server, a real network, real historical data, real users, and usually one extremely stupid thing nobody could have predicted.

The best defence I've found is boring. Dump the databases and tag rollback images before touching anything, bring the new stack up next to the old one on different ports, check it there, and only then swap. On one of these projects that meant the whole cutover happened in a single night, with the old containers still sitting there in case I needed them.

So yes, move it to a VPS

Just don't call it just moving it to a VPS. The new code may well be the easiest part. The actual job is understanding everything around it well enough that when you replace one piece, the rest of the system doesn't notice. So far no amount of AI, automation, reusable infrastructure, or aggressive vibe coding has changed that.

Next noteKIMEP lost its app, so I wrote a new one