Moving Self-Hosted Supabase to a New Server: Full Guide

Migrate your self-hosted Supabase instance to a new server without losing data. Database, storage files, secrets, and DNS cutover—step by step.

Cover Image for Moving Self-Hosted Supabase to a New Server: Full Guide

Sooner or later, every self-hoster faces it: your VPS provider raises prices, your project outgrows its 4GB droplet, or you want to move from x86 to a cheaper ARM instance. Now you need to move your self-hosted Supabase instance—database, storage files, auth users, secrets, and all—to a new server without breaking every client that talks to it.

This is different from migrating from Supabase Cloud to self-hosted. Host-to-host migration is actually simpler in some ways (you control both ends) and more dangerous in others (one wrong move with your JWT secret and every user gets logged out—or worse, your API keys stop working entirely).

This guide walks through the full migration: what to copy, what absolutely must stay identical, how to cut over DNS with minimal downtime, and how to roll back if things go sideways.

Why Server Migrations Go Wrong

The database is the part everyone remembers. Here's what people forget, roughly in order of how often it ruins someone's week:

  1. The JWT secret. Your JWT_SECRET, ANON_KEY, and SERVICE_ROLE_KEY are derived from each other. If you regenerate them on the new server, every deployed app, mobile client, and API integration breaks instantly. These must be copied byte-for-byte.
  2. Storage files. pg_dump captures the storage.objects metadata—not the actual files. Restore the database without the files and every download returns a 404. We covered this trap in detail in Self-Hosted Supabase Storage Backup: The Forgotten Piece.
  3. The .env file. SMTP credentials, OAuth client secrets, custom service config—none of this lives in the database.
  4. Postgres version drift. Your old server might run Postgres 15 while a fresh docker-compose.yml pulls Postgres 17. As of the week of June 15, 2026, Supabase's default self-hosted image moved from PG 15 to PG 17—if you're migrating right now, this will bite you unless you pin versions. See our June 2026 breaking changes prep guide for the details.

Step 1: Inventory the Old Server

Before touching anything, catalog what you're moving:

# What Postgres version are you actually running?
docker exec supabase-db psql -U postgres -c "SELECT version();"

# How big is the database?
docker exec supabase-db psql -U postgres -c \
  "SELECT pg_size_pretty(pg_database_size('postgres'));"

# How much storage data do you have?
du -sh ./volumes/storage

# Which services are you running?
docker compose ps --format '{{.Name}}'

Note the Postgres major version. You'll pin the same version on the new server first, and treat any major version upgrade as a separate step after the migration succeeds. Doing both at once means that when something breaks, you won't know which change caused it. (When you're ready, we have a dedicated guide for upgrading self-hosted Supabase versions.)

Step 2: Provision the New Server

Set up Docker and clone the Supabase repo on the new host, but don't start the stack yet. If you're shopping for a new home, our VPS provider comparison for self-hosted Supabase covers the current landscape, and the system requirements docs list minimum specs.

Pin the Postgres image to match your old server in docker-compose.yml:

db:
  image: supabase/postgres:15.8.1.085  # match your OLD server's version

One honest note: a migration is a great excuse to not reuse a crusty two-year-old docker-compose.yml. Pull the current repo, then port your customizations over deliberately rather than copying the whole file. You'll pick up two years of upstream fixes that way.

Step 3: Copy Secrets First

Copy the entire .env from the old server and keep these values identical:

  • JWT_SECRET
  • ANON_KEY and SERVICE_ROLE_KEY
  • POSTGRES_PASSWORD
  • VAULT_ENC_KEY (if you use Vault—encrypted secrets are unrecoverable without it)
  • SECRET_KEY_BASE

Values that should change: anything host-specific like SITE_URL or API_EXTERNAL_URL—but only at cutover time. Set them to the same domain as before if you're keeping your domain (you almost certainly are), and the change is zero.

Step 4: Migrate the Database

Take a fresh logical backup on the old server:

docker exec supabase-db pg_dumpall -U postgres \
  --clean --if-exists > supabase_full_$(date +%F).sql

For databases over ~10GB, use pg_dump with the custom format (-Fc) per database instead—it's compressed and supports parallel restore with pg_restore -j 4.

Transfer and restore on the new server:

rsync -avz --progress supabase_full_2026-06-12.sql newserver:/root/

# On the new server: start ONLY the db service first
docker compose up -d db
cat supabase_full_2026-06-12.sql | docker exec -i supabase-db psql -U postgres

Expect some role-related errors on restore (role "supabase_admin" already exists)—these are normal with pg_dumpall against a pre-initialized Supabase image, because the image creates its own internal roles. Errors about your schemas, tables, or data are not normal. Read the output carefully rather than assuming green.

Step 5: Migrate Storage Files

If you use the default file backend, rsync the volume directly:

rsync -avz --progress ./volumes/storage/ newserver:/root/supabase/docker/volumes/storage/

If you're on S3-compatible storage (MinIO, R2), this step might be a no-op—both servers can point at the same bucket. That's one of the strongest arguments for external S3 storage: your files stop being welded to a single machine.

Step 6: Cut Over DNS

This is where downtime happens, so stack the deck:

  1. 24+ hours before: lower your DNS TTL to 300 seconds so the cutover propagates fast.
  2. At cutover: stop the old stack (docker compose stop), take one final incremental sync of database and storage to catch changes since your last copy, start the full stack on the new server, and flip the DNS A record.
  3. Verify before celebrating:
# Auth issues tokens?
curl https://api.yourdomain.com/auth/v1/health

# REST API responds with your real anon key?
curl https://api.yourdomain.com/rest/v1/ -H "apikey: YOUR_ANON_KEY"

# Storage serves an actual file?
curl -I https://api.yourdomain.com/storage/v1/object/public/bucket/somefile.png

Realistic downtime for the stop-sync-start-flip sequence: 5–30 minutes depending on data size. If you need true zero-downtime, that requires logical replication between old and new servers—doable, but a different weight class of complexity (our zero-downtime deployments guide covers the building blocks). For most projects, a planned 15-minute window at 3 a.m. is the pragmatic answer, and pretending otherwise is how migrations get postponed for a year.

Keep the old server running (but stopped) for at least a week. It's your rollback: flip DNS back, docker compose up -d, and you've lost only the data written during the failed cutover window.

Where Supascale Fits

Everything above is manual, ordered, and unforgiving—miss one step and you find out in production. This is exactly the workflow Supascale was built to compress.

With Supascale on both servers, a migration becomes: take an automated S3 backup of database and storage together, then restore it with one click on the new instance. Custom domains and SSL certificates are reconfigured from the dashboard instead of hand-editing Kong and Caddy configs. And because it's a one-time $39.99 purchase with unlimited projects, the new server doesn't cost you a second license.

What Supascale doesn't do: it won't lower your DNS TTL for you, and it can't make a 200GB transfer faster than your bandwidth allows. The orchestration gets easier; physics stays the same.

Conclusion

Host-to-host migration is mostly a backup-and-restore exercise with two non-negotiables: keep your JWT secret and API keys identical, and move your storage files alongside the database. Pin the Postgres version, rehearse the restore before cutover day, keep the old server warm as a rollback, and treat version upgrades as a separate project.

If you'd rather not script this by hand, try Supascale—automated backups and one-click restore turn migration day from a checklist into a button.

Further Reading