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:
- The JWT secret. Your
JWT_SECRET,ANON_KEY, andSERVICE_ROLE_KEYare 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. - Storage files.
pg_dumpcaptures thestorage.objectsmetadata—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. - The
.envfile. SMTP credentials, OAuth client secrets, custom service config—none of this lives in the database. - Postgres version drift. Your old server might run Postgres 15 while a fresh
docker-compose.ymlpulls 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_SECRETANON_KEYandSERVICE_ROLE_KEYPOSTGRES_PASSWORDVAULT_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:
- 24+ hours before: lower your DNS TTL to 300 seconds so the cutover propagates fast.
- 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. - 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.
