ApproidBlogInfrastructure
Nov 27 2025Infrastructure

Migrating a client from Heroku in two weekends.

A Postgres dump, a fly.io app, two long Saturdays. Total downtime: 40 seconds.

A Postgres dump, a fly.io app, two long Saturdays. Total downtime: 40 seconds. Here's the playbook we used to migrate a client off Heroku without theatrics.

The setup

Mid-size Rails app, ~200 GB Postgres, 3 background-worker processes, a handful of cron jobs. Heroku bill had crept past what the client was willing to renew, and the dyno hours weren't the actual cost driver - the addons were.

The plan

  1. Saturday one - stand up the new infra (managed Postgres on Hetzner, app on fly.io, Redis from Upstash). Mirror config. Run a dry-run deploy. Replicate Postgres in real-time.
  2. Saturday two - flip DNS, drain Heroku queue, promote the replica, smoke test, monitor.

What went right

The Postgres replica was the secret. By Saturday two, the new database had been live-replicating for five days. The cutover was a 40-second window where we paused writes, finalised replication lag, and flipped the read/write target.

What went wrong

One cron job pointed at a Heroku-internal metadata endpoint we hadn't mirrored. We caught it in monitoring 12 minutes after cutover and patched it in a hotfix. No user-visible impact.

The lesson

The unsexy migration is the migration that succeeds. We didn't reinvent the deploy story; we copied the existing one and changed the IPs.