Skip to main content
A Railway project moves to Sevalla as one application per repository: each Railway service that runs your code becomes a process of that application, Railway’s PostgreSQL and Redis become Sevalla databases on internal connections, and each volume becomes a disk. You recreate the setup while the Railway services keep running, copy the data during a short maintenance window, then point the domains at Sevalla.

At a glance

Environment variables

How Railway maps to Sevalla

Before you begin

  • The Railway CLI, signed in with railway login and linked to the project with railway link. The commands on this page were tested with version 5.71.0.
  • The project’s code in a repository on GitHub, GitLab or Bitbucket. A service deployed with railway up from a local folder has no repository: push its code to one first. See Git providers.
  • A Sevalla company with a payment method.
  • jq, and the PostgreSQL client tools at the same major version as Railway PostgreSQL or newer. See Import and export PostgreSQL.
  • For Redis: redis-cli, and RedisShake 4.6 or later. See Import and export Redis.
  • A maintenance window: Railway has no maintenance mode, so the services are stopped while you copy the data. Add and verify the custom domains before it, as in Move the custom domains.

Take stock of the Railway project

Export the project’s configuration, with its variables, from the folder linked to the project:
railway-config.json describes every service, database and volume in the linked environment, with the settings made in the dashboard or with .railway/railway.ts. The CLI warns that it decrypts the variables into the file: don’t commit it. List the services with their start command, cron schedule, pre-deploy command and volume mount path:
Here web has the public domain, a pre-deploy command and a volume at /data, and starts with the command Railpack detects (-); worker runs all the time; cron runs every 5 minutes. Then list the databases and the reference variables, which need new values on Sevalla:
  • A service configured with a railway.json or railway.toml file takes the file’s settings at each deployment, without changing the dashboard’s: read the file too. Railway stops reading these files on Dec 1, 2026.
  • The region of each service is in .deploy.multiRegionConfig. The closest Sevalla locations are San Jose, USA for US West (us-west2), Ashburn, USA for US East (us-east4-eqdc4a), Amsterdam, Netherlands for EU West (europe-west4-drams3a) and Singapore for Southeast Asia (asia-southeast1-eqsg3a). See Locations.
  • psql "$(railway variable list --service Postgres --json | jq -r .DATABASE_PUBLIC_URL)" -At -c 'SHOW server_version' prints the PostgreSQL version, such as 18.6 (Debian 18.6-1.pgdg13+2), once the database has Public Access (see Copy the PostgreSQL data). Create the Sevalla database with the same major version or a newer one.
  • railway domain list --service web lists the service’s domains. Custom domains move last, in Move the custom domains.

Create the databases

1

Create the PostgreSQL database

In the sidebar, click Databases, then click Create database. Under Database, choose PostgreSQL and a version at least as new as Railway’s. Enter orders-db as the Display name, choose the Data center the application will use and a Size with room for the data, then click Create database.
2

Create the Redis database

If the project uses Railway Redis, create a second database the same way: choose Redis at a version at least as new as Railway’s, enter storefront-redis as the Display name, and choose the same Data center. See Create a database.

Create the application

1

Create it from the repository

In the sidebar, click Applications, then click Create > Application. Under Source, click Private repository, then choose the Git provider, the Repository and the Branch the Railway services deploy. Under Location, choose the same Data center as the databases, choose a Size, and click Create application. The application opens on its Overview tab with the status Not deployed yet: creating an application doesn’t deploy it. See Create an application.
2

Choose the build strategy

Sevalla builds with Ingot by default. To build exactly as Railway does, click the Settings tab, then click Update build strategy under Build strategy. Click Railpack, then click Update build strategy. See Choose Railpack or Ingot.
A web process on H1 can’t have custom domains. If you’ll move a domain, choose a larger size, such as S1.

Choose Railpack or Ingot

Both build the project with no changes. In the test for this page, a Node.js project with a web service, a worker, a cron service and a pre-deploy command built and ran unchanged with each: Choose Railpack to move without changing how the project builds, then switch to Ingot: it runs your code as a non-root user, starts the server directly under tini rather than through npm, and builds on the official language images. To switch, click Ingot in Update build strategy and deploy; if the deployment fails, switch back. Ingot shows what it detected in the build log’s build plan.

Connect the databases

1

Connect PostgreSQL

