# Moving from Railway

Move a Railway service to Deplane with one command: `deplane import railway` reads the project on Railway, shows what moves and what stays, then creates the Deplane app with its secrets, workers, cron jobs and release command, copies the Postgres database, adds a Redis, and deploys your code. Railway keeps serving your users until you switch.

The command runs on your own computer with your Railway token, which never leaves it. It only reads Railway (GraphQL queries, never changes). The command and its flags are in the [CLI reference](cli.md#deplane-import-railway), and every error has a fix in [Troubleshooting and error codes](errors.md#railway_token_missing). It works like [Moving from Heroku](heroku.md), which explains the switch-over in more detail.

## Before you start

1. [Install the Deplane CLI](install.md) and run `deplane login`.
2. Have the service's code in a folder: a checkout of the repository Railway deploys.
3. **Give Postgres a public address.** Railway databases are private by default, and Deplane copies the database from outside Railway. Open the Postgres service → **Settings → Networking → Public Networking** and add a **TCP Proxy**. This sets `DATABASE_PUBLIC_URL` (a `...proxy.rlwy.net` address), which the import reads. Without it the report has a ✗ line saying so. You can remove the proxy after the switch; Railway bills its traffic as egress.
4. Create a **project token**: the project's **Settings → Tokens**, for the environment to import (usually `production`).

**The Railway token.** A project token reaches one environment of one project, and that is the environment the CLI imports. An account token works too (the CLI then imports the `production` environment, or the only one), but reaches every project you have. Workspace tokens are not supported yet: use a project token for a workspace's project. Railway has no read-only tokens; the CLI only reads with it. Delete the token when the import is done.

Give the token to Deplane in one of three ways, never as a flag:

- run `deplane import railway my-project --check` in a terminal and paste it at the hidden prompt;
- set it for one command: `RAILWAY_TOKEN=... deplane import railway my-project --check`;
- or pipe it in from a password manager.

A token piped on stdin wins over `RAILWAY_TOKEN`. The CLI says which one it used, never the token. It sends it only to Railway's API (`backboard.railway.com`), never writes or prints it, leaves it out of `--debug`, and never sends it to Deplane.

## Which service

`deplane import railway PROJECT` imports the one service with a public domain. If several services have one, name it: `deplane import railway my-project/web`. The other services of the environment are sorted in the report:

- **Workers:** services without a public domain, with a start command and a running deployment, that Railway says are built from the same repository as the app (or the same image). A service with a cron schedule becomes a [cron job](workers.md). A service whose source Railway does not give, or that comes from another repository or image, is never made a worker, and its variables are not copied: the report lists it, to import on its own or add with `deplane processes add`.
- **Databases:** the services the app's variables reference, such as `${{Postgres.DATABASE_URL}}`.
- **Other apps:** another service with a public domain, or one built from another repository, is listed, to import on its own.

## Check first

```sh
$ deplane import railway shop --check
Railway service shop (project shop, environment production, region europe-west4-drams3a) → Deplane app shop

Processes
  ✓ web: shop, 2 replica(s) on Railway → 2 small instance(s).
  ⚠ Railway has no instance sizes (it bills by use): Deplane starts the app and its workers on small instances ...
  ✓ pre-deploy command → release command `npm run migrate`.
  ✓ worker: 1 replica(s) on Railway → worker worker, 1 small instance(s): `node worker.js`.

Data
  ✓ Postgres (Postgres): copied into a new Deplane database shop-db ... through its public URL ...
  ⚠ Redis (Redis): a new Deplane Redis shop-redis ... Its data is not copied ...

Config vars
  ✓ 2 copied as secrets: APP_URL, SECRET_KEY_BASE.
  ✓ Not copied, Deplane sets them: DATABASE_URL, PORT, REDIS_URL.
  ✓ Not copied, Railway-managed: RAILWAY_PUBLIC_DOMAIN.
  ⚠ Not copied, they read from the Postgres or Redis that Deplane replaces: PGHOST. Deplane sets only DATABASE_URL and REDIS_URL ...
  ⚠ Railway never returns sealed variables through its API, so any you sealed are not copied ...
...
Nothing was changed (--check). Run the command again without --check to import.
```

Then run the command again without `--check` (in a terminal it asks first; from a script, add `--yes`). The flags are the same as for Heroku: `--source DIR`, `--name NAME`, `--check`, `--yes`.

## What moves

| On Railway | On Deplane |
|---|---|
| The service | The app, with as many instances as replicas, all small: Railway has no sizes. Change them with [`deplane scale`](scaling.md). |
| Worker and cron services (see above) | [Workers and cron jobs](workers.md), with their start command and schedule. |
| The pre-deploy command | The [release command](release-command.md). |
| Variables, resolved (shared variables and references included) | Secrets. A worker's variables are added; where one sets a name differently, the web service's value is used and the report names it. Not copied: `RAILWAY_*`, `PORT`, and the variables that read from the Postgres and Redis Deplane replaces (such as `PGHOST` or `PGPASSWORD`: the report names them; Deplane sets only `DATABASE_URL` and `REDIS_URL`). |
| Postgres (`DATABASE_URL = ${{Postgres.DATABASE_URL}}`) | A new Deplane database, copied once from `DATABASE_PUBLIC_URL` (copy mode), attached as `DATABASE_URL`. |
| Redis (`REDIS_URL = ${{Redis.REDIS_URL}}`) | A new, empty Deplane Redis (Valkey 8, 256 MB), attached as `REDIS_URL`. |
| The code | Your folder, built by Railpack, or by your Dockerfile if it has one. |

## What does not move

- **Sealed variables.** Railway never returns their values through its API, so the CLI cannot see them, or even tell whether you have any. Set each yourself: `deplane secrets set NAME=VALUE --app APP`.
- **Redis data** is not copied.
- **MySQL and MongoDB services** stay on Railway. Variables that read from them hold their private address (`*.railway.internal`), which does not resolve on Deplane: point them at the service's public URL.
- **Variables that use Railway's private network** (`*.railway.internal`) are copied, with a warning.
- **Volumes** are not copied. Add a [disk](disks.md) and copy the files yourself.
- **Cron schedules, pre-deploy commands and sources** are read from fields Railway does not document. If Railway refuses them, the report says so, and no worker or cron job is created: add them yourself with `deplane processes add`.
- **Cron schedules Deplane cannot run.** Day names (`MON-FRI`), month names and 7 for Sunday are converted; anything else Deplane's cron does not take is a ✗ in the report, before anything is created.
- Deplane runs in one region, in Europe.

## Switch over

The first import is a rehearsal. When the Deplane app works:

1. **Before the day:** `deplane domains add HOSTNAME --app APP` for each custom domain, and add the TXT record it shows. Make sure your plan has room for one more database.
2. **Stop writes on Railway:** remove the active deployment of the service and of its workers (each service → **Deployments** → the active one → **Remove**). The site is down from here until DNS moves.
3. **Copy the data again** and move the app to the copy in one release:

   ```sh
   deplane db import --name APP-db-final --mode copy --yes   # paste DATABASE_PUBLIC_URL at the prompt
   deplane db switch APP-db-final --from APP-db
   ```

4. **Switch DNS** to the target `deplane domains add` showed, and wait for the certificate.
5. **Keep the Railway project** until the app has run well on Deplane; then remove the Postgres TCP proxy and delete the project.
6. **Delete the rehearsal's database:** `deplane db destroy APP-db`.
7. **Delete the Railway token** (the project's Settings → Tokens).

## Limits

The [Heroku import's limits](heroku.md#limits) apply: Postgres up to 5 GB and versions as for every [Postgres import](import-postgres.md#limits), a new 256 MB Redis without its data, 10 workers and cron jobs per app, sizes up to 2 GB, and copying the database needs a person logged in to the CLI. Railway's API allows 100 calls an hour on its Free plan and 1000 on Hobby; an import makes about 5, plus 5 for each service in the environment.
