← Blog

Move off Heroku Postgres without losing data

Railyard

April 7, 2026 · 8 min read

Follow this on your own server. Start free, no card needed

Heroku stopped offering a free Postgres tier a while back, and the cheapest plan that’s left, Essential-0, still caps you at 1 GB of storage, 20 connections and a 4,000 table limit for $5 a month. That’s fine for a toy app. It’s not fine for anything that’s actually grown, and it’s not fine for the price once you compare it to running the same database somewhere else. The part that stops most people isn’t the decision, it’s the fear of breaking the data on the way out: dropping rows, restoring into the wrong encoding, or cutting over before the new database is actually ready. By the end of this guide you’ll have a full, verified copy of your Heroku Postgres data running on a new database, your app pointed at it, and the old one still there as a safety net until you’re sure.

What you’ll need

  • The Heroku CLI installed and logged in (heroku login), with access to the app whose database you’re migrating.
  • A destination Postgres server. This guide uses a DigitalOcean Basic Droplet at $6/month (1 vCPU, 1 GiB RAM, 25 GB SSD) running Postgres in Docker, which comfortably fits a database that was living inside a 1 GB Heroku Essential-0 plan. Use a bigger box if your data is bigger.
  • pg_dump and pg_restore on your machine, matching (or newer than) your Heroku database’s Postgres version. The Postgres.app or postgresql-client package on your OS gives you both.
  • A short maintenance window isn’t strictly required, but it helps to pick a quiet time, since you’ll re-point DATABASE_URL partway through.
  • About 10-15 minutes, more if your database is large or your connection is slow.

Step 1: Capture a fresh backup on Heroku

Don’t restore from whatever automatic backup Heroku happens to have lying around. Take one right now, so you know exactly what point in time you’re migrating.

heroku pg:backups:capture --app your-app-name

This triggers a new backup on Heroku’s side, in Postgres’s custom compressed dump format. It can take anywhere from a few seconds to several minutes depending on the size of your database. Check on it with:

heroku pg:backups --app your-app-name

You should see a row for the backup you just captured, with a status of Completed and a size roughly matching what you’d expect for your database. If it still says Pending or Running, wait and check again before moving on, a backup that’s still capturing will fail to download.

Step 2: Download the backup

Once the capture finishes, pull it down to your machine:

heroku pg:backups:download --app your-app-name

This saves a file named latest.dump (or the specific backup ID if you passed one) in your current directory. If you’d rather hand the file to something else instead of downloading it yourself, heroku pg:backups:url --app your-app-name gives you a temporary public URL to the same dump, valid for about an hour.

You should see a latest.dump file on disk, with a size in the same ballpark as what heroku pg:backups reported.

Step 3: Set up the destination Postgres database

If you’re moving to a self-hosted Postgres instance rather than another managed provider, a small Docker Compose file gets you a running database fast:

services:
  postgres:
    image: postgres:16.4-alpine
    restart: unless-stopped
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: change-me
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  pgdata:

Bring it up with:

docker compose up -d

You should see the container running (docker compose ps) and be able to connect with psql -h your-server-ip -U appuser -d appdb. If you’re pointing at a managed Postgres from another provider instead, just make sure you have its connection string and that it accepts connections from your IP before moving on.

Step 4: Restore the dump into the new database

pg_restore reads the same custom format Heroku’s backup is already in, so there’s no conversion step:

pg_restore --verbose --clean --no-acl --no-owner \
  -h your-server-ip -U appuser -d appdb latest.dump

--clean drops any conflicting objects before recreating them, which matters if you’ve already run migrations against an empty database and it has tables that would otherwise collide with the restore. --no-acl and --no-owner skip Heroku-specific role and permission references that don’t exist on your new server and would otherwise throw errors you can safely ignore.

You should see a long stream of CREATE TABLE, CREATE INDEX and COPY lines scroll past with no fatal errors at the end. A few already exists or does not exist, skipping warnings are normal and not a sign anything went wrong.

Step 5: Verify the data actually matches

Before you touch DATABASE_URL, check that what came across is complete. Compare row counts table by table between Heroku and the new database:

SELECT schemaname, relname, n_live_tup
FROM pg_stat_user_tables
ORDER BY relname;

Run this against both databases (connect to Heroku with heroku pg:psql --app your-app-name) and diff the results. For anything your app treats as financially or legally important, spot-check a handful of specific rows by primary key rather than trusting counts alone.

You should see matching row counts for every table, with no table present on one side and missing on the other.

Step 6: Point your app at the new database

Once you’re confident the data matches, update whatever holds your DATABASE_URL, your .env file, your new host’s config vars, your secrets manager, to the new connection string, and redeploy. If your new destination needs sslmode=require or similar, set that in the connection string now rather than discovering it at runtime.

You should see your app’s next deploy come up successfully and read/write against the new database. Watch your error logs for a few minutes after the deploy for anything connection-related.

Step 7: Keep Heroku around briefly, then retire it

Don’t delete the Heroku app or downgrade its database the moment the cutover looks fine. Leave it running, untouched, for a day or two as a fallback you can switch back to if something surfaces that your row-count check missed. Once you’re confident, scale the Heroku dyno and database down, then remove them.

