Skip to content

Migrate a Railway project to exe.dev

Your Railway services and data on one VM, and your API keys off it.

Deploy on exe.dev

Shelley handles the setup. About 1 hour.

Prefer a terminal?
curl -fsSL https://exe.dev/deploy/migrate-railway/prompt.txt | ssh exe.dev new --prompt=/dev/stdin

A Railway project is a set of services: your apps, Postgres, Redis, maybe a cron job. All of it fits on one VM as a Docker Compose project, with the same images, the same variables, and the same *.railway.internal names, so the services find each other as before.

One thing changes on purpose: your API keys. On Railway, your Stripe and OpenAI keys are variables that anything in the container can read. On exe.dev they go into integrations, which add the key to each request after it leaves the VM.

Nothing changes on Railway until you say so. The examples move a project with a web service at www.example.com to a VM called my-app.

What you get#

  • Your Railway services, variables, and data running on the VM, with the same *.railway.internal names, at https://my-app.exe.xyz/.
  • git push exe main deploying like Railway: build, pre-deploy command, health check, and the old build kept if anything fails.
  • A list of the API keys that can move into exe.dev integrations, where the VM can't read them.
  • A report in ~/railway/report.md, and your steps for the move.
What's running, and where
Private https://my-app.exe.xyz/ the app, until you make the move
Public https://www.example.com/ the app, after the move
Private ssh my-app.exe.xyz the shell; git push exe main deploys
my-app.exe.xyz exe.dev VM
Your services Each Railway service in Docker Compose, built with its Dockerfile or with Railpack; the public one on port 8000 :8000~/app
Postgres and Redis The same images as Railway's templates, with your data ~/app/volumes
Integrations Your API keys for Stripe, OpenAI, and others, kept by exe.dev outside the VM
The move kit The hand-over, the scripts, and the report ~/railway

Deploy with Shelley#

Click Deploy and Shelley will set up Apps from Railway on a new VM, then check that it works. No config-file wrangling required.

Prefer a terminal?
curl -fsSL \
  https://exe.dev/deploy/migrate-railway/prompt.txt \
  | ssh exe.dev new --prompt=/dev/stdin
What Shelley is told
Set up Apps from Railway on this VM by following the exe.dev recipe at https://exe.dev/deploy/migrate-railway.md from top to bottom.

The recipe's example VM name is my-app. This VM's name is the output of `hostname`: use it wherever the recipe writes my-app. Skip the step that creates the VM; you are on it.

You are done when all of these are true. Check each one yourself:
- Your Railway services, variables, and data running on the VM, with the same `*.railway.internal` names, at https://my-app.exe.xyz/.
- `git push exe main` deploying like Railway: build, pre-deploy command, health check, and the old build kept if anything fails.
- A list of the API keys that can move into exe.dev integrations, where the VM can't read them.
- A report in `~/railway/report.md`, and your steps for the move.

Read the whole recipe, including the closed details, and do "Rehearse the
move by hand" in order, skipping VM creation; the rest of the page is for
the owner. Use the kit's scripts as published. Change compose.yaml only
where this project needs it, and list the changes in the report.
You need the owner once, early: show them the commands in "Hand over the
project" with this VM's name, and wait until ~/railway/handed-over exists
and each repository in ~/git has a commit. Install the rest meanwhile.
Never run railway commands yourself, never ask for a Railway token, and
never connect to Railway's databases: the addresses in ~/railway are live
production ones. Never run railway/move or handover --move, change DNS,
or run share set-public: the move is the owner's call. Keep the cron
jobs off until then.
Keep variable values out of your replies and the report. Never ask the
owner to paste an API key into the conversation: API keys go into
integrations, which the owner adds; sealed variables the owner sets with
railway/import set over SSH.
If the project needs something exe.dev can't do (public TCP ports other
than HTTP, several regions, MySQL or MongoDB data), put it in the report
and ask.
Finish with the report's summary, https://<vm>.exe.xyz/, the keys that
could move to integrations, and the owner's steps in "Make the move"
with this VM's name and the project's domains.

Finish with the URLs I should open.

Also at /deploy/migrate-railway/prompt.txt. The recipe it follows is /deploy/migrate-railway.md.