Click the Networking tab. Under Connected services, click Add internal connection. Choose orders-db as the Service and turn on Add environment variables to the application. Change the key DB_URL to DATABASE_URL, the name your code already reads, and click Add internal connection.
2

Connect Redis

Add an internal connection to storefront-redis the same way. Its URL is already called REDIS_URL.
Railway’s PostgreSQL URL starts with postgresql:// and Sevalla’s with postgres://; Railway’s Redis URL has the user default (redis://default:<password>@...) and Sevalla’s has none (redis://:<password>@...:6379/0). Clients that read a URL, such as pg and redis for Node.js, accept both forms. See Connect an application.

Move the variables

Sevalla variables belong to the application, so every process gets the same set: merge the variables of the services that become its processes.
1

Export them

railway variable list shows the values with every reference resolved, and leaves out sealed variables. grep drops PORT and the RAILWAY_* variables Railway sets. The file contains secrets: don’t commit it.
2

Clean up the file

In railway.env, delete the variables the internal connections replace, DATABASE_URL and REDIS_URL. Give each value that came from a reference variable its Sevalla value, as in the table below. Delete WORKER_URL for now: you set it once the worker exists, in Reach a process on the private network. If two services had different values for the same key, keep one, or rename the key in the code that reads it.
3

Import the file

Click the Environment variables tab, then click Add environment variables. Click Import .env and choose railway.env, then add the sealed variables by hand. Keep Available during runtime and Available during buildtime selected, and click Add environment variables.
Don’t add PORT: Sevalla sets it to 8080 for the web process and background workers. See Environment variables.

Recreate the services as processes

On the Processes tab, click Create process for each row after the first. Choose Background worker, Cron job or Job, enter a Name, such as the Railway service’s name, and the Custom start command, set the Schedule (cron jobs) or the Start policy (jobs), choose a Resource, and click Create process. See Processes. Without a custom start command, a process runs the image’s default command, the web process’s: every background worker and cron job needs its own.

Reach a process on the private network

On Railway, web reaches worker at worker.railway.internal. On Sevalla, each web process and background worker has an internal hostname instead:
1

Copy the hostname

On the application’s Networking tab, the Internal hostnames section lists each process’s hostname, such as storefront-x7k2m-a1b2c3d4.storefront-x7k2m.svc.cluster.local for worker.
2

Update the variable

On the Environment variables tab, set WORKER_URL to http:// followed by the hostname and :8080.
A background worker listens on PORT, which Sevalla sets to 8080, and the other processes of the same application reach it there with no internal connection. A server on another port needs a private port. To reach another application, add an internal connection to it.

Add a disk for the volume

On the application’s Disks tab, click Create disk. In Process, pick the process that had the volume, here the web process. Enter the volume’s mount path, /data, as the Path, pick a Size larger than the volume’s usage, and click Create disk. Keeping the mount path keeps your code and variables working. A Railway volume mounted at /app/data works as /app/data on Sevalla too, but not at /app itself: see Mount path rules. A process with a disk runs 1 instance, as a Railway service with a volume does, and stops for a few moments on every deployment. See Persistent storage.

Deploy

Deploy once before you copy the data, to check that everything builds and starts, and so that the disk is mounted.
1

Deploy

On the application’s Overview tab, click Deploy now under Recent deployments, check the Branch, and click Deploy.
2

Check the deployment

On the Deployments tab, wait for the status Deployed. The build log shows the job’s result, pre-deploy has been successful, then each process deploying, such as worker has been deployed successfully.
The application now runs against empty databases: the job created the tables, and the cron job writes to them on its schedule. The copy in the next sections replaces that data.

Stop the Railway services

Railway has no maintenance mode, so you stop the services that write data with railway down, which removes a service’s running deployment. Railway reaches a volume’s files through the service it’s attached to, so download them before you stop that service.
1

Clear the cron schedule

In Railway, open the cron service’s Settings and delete the expression in Cron Schedule. railway down alone doesn’t stop a cron service: at its next scheduled time, Railway deploys it again and runs it.
2

Stop the worker and the cron service

3

Download the volume

The volume’s name is in railway volume list. The first time, the CLI asks you to register an SSH key with Railway. Files written after the download aren’t copied, so stop the web service right after it.
4

Stop the web service

Railway now answers requests to the service’s domains with 404.
The services keep their settings, and the databases and volumes keep running, so you can still read them.

Copy the PostgreSQL data

