Skip to content

How to migrate from Railway without reusing exposed secrets

Nick Potts, who builds Ployz. Published .

TL;DR: If your Railway secrets may have leaked, don't copy them to your next host. Copy the names, rotate every value where it was issued, and build the new environment with credentials nobody else has seen. Then make sure the old ones stop working.

  • Rotate first, move second. A leaked key is live until its issuer revokes it, wherever your app runs.
  • Export names, never values. One railway variable list --json | jq line gives you a clean inventory.
  • The new host gets its own credentials. Your database, API providers and buckets each issue a fresh key for it.
  • You're done when the old values fail. Revoke them, test them, then delete the old project.
  • Pick Ployz if you want the new home on servers you rent, with sealed secrets and one-click Postgres.

Why this migration starts with rotation

On 21 October 2026, Railway disclosed that an unauthorized third party may have accessed customers' encrypted environment variables and decrypted some of them. Railway recommends rolling every variable stored there.

The quickest migration is to export every variable and paste the file into the new host. After a disclosure like this, that hands your new host keys someone else may also hold. So move the shape of your config, and mint every secret again at its source.

If you're staying on Railway, steps 1 to 3 are the same work, and Railway's own guides are below. If you're still choosing where to go, my self-hosted PaaS comparison and Railway alternatives cover the options. The checklist works for any destination.

Step 1: Inventory variable names, services and data

Make a list you could hand to a teammate without leaking anything. For each project, write down:

WhatWhat to record
ServicesRepo, branch, build and start commands, port, replicas, region
Variable namesPer service and per environment, including production, staging and previews
DatabasesEngine and version, extensions, size, and every consumer, including BI tools, CI and your laptop
VolumesMount path, size, and what writes there, such as uploads or SQLite files
BucketsName, which services use them, and anything outside the platform that reads them
DomainsEvery custom domain, its DNS provider, current records and TTL
WorkersCommand, and whether running it twice is safe
IntegrationsGitHub app or deploy keys, registry credentials, platform API tokens in CI, webhooks, OAuth callback URLs and IP allowlists

Railway's CLI can list the names for you. Run this from the linked project, once per service and environment:

set -o pipefail
railway variable list --json --service web --environment production | jq -r 'keys[]' > web.vars.txt

pipefail makes a failed railway call fail the whole line, so an empty file never passes for a service with no variables. jq keeps only the keys, so web.vars.txt holds names and nothing else, even when a value spans several lines. The values still pass through the pipe, so run it on your own machine and don't save the raw JSON. The flags are in the Railway variable docs.

Step 2: Sort config, generated variables, secrets and data keys

Give every name one of four labels. The label decides what happens to it.

KindExamplesWhat to do
ConfigNODE_ENV, LOG_LEVEL, feature flagsCopy the value if it isn't secret.
Platform-generatedPrivate hostnames, generated domains, ports, database URLs the platform builtRecreate on the new host. Rotate any credentials inside the old values too.
SecretDatabase passwords, API keys, AI provider keys, OAuth client secrets, GitHub tokens, webhook signing secrets, SMTP passwordsRotate at the source, then set the new value.
Data keyEncryption keys for app data or database columns, session or token signing secretsRotate with a plan for the data or sessions that depend on them.

Data keys need care. Rotate a key that encrypts rows in your database without re-encrypting them, and you lock yourself out of your own records. Rotating a session signing secret logs everyone out, which is often the right call after an exposure, so make it on purpose. Check how your framework rotates keys before you touch one.

Look inside the database too. OAuth refresh tokens and customers' integration keys stored in tables travel with the dump in step 5, so decide whether they need revoking.

Step 3: Rotate every secret at its source before you move

Deleting a variable from a platform doesn't kill the credential. A key works until whoever issued it revokes it. Railway's GitHub credential guide makes the same point about registry tokens.

Work through your secrets one at a time, at the place that issued each:

SecretWhere to rotate it
Database passwordIn the database. For a platform-managed database, use its rotate or regenerate action so the database and its variables stay in sync.
API keys for payments, email and mapsThe provider's dashboard. Create a new key, swap it in and revoke the old one.
AI provider keysThe provider's console. Check whether the old key is already revoked, issue a new one, and set a spend limit if you can.
GitHub tokensDelete old personal access tokens and create replacements with the narrowest scope. Review GitHub App installations and deploy keys.
OAuth client secretsThe identity provider, such as GitHub or Google. Add the new host's callback URL while you're there.
Webhook signing secretsThe sending service's dashboard.
Registry credentialsRevoke and reissue at the registry.
Platform API tokens in CIRevoke on the platform and remove them from CI secrets.

Do this now, not after the move. Every hour a leaked key stays valid is an hour someone else can use it, and a short blip while services pick up new values is a far better trade. Railway's PostgreSQL rotation guide says the same.

Where a provider allows more than one key, issue one for the new host and another for whatever stays on the old one. When you retire the old host in step 8, you revoke its key without touching the new one.

While you're in each dashboard, check the usage page for activity you don't recognize.

Step 4: Build the new environment with fresh credentials

Deploy the app on the new host with plain config copied and every secret new.

  • Paste nothing from the old export. Keep new secrets in a password manager or the provider's dashboard, and set them one at a time.
  • Keep secrets out of shell history. Pipe them from a protected file or a hidden prompt instead of typing them into a command.
  • Rebuild anything baked at build time. Frameworks that inline public API URLs or frontend keys need a fresh build. Don't pass long-lived secrets as Docker build arguments, because they can end up in the image.
  • Know where builds run. A CI service that builds your app sees your secrets too.
  • Keep your local .env out of the upload. Add it to .gitignore and check what your deploy tool sends.

Deploy the same commit that runs in production today. Change platforms and code in separate steps.

Step 5: Dump and restore the database with new credentials

The new database gets its own user and password. Only the data and schema move.

From a machine you trust, dump the old database using its rotated credentials. Use a pg_dump at least as new as the source server:

umask 077
pg_dump --format=custom "$OLD_DATABASE_URL" > app.dump

Then restore into an empty new database:

pg_restore --exit-on-error --no-owner --no-privileges -d "$NEW_DATABASE_URL" app.dump

--no-owner --no-privileges makes every object belong to the new user and leaves the old grants behind (pg_restore). Recreate any app-specific roles on purpose. Skip pg_dumpall for globals, because it copies roles and their passwords along with them (pg_dumpall). On a shared machine, keep the password in a PostgreSQL password file instead of the connection URL.

  • Compare row counts on your biggest tables, and click through the app against the new database.
  • For the final copy, stop writes first. A dump doesn't include anything written after it starts.
  • Treat app.dump like a secret, because it is your data. Delete it once the restore checks out.
  • MySQL, Redis and MongoDB have their own tools: mysqldump --single-transaction, redis-cli --rdb and mongodump. If Redis only holds a cache, let it refill.

Step 6: Copy volumes and buckets with new credentials

Files in volumes aren't in the database dump, so copy them separately.

On Railway, railway service files download copies files or whole directories out of a service (CLI docs). Stop writers before the final copy, especially for SQLite files.

For buckets, read from the source with its current credentials. If Railway reset them, its bucket guide shows where to find the new ones. Write to the destination with keys created only for it, using rclone or the AWS CLI.

Compare object counts and total size, and open a few files through the app before you trust the copy.

Step 7: Cut DNS over and test the app

  1. A day ahead, lower the TTL on the records you'll change so the switch spreads quickly.
  2. Freeze writes on the old host and pause its workers so nothing runs twice.
  3. Take the final database dump and file copy from steps 5 and 6.
  4. Restore that dump into an empty destination database, replacing the rehearsal copy, and check the data.
  5. Point DNS at the new host and wait for its certificate.
  6. Start workers on the new host.
  7. Update everything that calls you: webhook URLs, OAuth callback URLs and IP allowlists.

Then do what your users do. Sign up, log in, pay, upload a file and get an email. Watch the error logs. Some clients hold on to the old DNS answer for a while, so make sure the old host can't write to the old database.