How the move works#

  1. Hand over. On your computer, a script copies the project to the VM: its settings, variables, and data. You git push the code.
  2. Rehearse. Shelley sets everything up on the VM and checks its pages against Railway's. Railway keeps running.
  3. Move. The same script stops Railway, copies the data once more, and starts the VM. You point your domain at it.

Shelley never logs in to Railway. The commands that touch Railway are yours.

Where everything goes#

On Railway On the VM
Services Docker Compose services in ~/app
Railpack, Nixpacks, Dockerfile builds Railpack or docker build, on git push exe main
Variables ~/app/compose.yaml
API keys for other services Integrations
Postgres, Redis, volumes The same images, data in ~/app/volumes
Cron schedules Crontab lines, in UTC
Domains https://my-app.exe.xyz/ and your custom domain
railway ssh, railway logs ssh my-app.exe.xyz, docker compose logs

Not copied: Railway Buckets, MySQL, MongoDB, replicas, and regions. Shelley lists any of these it finds in the report.

Hand over the project#

When Shelley asks, run this on your computer, logged in to the Railway CLI. In an empty directory, link the environment to move, usually production, and fetch the script:

railway link
curl -fsSLO https://exe.dev/deploy/migrate-railway/handover
less handover
bash handover my-app.exe.xyz

It only reads from Railway. If it stops at railway ssh, run railway ssh keys add and try again.

Then push each repository Railway builds from. In its checkout:

ssh my-app.exe.xyz git init -q --bare git/web.git
git remote add exe my-app.exe.xyz:git/web.git
git push exe main

The script prints these lines for each of your repositories.

Look around#

When Shelley is done, read its report and open https://my-app.exe.xyz/:

ssh my-app.exe.xyz cat railway/report.md

This is your app with a copy of your data. It uses your real variables, so if it sends email or charges cards, it does that from here too.

Railway never shows sealed variables again, so the VM has them empty. Set each one: paste the value, press Enter, then Ctrl-D:

ssh my-app.exe.xyz railway/import set web STRIPE_WEBHOOK_SECRET

Take the keys off the VM#

For each API key in the report, make a new key at the vendor, give it to exe.dev, and revoke the old one. Take Stripe. Create a restricted key in the Stripe Dashboard, then at the ssh exe.dev prompt, which asks for the key with hidden input:

exe.dev ▶ integrations add http-proxy --name stripe --target https://api.stripe.com --bearer=- --attach vm:my-app

The VM can now call https://stripe.int.exe.xyz with no key. Point the app at it, and replace the old key with a placeholder, since SDKs want one:

ssh my-app.exe.xyz railway/import set web STRIPE_SECRET_KEY added-by-exe-dev
Service Point the app at
OpenAI OPENAI_BASE_URL=https://openai.int.exe.xyz/v1
Resend RESEND_BASE_URL=https://resend.int.exe.xyz
Stripe In code: new Stripe(key, { host: "stripe.int.exe.xyz" })

For Anthropic, use the LLM integration with your own key; for Twilio, Postmark, and others, the integration catalog.

Secrets the app checks itself, such as a session secret or webhook signing secret, stay in compose.yaml.

Make the move#

A day ahead, lower your domain's DNS TTL to 300 seconds. Then, in the directory with the hand-over script:

bash handover my-app.exe.xyz --move

Type yes when asked. It stops your app services on Railway (the databases stay), copies the data, and starts everything on the VM. Then:

  1. At your DNS provider, point www.example.com at my-app.exe.xyz with a CNAME. See custom domains.
  2. On your computer:
ssh exe.dev domain add my-app www.example.com
ssh exe.dev share set-public my-app

Changed your mind before step 1? ssh my-app.exe.xyz railway/move undo prints the commands that start Railway again.

Day to day#

git push exe main deploys. If the build or health check fails, the push is rejected and the old build keeps running.

On the VM, in ~/app:

Instead of Run
railway logs docker compose logs -f web
railway ssh docker compose exec web bash
railway variable set ~/railway/import set web KEY value
railway connect postgres docker compose exec postgres psql -U postgres railway