1

Open the Railway database to your computer

In Railway, open the Postgres service’s Settings, and under Networking, add Public Access. Railway creates a TCP proxy and the variable DATABASE_PUBLIC_URL. Skip this if the variable exists.
2

Dump it

3

Open the Sevalla database to your computer

In the sidebar, click Databases, then click orders-db. Click the Networking tab, click Enable under External connection, then click Enable in the dialog. Copy the External connection URL. See Enable the external connection.
4

Restore the dump

--clean --if-exists drops the tables the first deployment created before it restores Railway’s. See Import a database.

Copy the Redis data

Skip this when Redis holds only caches or jobs you can lose: the application refills it.
1

Export the Railway database

Add Public Access to the Redis service on Railway, as for PostgreSQL, then save a snapshot of every key:
It ends with Transfer finished with success after <n> bytes.
2

Import it into Sevalla

Enable the external connection of storefront-redis as for orders-db. Save shake.toml next to the redis-shake binary, with the absolute path to railway.rdb and the database’s External hostname, External port and password:
shake.toml
RedisShake writes every key, with its expiry, and ends with all done. See Import and export Redis.

Copy the volume files

1

Archive the download

COPYFILE_DISABLE=1 and --no-xattrs keep macOS’s ._* files and extended attributes out of the archive. railway-files.txt lists each file with its checksum.
2

Upload the archive

On the application’s Web terminal tab, open a shell on the web process. Click Files > Upload files…, choose volume.tar.gz, set the Destination to /data, and click Upload. A file can be up to 100 MB: split a larger archive with split -b 90m volume.tar.gz volume.tar.gz.part-, upload the parts, and join them in the terminal with cat /data/volume.tar.gz.part-* > /data/volume.tar.gz && rm /data/volume.tar.gz.part-*. See Copy files to and from a process.
3

Unpack it

In the terminal:
--strip-components=1 drops the railway-volume folder from the paths, and --no-same-owner makes the files belong to the process’s user.
To copy the files without the dashboard, see Copy the volume with the API.

Verify the data and the application

1

Compare the data

Each table’s row count and checksum must match on both sides. For the orders table:
For Redis, redis-cli -u "<url>" DBSIZE returns the same number of keys for both URLs.
2

Compare the files

In the web terminal, list the files on the disk with their checksums, and compare the list with railway-files.txt:
3

Test the application

Click the domain under the application’s name and test the critical paths: a page that reads from the database, a write, a file from the disk, and a task for the worker. On the Processes tab, the web process and the background worker show Running, and the cron job Succeeded after its next run.With the API, Get application runtime logs (getApplicationRuntimeLogsV3) with search finds a line that the worker or the cron job logs. APP_ID comes from Deploy with the API:
CLI: sevalla apps logs runtime "$APP_ID" --search "Cron run". MCP: search_runtime_logs.
4

Turn on auto-deploy

To deploy every push to the branch, click the Settings tab, then click Update auto-deploy in the Auto-deploy section. Turn on Auto-deploy and click Update auto-deploy. See Deployments.
To rehearse, run the data copy once without stopping the Railway services, test on the sevalla.app domain, then repeat it for the real cutover: pg_restore --clean replaces the rehearsal’s tables, and for Redis, empty the database first or add rdb_restore_command_behavior = "rewrite" under an [advanced] section of shake.toml.

Move the custom domains

Add and verify each domain before you stop the Railway services: verification can take hours, and Railway keeps serving the domain meanwhile. Point it at Sevalla once the data is copied and verified. A day ahead, lower the TTL of the record that points at Railway, so the switch reaches visitors within minutes.
1

Add each domain

On the application’s Domains tab, click Add custom domain, enter the domain in Domain name, for example shop.example.com, and click Add custom domain.
2

Verify it

Add the ownership record (TXT on _cf-custom-hostname.shop.example.com) and the SSL validation record (TXT on _acme-challenge.shop.example.com) that Sevalla shows at your DNS provider. Keep the CNAME that points at Railway, and Railway’s TXT verification record, until the next step. When the domain’s status changes to Pointing, its certificate is ready. See Verify the domain.
3

Point it at Sevalla

Replace the CNAME that points at Railway (a target ending in up.railway.app) with the pointing record that Sevalla shows. The status changes to Active when Sevalla sees it. See Point the domain.
4

Remove it from Railway