Once the new database takes writes, keep the old one read-only until you delete it, so nothing writes to the wrong copy.

Step 8: Revoke old credentials and delete the old project

The move isn't finished while the old values still work.

  1. Revoke what the old host held: its database user, the API keys you issued it, registry and deploy credentials, and platform API tokens.
  2. Prove they fail. Try the old database URL and an old API key, and check each provider's last-used date.
  3. Remove Railway's GitHub access to repositories you no longer deploy there, from the Railway GitHub App settings, or uninstall it.
  4. Export the audit log if you want it for your incident record. Retention depends on your Railway plan.
  5. Delete the old project, including volumes, buckets and backups, once you no longer need them.

If you're staying on Railway

Railway has published step-by-step guides for this. Check its status page for the current state, then work through the ones that apply:

Its PostgreSQL guide recommends replacing pasted connection strings with reference variables such as ${{ Postgres.DATABASE_URL }}, then redeploying every service that uses them. That's good practice anywhere.

Where Ployz fits

I build Ployz. It gives you Railway's canvas, git-push deploys and Railway-style database variables, on Linux servers you rent. If this disclosure has you leaving anyway, here's the checklist on Ployz.

Steps 4 and 5 start with one command from your project folder. It installs Ployz on your server, builds the app there and gives it an HTTPS address. In a Git checkout it skips everything Git ignores, so an ignored .env stays on your laptop:

ployz up --server root@your-server

Each fresh secret goes in as a sealed variable. Ployz encrypts it on save, and the dashboard never shows the value again:

printf %s "$NEW_OPENAI_KEY" | ployz set my-app.env.OPENAI_API_KEY --secret

For the database, choose Create → Database → PostgreSQL and name it postgres. It starts on a 5 GB volume, so if the size you recorded in step 1 is bigger, raise Storage limit (GB) on the volume before you deploy. Ployz generates and seals a brand-new password, and the variable names match Railway's (DATABASE_URL, PGHOST, PGPASSWORD). In your app's Variables tab, set DATABASE_URL to ${{ postgres.DATABASE_URL }} with Sealed unticked; the reference still keeps the password hidden. If your app reads PGHOST and friends instead, reference those too. Click Deploy, and once postgres is running, restore the dump from step 5:

ployz exec -T postgres -- pg_restore --no-owner --no-privileges -d ployz < app.dump

To keep your secrets on your own hardware during builds too, choose Your servers only under Organization → Builds. See variables, databases and where builds run for the details.

What you'd otherwise add to Railway comes with it: searchable logs, metrics for every service and server, uptime checks with email alerts, a restore point for every database, and a preview environment for every pull request. Your coding agent can run the whole thing, and deleting a service, volume or domain waits for you. The Free plan covers one server, and the code is open source at github.com/getployz/ployz.

Move your app to your own server with Ployz, free.

FAQ

Should I rotate secrets before or after migrating off a PaaS?

Before. If a secret may be exposed, revoke it now, wherever your app runs, and build the new host with fresh values. Rotating after the move keeps the exposed value valid longer and copies it to a second place.

Does deleting an environment variable revoke the secret?

No. A key works until whoever issued it revokes it, whether that's your database, an API provider or GitHub. Removing it from your platform only stops future deploys from reading it there.

Can I export my Railway variables and import them elsewhere?

Yes, but after a possible exposure, import only the names and plain config. Create every secret again at its source. railway variable list --json | jq -r 'keys[]' gives you the names without the values.

Do I need to rotate my database password if I'm moving the database?

Yes, on the old database, because it still holds your data until you delete it. Give the new database its own password, and restore with --no-owner --no-privileges so the old ownership and grants stay behind.

How I checked

I read Railway's security disclosure, its PostgreSQL and bucket and GitHub guides, its status page, its CLI docs and PostgreSQL's dump and restore docs on 22 October 2026. I ran the Ployz commands above on a test server with throwaway data: sealed secrets, the postgres reference, and a dump restored into a fresh database. Rehearse your own migration on a copy before you move production.

Run your apps and databases on your own servers with Ployz.

Deploy your app