If you’re reading this, you’re probably leaving Railway this week. Maybe it’s the October 21 security incident and the paused deploys. Maybe it’s a usage bill you didn’t expect. Either way, the good news is that Railway doesn’t lock you in much. Your code is in GitHub, your database speaks standard Postgres, and your config is a list of env vars.
This guide moves a typical Railway app (a web service plus Postgres) to InstaPods, step by step. Most of it applies to any host you pick.
Short version: list your variables, dump Postgres with pg_dump, deploy the repo with instapods deploy --repo, add a database and restore, set fresh keys with instapods env set, move your domain, then revoke the old keys and delete the Railway project.
Make the migration your secret rotation
Don’t copy your old API keys across. If you’re moving because of the incident, those keys are the problem.
Do it in this order instead:
- Create new keys at each provider (OpenAI, Stripe, AWS, and so on).
- Put the new keys only on the new host.
- Cut over.
- Revoke the old keys.
You end up rotated and migrated in one pass, and the old values that may have leaked stop working once you revoke them in step 4. If you can’t cut over today, revoke your AI and cloud keys first anyway: a few hours of downtime is cheaper than a leaked key. The rotation checklist has the full list of what to rotate, including the JWT and session secrets people forget.
What you’re actually moving
| Piece | Where it is on Railway | How it travels |
|---|---|---|
| Code | Your GitHub repo | Point the new host at the same repo |
| Env vars | Each service’s Variables tab | Re-enter (with new values for secrets) |
| Postgres / MySQL data | A Railway database service | pg_dump / mysqldump, then restore |
| Redis | A Railway database service | Usually a cache: start empty |
| Files on a volume | A Railway volume | Copy them out with railway ssh or your app’s own export |
| Custom domain | DNS pointing at Railway | Re-point the CNAME |
| Cron jobs | A cron service | A cron entry on the new server |
Step 1: Write down your variables
Open each Railway service’s Variables tab, or run railway variable list in the linked project. Copy the names into a file. For each one, note whether it’s:
- Config (
NODE_ENV,LOG_LEVEL, feature flags): copy the value as is. - A secret (API keys, tokens, signing secrets): you’ll make a new one.
- Railway-provided (
RAILWAY_PUBLIC_DOMAIN,RAILWAY_ENVIRONMENT_NAME,RAILWAY_PRIVATE_DOMAIN, or references like${{Postgres.DATABASE_URL}}): these won’t exist on the new host. Search your code forRAILWAY_and replace each with a plain variable you set yourself. PORT: Railway injects it. On a repo deploy, InstaPods setsPORTfor you too (3000 on Node.js, 8000 on Python, 8080 on Go), so code that readsprocess.env.PORTkeeps working. Make sure you bind to0.0.0.0, notlocalhost.
Step 2: Dump your Postgres database
Railway’s backup guide gives two ways in. The simplest uses the database’s public URL (find DATABASE_PUBLIC_URL on the Postgres service):
pg_dump "$DATABASE_PUBLIC_URL" --format=plain --no-owner --no-acl --file=railway.sql
Railway bills traffic over the public proxy as egress, so for a big database the tunnel below is cheaper.
If you don’t want to enable the public proxy, open a tunnel instead. Run railway login first (Railway invalidated old sessions), then run this in a linked project directory and leave it open:
railway connect postgres --tunnel-only
It prints a local host, port, user and password. Run pg_dump against those in a second terminal.
Check the version before you go further. InstaPods runs PostgreSQL 16, and Postgres only guarantees that dumps restore into the same or a newer major version. Run SELECT version(); on your Railway database:
- 16 or older: the version is fine. Still run
\dxto list extensions: anything beyond the basics (pgvector, PostGIS) has to exist on the new server too. - 17 or newer: a plain SQL dump of a typical app schema (tables, indexes, foreign keys) usually restores, but not always. Remove any
SETline the older server rejects (for exampleSET transaction_timeout = 0;from a Postgres 17 dump) and test the restore before you cut over. If your schema or queries use features added after 16, such asJSON_TABLEorMERGE ... RETURNING, plan for that.
I’d rather you hear that here than halfway through a cutover.
Step 3: Deploy your repo
Install the CLI and log in:
curl -fsSL https://instapods.com/install.sh | sh
instapods login
Then deploy straight from GitHub. You need the Build plan ($7/mo) or higher for a database:
instapods deploy my-app --repo https://github.com/you/your-repo --plan build
InstaPods detects the stack from the repo (Node.js, Python, PHP, Go or static), creates the pod, builds it and follows the first build to completion. Every push to the branch redeploys it after that, the same way Railway did. For a monorepo, add --subdir services/api. For a branch other than the default, add --branch.
No Dockerfile needed. It runs as a normal service on a real Linux server, and you can instapods ssh my-app into it any time.
Railway service runs a Docker image? Run the same image
If your Railway service deploys an image you already publish (say your CI pushes to ghcr.io), skip the repo build and run that image as is:
instapods deploy my-app --image ghcr.io/you/app:1.2 --plan build
Or use Import → Docker image in the dashboard. If you start from the Railway import tab instead, its Deploy the image link takes you there.
What to know before you do it:
- Any public registry works:
ghcr.io, Docker Hub, Quay, ECR Public. The image needs alinux/amd64build and has to be under 3 GB compressed. - It runs the way Docker runs it: the image’s own
ENTRYPOINT/CMD,USERandENV. Your URL is routed to the port the imageEXPOSEs. With noEXPOSE, the app getsPORT=8080, and--port 3000overrides it. - Data lives in
VOLUMEpaths. Those survive every redeploy and are backed up. Anything written outside them is replaced on the next deploy, same as with Docker. A Railway volume mounted at, say,/datadoes not carry over by itself: if your image doesn’t declareVOLUME /data, files there are wiped on every deploy. - The database is local. Add Postgres, MySQL or Redis in Step 4 and it runs in the same pod at
127.0.0.1, withDATABASE_URL(orREDIS_URL) set for you. Runinstapods env list my-appbefore Step 5; ifDATABASE_URLis already there, skip that line. - Bad redeploys don’t go live. A new version only takes over once it answers HTTP (it waits up to 2 minutes). If it never does, the previous version keeps serving, and rollback from the pod’s Image tab is instant.
- Build plan or higher ($7/mo). Image pods don’t run on Launch.
Not supported yet: private images that need a registry login, docker-compose setups with several services, arm64-only images, more than one public port, UDP, and anything that needs --privileged. The Docker image guide has the full details.
Step 4: Add Postgres and restore
instapods services add my-app -s postgresql -w
instapods services creds my-app -s postgresql
That installs PostgreSQL in the same pod as your app (about 10 seconds) and prints the password. The database is on localhost, so there’s no public port to leak.
Upload the dump and restore it:
instapods files upload my-app ./railway.sql --remote /tmp/railway.sql
instapods exec my-app -- sh -c "PGPASSWORD='<password>' psql -h localhost -U instapod -d instapod -f /tmp/railway.sql"
Read the output. Errors about roles mean the dump was made without --no-owner --no-acl, so re-dump with those flags. Errors about syntax or unknown features mean the version step above bit you.
Then delete the dump from the server:
instapods exec my-app -- rm /tmp/railway.sql
MySQL: same pattern with mysqldump on the Railway side and instapods services add my-app -s mysql -w. Redis: if it’s a cache or a job queue you can drain, start fresh with instapods services add my-app -s redis -w.
Step 5: Set your env vars, with new secrets
instapods env set my-app \
NODE_ENV=production \
DATABASE_URL='postgres://instapod:<password>@localhost:5432/instapod' \
OPENAI_API_KEY='<new key>' \
STRIPE_SECRET_KEY='<new key>' \
SESSION_SECRET="$(openssl rand -base64 32)"
env set writes to /home/instapod/app/.env, puts the values in your app’s process environment and restarts the app. You don’t need dotenv or a redeploy. Check them with instapods env list my-app.
That .env is a normal file on your own server. You can read it, edit it, and back it up.
Now open the pod’s URL (instapods pods get my-app shows it) and click around. Log in, hit a page that reads from the database, trigger whatever calls your AI or payment provider.
Step 6: Move your domain
Lower the TTL on your DNS record a few hours ahead if you can. Then:
instapods domains add my-app app.example.com
It prints a CNAME target. Change your DNS record from the Railway target to that one, wait for it to resolve, then:
instapods domains verify my-app app.example.com
That issues a Let’s Encrypt certificate and starts serving the domain. A root domain (example.com with no subdomain) works if your DNS provider supports CNAME flattening.
Step 7: Cron jobs and workers
A Railway cron service becomes a cron entry on your server (push your SSH key first with instapods ssh-keys add my-app):
instapods ssh my-app
crontab -e
A background worker (a queue consumer, a bot) depends on what it talks to. The database listens on localhost inside your app’s pod, so a worker that needs it is simplest as a second service on the same server (SSH in and add a systemd unit). A worker that only needs outside APIs can go in its own pod: deploy it with instapods deploy my-worker --repo ... --subdir worker, then convert it to a worker pod in its Settings tab so it has no public URL.
Step 8: Revoke the old keys and shut Railway down
Once traffic is on the new host and you’ve watched it for a day:
- Revoke every old key at the provider that issued it. This is the step that actually protects you.
- Delete the Railway project, including any staging and PR environments that held copies of the same keys.
- Remove
RAILWAY_TOKENfrom your CI secrets.
What you give up, honestly
I’d rather you move once than twice, so here’s what InstaPods doesn’t do that Railway does:
- No autoscaling. You pick a plan size. If you outgrow it, you resize. For most apps that’s a feature: the bill can’t grow by itself.
- No visual project canvas wiring multiple services together. Each pod is one server; databases run inside it.
- PostgreSQL 16, not 17 or newer, as covered above.
- Fewer regions. Europe (France) and North America (Canada).
- No Dockerfile builds from your repo yet. InstaPods builds Node.js, Python, PHP, Go and static sites from source, and it doesn’t run your Dockerfile. A pushed image does run as is, though, including Rust, Java and Ruby (see above). If your Railway service only builds from a Dockerfile, build it in GitHub Actions with
docker buildx build --platform linux/amd64 --push, push it toghcr.io, then deploy the image.
What you get instead: a flat price ($3/mo for a small app, $7/mo with a database), a full Linux server with SSH, storage included in the plan, and no usage meter. There’s a fuller side by side on InstaPods vs Railway.
Not ready to move?
That’s fair. If you’re staying on Railway, the rotation checklist is the part that matters today. Do it whether or not Railway emails you.