Once the domain is Active, delete it from the Railway service with railway domain delete shop.example.com --service web, and delete Railway’s TXT verification record.
For a wildcard domain, Railway and Sevalla both use an _acme-challenge CNAME: replace Railway’s with Sevalla’s when you point the domain. If the domain’s DNS is on Cloudflare with the proxy on, as Railway’s guide suggests for root domains, see Domains on Cloudflare DNS. Add the custom domains to any setting that lists allowed hosts, then deploy.

Shut down the Railway project

When the domains are Active and traffic reaches Sevalla:
  1. Disable the external connections you opened: on each database’s Networking tab, click Disable under External connection, then click Disable in the dialog.
  2. Keep railway.dump, railway.rdb and volume.tar.gz until you no longer need them, and delete railway-config.json and railway.env, which contain secrets.
  3. Delete the Railway project with railway delete --project <project ID> once you’re sure. Deleting it deletes its databases and volumes, and their data.
To keep a staging environment, as Railway’s environments do, create a second application from the same repository and put both in a pipeline, one stage each. For Railway’s PR environments, turn on preview apps.

Troubleshoot

Deploy with the API

This sequence does what Create the databases to Verify the data and the application do in the dashboard, with the Sevalla API and Ingot: it creates the databases and the application, adds the variables, turns each Railway service into a process, adds the disk, deploys, and copies the data. A script or an AI agent runs it as is, in one shell, from the folder linked to the Railway project. Each step names the CLI command and the Sevalla MCP server tool that do the same. You need:
  • An API key with the Company developer role, in SEVALLA_API_KEY.
  • The repository on GitHub, and a GitHub account with access to it connected to Sevalla. The API has no call that reports the connection: without access, the step that creates the application fails with 400 and Cannot access repository https://github.com/acme/storefront: Repository not found or you don't have access., followed by where to connect a Git account (My account in the dashboard) and, for GitHub, to give Sevalla’s GitHub App access to the repository.
  • The tools in Before you begin, plus curl and openssl.
The API allows 20 service creations per hour per company. Export the configuration and the variables of the services that become processes, without the RAILWAY_* variables, PORT, and the variables that point at a Railway hostname, which get new values:
Both files contain secrets: don’t commit them.
1

Choose the location

List clusters (listClustersV3) returns every location with its region ID in location. Save the one you chose in Take stock of the Railway project, here eu-frankfurt-1 (Frankfurt, Germany):
CLI: sevalla resources clusters. MCP: get_platform_options.
2

Look up the sizes

List process resource types (listProcessResourceTypesV3), List database resource types (listDatabaseResourceTypesV3) and List disk resource types (listDiskResourceTypesV3). Save S1 for the processes, DB1, the smallest database size, and the 10 GB disk size, ps1; pick larger ones for more data:
CLI: sevalla resources process-types, sevalla resources db-types, sevalla resources disk-types. MCP: get_platform_options.
3

Look up the database versions

List database versions (listDatabaseVersionsV3) returns the versions the location offers, oldest first. Save the newest PostgreSQL and Redis versions, at least as new as Railway’s:
CLI: sevalla resources db-versions --cluster "$CLUSTER_ID". MCP: not needed, create_database takes the newest version by default.
4

Create the databases

Create database (createDatabaseV3), once for PostgreSQL and once for Redis. You choose the passwords, and the API never returns them, so keep them for the connection URLs:
CLI: sevalla databases create --name orders-db --type postgresql --db-version "$PG_VERSION" --cluster "$CLUSTER_ID" --resource-type "$DB_SIZE_ID" --db-name storefront --db-user storefront --db-password "$PG_PASSWORD". MCP: create_database.
5

Wait for the databases

Get database (getDatabaseV3) shows status ready when a database is up, with its internal hostname and port:
CLI: sevalla databases get "$PG_ID". MCP: get_database.
6

Create the application

Create application (createApplicationV3) creates the application with a web process at S1, which can have custom domains, and doesn’t deploy it:
CLI: sevalla apps create --name storefront --cluster "$CLUSTER_ID" --source privateGit --git-type github --repository "$REPO" --branch main. MCP: create_application with deploy: false.
7

Connect the databases

Create internal connection (createApplicationInternalConnectionV3), once per database:
The API adds no environment variables; the next step adds the two URLs. CLI: sevalla apps internal-connections create "$APP_ID" --target-id "$PG_ID" --target-type database. MCP: sevalla_api_create with createApplicationInternalConnectionV3 and the same body.
8