A backup of the databases and volumes runs every night into ~/backups.

Clean up#

After a week or so, delete the project in the Railway dashboard, revoke any keys you haven't, and delete ~/railway/variables and ~/railway/data on the VM.

The nuts and bolts#

These are the steps Shelley follows.

Rehearse the move by hand

Create the VM#

On your computer. Give it about four times the size of your data, plus 20 GB:

ssh exe.dev new --name=my-app --disk=40GB

Everything below runs on the VM: ssh my-app.exe.xyz.

Install Docker, Railpack, and the kit#

sudo systemctl enable --now docker.service
curl -sSL https://railpack.com/install.sh | sh
mkdir -p -m 700 ~/railway && cd ~/railway
for f in import deploy restore check backup move; do
  curl -fsSLO https://exe.dev/deploy/migrate-railway/$f
done
chmod +x import deploy restore check backup move
Script What it does
import Turns the hand-over into ~/app/compose.yaml; sets variables
deploy Builds and releases on git push; rolls back
restore Loads the data into Postgres, Redis, and volumes
check Compares pages with Railway's
backup The nightly backup
move The move, and its undo
handover Runs on the owner's computer

Then ask the owner to hand over the project, and wait for ~/railway/handed-over.

Set up the project#

~/railway/import setup

It writes ~/app/compose.yaml: one service per Railway service, with its image, start command, variables, volume, and *.railway.internal name. The public service gets port 8000. Railway's domain becomes my-app.exe.xyz, and the databases get new passwords. It also makes a git repository in ~/git for each code repository, and prints a plan, saved in ~/railway/plan.md, listing sealed variables and likely API keys.

Restore the data, build, and start#

~/railway/restore
for g in ~/git/*.git; do
  ~/railway/deploy "$(basename "$g" .git)" "$(git --git-dir "$g" rev-parse HEAD)"
done
cd ~/app && docker compose up -d && docker compose ps

deploy builds each code service with its Dockerfile, or with Railpack, runs the pre-deploy command, starts it, and waits for the health check. Cron services are built but not scheduled until the move. If a service fails, docker compose logs <service> says why.

Check against Railway#

List the app's important pages in ~/railway/paths.txt, one per line, then run ~/railway/check. Expect same for each. Pages with live data may differ.

Write the report#

Write ~/railway/report.md for the owner: the services, what changed, sealed variables to set, keys that could move to integrations, anything exe.dev doesn't do that Railway did, and the steps in Make the move. No variable values.

Troubleshooting#

A push is rejected with "health check failed"

The new build didn't answer in time, and the old one still runs. The push output ends with the service's logs. Usually the app listens on a fixed port instead of PORT, or on localhost instead of all addresses.

An integration answers 403

The name in the address is wrong, or the integration isn't attached to the VM. On your computer: ssh exe.dev integrations attach stripe vm:my-app.

Frequently asked questions#

Why move the API keys out of the environment?

On Railway, a Stripe or OpenAI key is a variable in the container, where any dependency or crash log can read it, and the platform stores it. On October 21, 2026, Railway disclosed that an unauthorized party may have accessed encrypted environment variables and decrypted some of them, and advised customers to roll all of them. If you're making new keys anyway, give them to exe.dev integrations: the VM can use them but never read them.

Why not give Shelley a Railway token?

A token could read every variable and change or delete the project. The hand-over only needs a few read-only commands, so you run them yourself, and their output goes straight to the VM.

How long is the app down?

A few minutes: from stopping Railway to your DNS change, plus the time to copy the data. Lower your DNS TTL to 300 seconds a day ahead.

My service builds with Nixpacks.

Nixpacks is deprecated on Railway, and Railpack replaces it, so the VM builds it with Railpack. Shelley checks the build and notes any difference. Services with a Dockerfile build from it, as on Railway.

Can I go back?

Until your domain points at the VM, yes: railway/move undo prints the commands that start your services on Railway again. After that, new data exists only on the VM.

What does it cost?

Your exe.dev plan: one VM runs all of the services, databases, and cron jobs, with no usage-based bill.

Updated Oct 30, 2026 · Markdown · manifest.json · Something wrong? Tell us