Moved an entire development/staging platform (a couple dozen web apps, a self-hosted database stack, a secrets service, and CI) onto new hardware as a parallel build, with negligible downtime and a one-command rollback preserved for a week.
The challenge
The development/staging tier of my self-hosted platform ran 24/7 on an aging laptop, a genuine thermal risk and a single point of failure for everything downstream. On it lived a couple dozen browser and backend apps, a full self-hosted database stack, a centralized secrets service, a self-hosted CI runner, an email-ingest service, and a batch of host-managed background jobs and timers.
The box needed to be retired, but it couldn't just be "moved." Everything on it was live. The migration had to:
- Stand up a complete twin on new hardware (a mini-PC) and cut over with negligible downtime.
- Lose nothing: data, credentials, and configuration had to survive intact.
- Keep a real rollback available for about a week, not a leap of faith.
- Stay legible: every irreversible action explained in plain English and gated on explicit approval.
Approach
Three strategic choices did most of the work:
- Parallel build, destroy nothing. The new box was assembled as a full twin while the old box kept serving. Nothing became irreversible until a single, late cutover step.
- Same-role rebuild, not a promotion. Because the new box took the same role as the old one, every credential, machine-scoped secret, and line of app config stayed byte-identical. That collapsed what is normally a risky re-credentialing exercise into a plain copy.
- Gate at every phase. The work was decomposed into numbered phases (platform → golden-path proof → batch → special services → cutover → monitoring → decommission), each with a verification gate. Announce the phase, execute in order, stop on any failure rather than improvise.
A clarifying insight framed the whole cutover: a machine has two identities: a cosmetic OS hostname and a functional name on a private mesh VPN. Only the functional name matters, and only one machine can hold it. The entire migration pivots on reassigning that one name, and it does not auto-follow an OS rename.
Execution highlights
- Platform first, proven before batch. I restored the database (per-schema, carrying keys verbatim), rebuilt the secrets service, and registered the CI runner, then proved one app of each deployment template end-to-end (a static site, a build-and-serve SPA, and a full backend with a health check that verifies the deployed commit) before replicating across the rest.
- Handled contention deliberately. Services that consume shared external state (an email mailbox, a nightly data sync, the CI job queue) were installed but left disabled during the parallel window so the two boxes never fought, then were handed off cleanly at cutover.
- Cutover as a coordinated, reversible pivot. Froze the source, took a final delta backup (confirmed zero changes), moved the VPN name to the new box, verified the apps went green, then powered the old box down with its disk intact as the rollback.
Results
- Every web app, plus database, secrets, CI, and the email service, migrated; every service healthy on the new hardware; monitoring (uptime + host metrics) re-pointed and green.
- Zero data loss: the final backup diff showed no changed rows; the database matched exactly.
- Zero re-credentialing: the same-role strategy kept every secret and app config byte-identical.
- Rollback preserved: a ~30-second revert (swap the VPN name back, power on) stayed available for a full week.
- Latent issues found and fixed en route, not in production: a mis-permissioned config file that had silently broken an app on the old box; a dependency-drift break (a fresh install pulling a newer, incompatible library version) caught and pinned; and an unnecessary public-internet exposure deliberately closed rather than replicated forward.
What this demonstrates
- A "byte-identical same-role rebuild" is the biggest de-risker when the hardware changes but the role doesn't: you skip credential rotation and copy instead.
- Proving the platform plus one of each template before batching turns a scary multi-app migration into a boring one.
- Naming the two identities explicitly (cosmetic hostname vs. functional network name) makes the cutover a single, well-understood pivot instead of a mystery.
- A rollback should be physical and boring: old box off, disk intact, name reassignable. No heroics required if something goes wrong.