Add the environment variables

Create environment variable (createEnvironmentVariableV3), once per variable: DATABASE_URL and REDIS_URL in the format the internal connections use, then every entry of config.json:
CLI: sevalla apps env-vars create "$APP_ID" --key <key> --value <value>, once per variable. MCP: set_environment_variables with all the variables in one call.
9

Create the processes

Create process (createProcessV3), once per Railway service that deploys the same repository as web: a cron job for a service with a cron schedule, a background worker, its command after exec, for the others, then a job with the start policy beforeDeployment for the pre-deploy command. Each process is named after its service:
CLI: sevalla apps processes create "$APP_ID" --name worker --type worker --resource-type "$S1_ID" --command "exec node worker.js"; for the cron job --type cron --schedule "*/5 * * * *", and for the job --type job --job-start-policy beforeDeployment. MCP: sevalla_api_create with createProcessV3 and the same body.
10

Point WORKER_URL at the worker

List processes (listProcessesV3) returns each process’s internal_hostname. The worker listens on 8080, which the web process reaches without a private port. Add the variable that replaces http://${{worker.RAILWAY_PRIVATE_DOMAIN}}:${{worker.PORT}}:
CLI: sevalla apps processes list --app-id "$APP_ID". MCP: sevalla_api_read with listProcessesV3, then set_environment_variables.
11

Create the disk

Attach or update process disk (updateProcessDiskV3) on the web process, at the volume’s mount path:
CLI: sevalla apps processes disk update "$WEB_ID" --app-id "$APP_ID" --resource-type "$DISK_SIZE_ID" --mount-path "$MOUNT_PATH". MCP: sevalla_api_update with updateProcessDiskV3 and the same body.
12

Deploy

Trigger deployment (triggerDeploymentV3) deploys the latest commit of the branch:
CLI: sevalla apps deployments trigger "$APP_ID". MCP: trigger_deployment.
13

Wait for the deployment

Get deployment (getDeploymentV3) reports the status, which ends as success, failed, cancelled or skipped. The loop keeps polling through a failed request, for up to 30 minutes:
Get application deployment logs (getApplicationDeploymentLogsV3) with ?limit=5000 returns the whole build log. CLI: sevalla apps deployments get "$DEPLOYMENT_ID" --app-id "$APP_ID". MCP: get_deployment.
14

Open the databases to your computer

Stop the Railway services and download the volume first, as in Stop the Railway services. Toggle external connection (toggleDatabaseExternalConnectionV3) turns a database’s external connection on, or off when it’s on. Then Get database returns external_hostname and external_port:
CLI: sevalla databases connections external-toggle "$PG_ID". MCP: sevalla_api_update with toggleDatabaseExternalConnectionV3.
15

Copy the PostgreSQL and Redis data

The same commands as in Copy the PostgreSQL data and Copy the Redis data, with the values from the API:
16

Close the external connections

Toggle external connection again, after Copy the volume with the API and the checks:
17

Turn on auto-deploy

Update application (updateApplicationV3):
CLI: sevalla apps update "$APP_ID" --data '{"auto_deploy": true}'. MCP: sevalla_api_update with updateApplicationV3 and the same body.
To build with Railpack instead of Ingot, update the application with {"build_type": "railpack"} before you deploy (CLI: sevalla apps update "$APP_ID" --data '{"build_type": "railpack"}'). Add application domain (addApplicationDomainV3) adds each custom domain; List application domains then returns the DNS records in dns_records.

Copy the volume with the API

Run a command in a process (execProcessCommandV3) runs one command in the web process, which has the disk. A command’s argument can’t exceed 128 KB, so the archive goes up in parts of 96000 bytes, each sent as Base64 and appended to a file on the disk. This takes about 3 minutes for a 13 MB archive. Run it after Copy the volume files has made volume.tar.gz and railway-files.txt:
It prints 0 for the cleanup, then one line with the number of parts and 0, their exit code. Any other exit code means a part is missing: run the block again. Then unpack the archive, delete it, and compare the files with railway-files.txt:
CLI: sevalla apps processes exec "$WEB_ID" --app-id "$APP_ID" -- sh -c '<script>'. MCP: sevalla_api_update with execProcessCommandV3.