The shortcut: do it with Railyard

Everything above is the manual path, worth knowing because it’s what’s actually happening underneath any tool that claims to do this for you. Railyard has two ways to skip most of it.

If you’re moving the whole app, not just the database, Applications → Import reads your app directly from Heroku using an API token: config vars, the formation (your process types and counts), and any Postgres, Redis, MySQL or Memcached add-ons come across automatically, along with custom domains. You still add the Git URL yourself, since Heroku’s API doesn’t expose your repository. Heroku import is currently in beta, meaning it hasn’t been run against enough real accounts yet to call it fully proven, so Railyard shows you the plan for your specific app before anything happens, and you check it over before confirming. The database itself is brought across by streaming a dump straight from your Heroku DATABASE_URL into the new managed database, and the old Heroku app keeps serving traffic until the new URL answers and you’ve moved DNS yourself.

If you only want to move the database and you’re keeping your app and deploy pipeline where they are, the standalone Import data flow does the database half of that same job on its own: paste in your Heroku Postgres connection URL (the same one heroku config:get DATABASE_URL gives you), and Railyard pulls the data directly from it. It backs up whatever’s currently in the destination database first, so if the import fails or you cancel partway through, your existing data is restored automatically rather than left half-overwritten. If you choose to deploy after the import finishes, it runs your app’s migrations for you as the last step.

Either way, your Heroku heroku config -s output pastes straight into Railyard’s Variables → Bulk import, so your environment variables don’t need to be re-typed by hand.

Full details on both flows: Importing from another platform and Databases and backups. If you’re still deciding whether to move at all, the complete walkthrough is at Migrating from Heroku, and our Heroku bill checker will tell you roughly what you’re paying today versus elsewhere.

Troubleshooting

heroku: command not found or authentication errors. Make sure the CLI is installed and run heroku login again; a stale or expired session shows up as a permissions error on commands that otherwise look correct.

The backup download fails or returns an empty file. This almost always means the capture from Step 1 hadn’t actually finished. Re-check heroku pg:backups --app your-app-name and wait for Completed before downloading or requesting a URL.

pg_restore errors about objects that already exist. This happens when you restore into a database that already has conflicting tables, usually one where migrations already ran. Use --clean as shown above, or restore into a genuinely empty database and run your own migrations instead of the dump’s schema.

SSL or sslmode connection errors. Heroku Postgres requires an SSL connection for direct pg_dump/psql access; if you’re dumping straight from Heroku rather than from a downloaded backup file, append ?sslmode=require to the connection string. Your destination database may have the opposite problem, refusing SSL entirely on a fresh local install, in which case drop the parameter for that side.

Your app is still writing to the old database after cutover. This is almost always a cached DATABASE_URL, a secrets manager value that didn’t actually redeploy, or a background worker that didn’t restart alongside the web process. Check every process type separately, not just the one serving HTTP traffic.

FAQ

Does this cause downtime? Not if you follow the order above. The Heroku app keeps serving traffic through the backup, restore and verification steps; the only moment that matters is when you flip DATABASE_URL, which is typically a single redeploy.

What happens to my Heroku app while I migrate? Nothing, it keeps running exactly as before. The backup capture reads from a replica-like snapshot process and doesn’t lock your production tables.

Can I migrate without the Heroku CLI? Only partially. You can get a connection string from your app’s Config Vars in the Heroku dashboard and run pg_dump directly against it, but capturing and downloading a backup specifically requires the CLI (or Railyard pulling from the live URL directly, which needs no CLI at all).

Do I need to match Postgres versions exactly? No, but your destination should be the same major version or newer. Restoring a newer Heroku Postgres dump into an older destination can fail on features that didn’t exist yet in the older version.

What if my database is bigger than fits in a quick backup/restore cycle? The process is the same, it just takes longer. Budget more time for Steps 1 and 4 specifically, and consider running the restore on a box with more RAM temporarily if pg_restore is slow.

Whichever path you take, manual or through Railyard, the discipline that actually protects your data is the same: capture before you touch anything, verify before you cut over, and don’t delete the old copy until you’ve had time to be sure.

Why people pick Railyard for this

  • Your own cloud account. Deploy to DigitalOcean, Hetzner, Vultr, AWS, OVHcloud, or any Linux server over SSH, billed directly by your provider with no markup.
  • One flat price per team. Free is free forever: one server plus a database server, three apps, no card. Pro is $19 a month for up to five servers and unlimited apps. Team is $49 a month with unlimited servers. No per-app, per-deploy, per-seat or per-gigabyte charges.
  • One-click templates. Deploy open-source apps, like databases, analytics and dev tools, to your own server in one click.
  • Managed databases. Postgres, MySQL and Redis with scheduled, restore-verified backups and point-in-time recovery.
  • Push-to-deploy. Deploy from GitHub, GitLab or the CLI, with automatic HTTPS and zero-downtime releases.

Skip the dump and restore

Paste your Heroku Postgres connection URL into Railyard and it backs up, streams the data in, and migrates for you.

Free plan, no card, and 14 days of Pro for every new team. One flat price per team.