> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sevalla.com/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> These are the docs for Sevalla, a platform for deploying applications, databases, static sites, object storage and load balancers. The dashboard is at https://app.sevalla.com.
> Bold text in steps is an exact dashboard label.
> To act on a Sevalla account, prefer the Sevalla MCP server (https://mcp.sevalla.com/mcp, see https://docs.sevalla.com/ai-agents/overview.md), then the Sevalla CLI (https://docs.sevalla.com/developer-tools/cli.md), then the REST API (https://api-docs.sevalla.com, OpenAPI spec at https://api.sevalla.com/v3/openapi.json).
> Every page index is at https://docs.sevalla.com/llms.txt.

# Migrate from Railway

> Move a Railway project to Sevalla: map services, Railpack, private networking, reference variables, volumes, cron and pre-deploy commands, then copy the data and move the domains.

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

| | |
| - | - |
| Source | The GitHub repository the Railway services deploy from |
| Build | [Railpack](https://docs.sevalla.com/applications/builds/railpack.md), as on Railway, with no changes; or [Ingot](https://docs.sevalla.com/applications/builds/ingot.md), Sevalla's default |
| Start command | The web service's start command, or the one the build detects, such as `npm start` from `package.json` |
| Port | The `PORT` environment variable, `8080` by default |
| Processes | Web process for the service with the public domain; a **Background worker** per long-running service, its command after `exec`; a **Cron job** per cron service; a **Job** with the start policy **Before deployment** for the pre-deploy command |
| Services | A PostgreSQL database for Railway PostgreSQL and a Redis database for Railway Redis, over internal connections |
| Disk | One per volume, on the process that had the volume, at the same mount path |
| Size | **S1** or larger for the web process if you move a custom domain, because **H1** can't have one |

### Environment variables

| Variable | Required | Available during | Secret | Value |
| - | - | - | - | - |
| `DATABASE_URL` | With Railway PostgreSQL | Build and runtime | Yes | `DB_URL` from the internal connection to `orders-db`, renamed: `postgres://<user>:<password>@<host>:<port>/<database>` |
| `REDIS_URL` | With Railway Redis | Build and runtime | Yes | Added by the internal connection to `storefront-redis`: `redis://:<password>@<host>:<port>/0` |
| A variable that points at another service, such as `WORKER_URL` | As on Railway | Build and runtime | No | The process's internal hostname, for example `http://storefront-x7k2m-a1b2c3d4.storefront-x7k2m.svc.cluster.local:8080` |
| The services' other variables | As on Railway | Build and runtime | As on Railway | From `railway variable list --kv`, without the `RAILWAY_*` variables Railway sets |
| `PORT` | No | Runtime | No | Set by Sevalla to `8080`. Don't add it |

## How Railway maps to Sevalla

| Railway | Sevalla | Notes |
| - | - | - |
| Project | [Project](https://docs.sevalla.com/projects/overview.md) | Groups services, with their usage and who can access them |
| Service from a GitHub repository | [Application](https://docs.sevalla.com/applications/overview.md) | Services that deploy the same repository and differ only in their start command become [processes](https://docs.sevalla.com/applications/processes.md) of one application. A service from another repository or a Docker image becomes its own application |
| Service with a public domain | Web process | Created with the application. Listens on `PORT`, `8080` by default |
| Service that runs all the time (a worker) | [Background worker](https://docs.sevalla.com/applications/processes.md#background-workers) | |
| Cron service (**Cron Schedule**, `cronSchedule`) | [Cron job](https://docs.sevalla.com/applications/processes.md#cron-jobs) | Any 5-field schedule, in UTC unless you set a time zone. Railway's 5-minute minimum doesn't apply |
| Pre-deploy command (`preDeployCommand`) | [Job](https://docs.sevalla.com/applications/processes.md#jobs) with the start policy **Before deployment** | Runs after the build, before the rollout. If it fails, the deployment fails and the previous version keeps running |
| Healthcheck path | [Readiness probe](https://docs.sevalla.com/applications/processes.md#health-checks) | Railway checks the path only during a deployment; Sevalla's probe keeps checking every instance |
| Replicas | [Instances](https://docs.sevalla.com/applications/scaling.md) | Per process, with horizontal autoscaling |
| Multi-region replicas | One application per [location](https://docs.sevalla.com/platform/locations.md) behind a [load balancer](https://docs.sevalla.com/load-balancers/overview.md) with geo routing | An application runs in one location |
| Serverless (app sleeping) | [Hibernation](https://docs.sevalla.com/applications/scaling.md#hibernation) | |
| Railpack | [Railpack](https://docs.sevalla.com/applications/builds/railpack.md) build strategy | Sevalla runs Railpack 0.30.0 |
| Dockerfile | [Dockerfile](https://docs.sevalla.com/applications/builds/dockerfile.md) build strategy | |
| Config as code (`railway.json`, `railway.toml`) and infrastructure as code (`.railway/railway.ts`) | Settings on the application, or the [Terraform provider](https://docs.sevalla.com/developer-tools/terraform.md) | Sevalla reads no settings file from the repository |
| Variables | [Environment variables](https://docs.sevalla.com/applications/environment-variables.md) | Per application: every process of the application gets the same variables |
| Shared variables (`${{shared.KEY}}`) | [Global variables](https://docs.sevalla.com/applications/environment-variables.md#global-variables) | Global variables reach every application in the company |
| Reference variables (`${{Postgres.DATABASE_URL}}`) | [Variables from internal connections](https://docs.sevalla.com/applications/environment-variables.md#variables-from-internal-connections) and [references](https://docs.sevalla.com/applications/environment-variables.md#reference-another-variable) (`${KEY}`) | See [Move the variables](#move-the-variables) |
| Sealed variables | Environment variables | Railway never returns a sealed value: type it in again |
| Railway-provided variables (`RAILWAY_*`) | [Variables Sevalla sets](https://docs.sevalla.com/applications/environment-variables.md#variables-sevalla-sets) (`SVL_*`) | |
| Private networking (`<service>.railway.internal`) | [Internal hostnames](https://docs.sevalla.com/applications/networking.md#internal-hostnames) and [internal connections](https://docs.sevalla.com/applications/networking.md#internal-connections) | |
| TCP proxy on a service | [TCP proxy](https://docs.sevalla.com/applications/networking.md#tcp-proxies) | Per process |
| TCP proxy on a database (Public Access) | [External connection](https://docs.sevalla.com/databases/connect.md#enable-the-external-connection) | |
| Volume | [Disk](https://docs.sevalla.com/applications/disks.md) | One per web process or background worker. A process with a disk runs 1 instance |
| Railway PostgreSQL | [PostgreSQL database](https://docs.sevalla.com/databases/overview.md) | |
| Railway Redis | Redis or Valkey database | |
| Railway MySQL | MySQL database | |
| Railway MongoDB | None | Sevalla has no MongoDB database: use a hosted MongoDB service |
| Buckets | [Object storage](https://docs.sevalla.com/object-storage/overview.md) | S3-compatible buckets |
| Environments (production, staging) | One application per environment, in a [pipeline](https://docs.sevalla.com/pipelines/overview.md) with a stage per environment | Promote a build from one stage to the next |
| PR environments | [Preview apps](https://docs.sevalla.com/pipelines/preview-apps.md) | Created for pull requests in a pipeline |
| `*.up.railway.app` domain | The application's `sevalla.app` domain | |
| Custom domains | [Custom domains](https://docs.sevalla.com/domains/overview.md) | Sevalla issues and renews the SSL certificates |
| Watch paths | [Deploy paths](https://docs.sevalla.com/applications/settings.md#deploy-paths) | |
| `railway logs` | [Logs](https://docs.sevalla.com/applications/logs.md) | |
| `railway ssh` | [Web terminal](https://docs.sevalla.com/applications/web-terminal.md) | Also `sevalla apps processes terminal` |
| Metrics | [Analytics](https://docs.sevalla.com/applications/analytics.md) | |
| Workspace members | [Users and roles](https://docs.sevalla.com/company/users-and-roles.md) | |
| Public API and CLI | [Sevalla API](https://docs.sevalla.com/developer-tools/api.md) and [CLI](https://docs.sevalla.com/developer-tools/cli.md) | |

## Before you begin

* The [Railway CLI](https://docs.railway.com/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](https://docs.sevalla.com/git/overview.md).
* 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](https://docs.sevalla.com/databases/import-export/postgresql.md#before-you-begin).
* For Redis: `redis-cli`, and [RedisShake](https://github.com/tair-opensource/RedisShake/releases) 4.6 or later. See [Import and export Redis](https://docs.sevalla.com/databases/import-export/redis.md#before-you-begin).
* 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](#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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
railway config pull --json --include-variables > railway-config.json
```

`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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
jq -r '.resources[] | select(.type == "service")
  | [.name, .deploy.startCommand // "-", .deploy.cronSchedule // "-",
     (.deploy.preDeployCommand // ["-"] | join(" ")), ([.volumeAttachments[]?.mountPath] | first // "-")] | @tsv' railway-config.json
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
web	-	-	npm run migrate	/data
cron	node cron.js	*/5 * * * *	-	-
worker	node worker.js	-	-	-
```

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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
jq -r '.resources[] | select(.type == "database") | "\(.name)\t\(.engine)"' railway-config.json
jq -r '.resources[] | select(.type == "service") | .name as $service
  | .variables // {} | to_entries[] | select(.value.value // "" | contains("${{"))
  | "\($service)\t\(.key)=\(.value.value)"' railway-config.json
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
Postgres	postgres
Redis	redis
web	DATABASE_URL=${{Postgres.DATABASE_URL}}
web	NODE_ENV=${{shared.NODE_ENV}}
web	REDIS_URL=${{Redis.REDIS_URL}}
web	UPLOADS_DIR=${{RAILWAY_VOLUME_MOUNT_PATH}}
web	WORKER_URL=http://${{worker.RAILWAY_PRIVATE_DOMAIN}}:${{worker.PORT}}
cron	DATABASE_URL=${{Postgres.DATABASE_URL}}
cron	NODE_ENV=${{shared.NODE_ENV}}
worker	DATABASE_URL=${{Postgres.DATABASE_URL}}
worker	NODE_ENV=${{shared.NODE_ENV}}
worker	REDIS_URL=${{Redis.REDIS_URL}}
```

* 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](https://docs.sevalla.com/platform/locations.md).
* `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](#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](#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](https://docs.sevalla.com/databases/create.md).

## 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](https://docs.sevalla.com/applications/create.md).

**2. Choose the build strategy**

Sevalla builds with [Ingot](https://docs.sevalla.com/applications/builds/ingot.md) 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](#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:

| | Railpack | Ingot |
| - | - | - |
| Version | Railpack 0.30.0, in beta on Sevalla and older than Railway's (0.43.0 in the test) | Ingot 0.5.0 |
| Node.js | `24.22.0`, from `engines.node` (`24.x`) | `24.22.2`, on the official `node:24-trixie-slim` image, from `engines.node` |
| Web process | `npm run start` | `node server.js`, from `scripts.start`, under `tini` |
| User | `root` | `node` (`1000:1000`) |
| First deployment | 2m 40s | 1m 54s |

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](https://docs.sevalla.com/databases/connect.md#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**

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for service in web worker cron; do railway variable list --service "$service" --kv; done \
  | grep -Ev '^(RAILWAY_|PORT=)' | sort -u > railway.env
cut -d= -f1 railway.env
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DATABASE_URL
NODE_ENV
REDIS_URL
UPLOADS_DIR
WORKER_URL
```

`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](#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**.

| Railway reference | Sevalla value |
| - | - |
| `${{Postgres.DATABASE_URL}}` | `DATABASE_URL` from the internal connection |
| `${{Redis.REDIS_URL}}` | `REDIS_URL` from the internal connection |
| `http://${{worker.RAILWAY_PRIVATE_DOMAIN}}:${{worker.PORT}}` | The process's internal hostname and port, from [Reach a process on the private network](#reach-a-process-on-the-private-network) |
| `${{shared.NODE_ENV}}` | The value itself, or a [global variable](https://docs.sevalla.com/applications/environment-variables.md#global-variables) with the same key when other applications need it too |
| `${{KEY}}`, a variable of the same service | `${KEY}`: Sevalla resolves it when it deploys. See [Reference another variable](https://docs.sevalla.com/applications/environment-variables.md#reference-another-variable) |
| `${{RAILWAY_VOLUME_MOUNT_PATH}}` | The disk's mount path, `/data` |
| `${{web.RAILWAY_PUBLIC_DOMAIN}}` | The application's domain, under its name in the page header, or the custom domain |

Don't add `PORT`: Sevalla sets it to `8080` for the web process and background workers. See [Environment variables](https://docs.sevalla.com/applications/environment-variables.md).

## Recreate the services as processes

| Railway service | Sevalla process | What to set |
| - | - | - |
| `web`, with the public domain | The existing **Web** process | Nothing when Railway detects the start command, as here. Otherwise its start command as the **Custom start command**, after `exec`. For a **Healthcheck Path**, add a **Readiness probe** with that **Path** in the process's **Health checks** section |
| `worker`, running all the time | A **Background worker** | Its start command after `exec` as the **Custom start command**: `exec node worker.js`. `exec` makes the command the instance's main process, so it gets `SIGTERM` when the instance stops |
| `cron`, with a **Cron Schedule** | A **Cron job** | Its start command as the **Custom start command**, `node cron.js`, and the same **Schedule**, `*/5 * * * *`, in UTC as on Railway |
| The pre-deploy command of `web` | A **Job** with the **Start policy** **Before deployment** | The command, `npm run migrate`, as the **Custom start command** |

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](https://docs.sevalla.com/applications/processes.md).

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](https://docs.sevalla.com/applications/networking.md#private-ports). To reach another application, add an [internal connection](https://docs.sevalla.com/applications/networking.md#internal-connections) 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](https://docs.sevalla.com/applications/disks.md#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](https://docs.sevalla.com/applications/disks.md).

## 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**

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
railway down --service worker --yes
railway down --service cron --yes
```

**3. Download the volume**

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
railway volume files --volume web-volume download / railway-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**

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
railway down --service web --yes
```

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**

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
pg_dump "$(railway variable list --service Postgres --json | jq -r .DATABASE_PUBLIC_URL)" \
  --format=custom --no-owner --no-privileges --file=railway.dump
```

**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](https://docs.sevalla.com/databases/connect.md#enable-the-external-connection).

**4. Restore the dump**

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
pg_restore --dbname="postgres://storefront:<password>@<external hostname>:<external port>/storefront" \
  --clean --if-exists --no-owner --no-privileges --jobs=4 railway.dump
```

`--clean --if-exists` drops the tables the first deployment created before it restores Railway's. See [Import a database](https://docs.sevalla.com/databases/import-export/postgresql.md#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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
redis-cli -u "$(railway variable list --service Redis --json | jq -r .REDIS_PUBLIC_URL)" --rdb railway.rdb
```

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:

```toml shake.toml theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
[rdb_reader]
filepath = "/Users/you/railway.rdb"

[redis_writer]
cluster = false
address = "<external hostname>:<external port>"
username = ""
password = "<password>"
tls = false
```

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
./redis-shake shake.toml
```

RedisShake writes every key, with its expiry, and ends with `all done`. See [Import and export Redis](https://docs.sevalla.com/databases/import-export/redis.md).

## Copy the volume files

**1. Archive the download**

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
COPYFILE_DISABLE=1 tar --no-xattrs --exclude=lost+found -czf volume.tar.gz railway-volume
cd railway-volume && find . -type f -not -path './lost+found/*' -exec shasum -a 256 {} + | LC_ALL=C sort -k 2 > ../railway-files.txt; cd ..
```

`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](https://docs.sevalla.com/applications/web-terminal.md#copy-files-to-and-from-a-process).

**3. Unpack it**

In the terminal:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
tar -xzf /data/volume.tar.gz -C /data --strip-components=1 --no-same-owner && rm /data/volume.tar.gz
```

`--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](#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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for url in "$(railway variable list --service Postgres --json | jq -r .DATABASE_PUBLIC_URL)" \
  "postgres://storefront:<password>@<external hostname>:<external port>/storefront"; do
  psql "$url" -At -c "SELECT count(*), md5(string_agg(t::text, ',' ORDER BY id)) FROM orders t"
done
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
300|fea6868e0c0e427a70f8d87605dc5960
300|fea6868e0c0e427a70f8d87605dc5960
```

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`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
cd /data && find . -type f -not -path './lost+found/*' -exec sha256sum {} + | LC_ALL=C sort -k 2
```

**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](https://api-docs.sevalla.com/v3/applications/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](#deploy-with-the-api):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s "$API/applications/$APP_ID/runtime-logs?search=Cron%20run" -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.[].message'
```

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](https://docs.sevalla.com/applications/deployments.md#deploy-automatically-on-push).

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](https://docs.sevalla.com/domains/overview.md#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](https://docs.sevalla.com/domains/overview.md#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](https://docs.sevalla.com/domains/overview.md#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](https://docs.sevalla.com/pipelines/overview.md), one stage each. For Railway's PR environments, turn on [preview apps](https://docs.sevalla.com/pipelines/preview-apps.md).

## Troubleshoot

| Symptom | Cause | Fix |
| - | - | - |
| `getaddrinfo ENOTFOUND postgres.railway.internal` (or another `*.railway.internal` host) in the runtime logs | A variable still holds a Railway private hostname | Replace it with the internal connection's variable or the process's internal hostname, then deploy. See [Move the variables](#move-the-variables) |
| ``SSH authentication failed. Ensure one of your keys is registered with Railway using `railway ssh keys add`.`` from `railway volume files` | No SSH key of yours is registered with Railway | Run `railway ssh keys add`, then run the command again |
| `null` instead of a URL from `railway variable list ... jq -r .DATABASE_PUBLIC_URL` | The database has no Public Access | Add **Public Access** in the database's **Settings** > **Networking** on Railway |
| `pg_restore: error: connection to server at "..." failed: server closed the connection unexpectedly` | The external connection was enabled seconds ago | Wait a minute and run `pg_restore` again |
| `ERR stat <folder>/data/railway.rdb: no such file or directory` from RedisShake | `filepath` in `shake.toml` is relative: RedisShake reads it from its `data` folder | Set `filepath` to the absolute path of `railway.rdb` |
| `tar: .: Cannot change mode to rwxrwxr-x: Operation not permitted` when you unpack the archive | The archive has a `.` entry, made with `-C railway-volume .`, and the process doesn't run as `root` | Make the archive from the `railway-volume` folder and unpack it with `--strip-components=1`, as in [Copy the volume files](#copy-the-volume-files). The files are unpacked anyway |
| `pre-deploy has failed` in the build log, and `Error: connect ECONNREFUSED ::1:5432` in the runtime logs | `DATABASE_URL` isn't set, so the job connects to `localhost` | Add the internal connection with its variables, as in [Connect the databases](#connect-the-databases), then deploy |
| `argument list too long` from [Run a command in a process](https://api-docs.sevalla.com/v3/applications/run-a-command-in-a-process) | A single argument is larger than 128 KB | Split the file into parts of `96000` bytes, as in [Copy the volume with the API](#copy-the-volume-with-the-api) |
| `tar: Ignoring unknown extended header keyword 'LIBARCHIVE.xattr.com.apple.provenance'`, and `._*` files on the disk | The archive was made with macOS's `tar` without `COPYFILE_DISABLE=1` and `--no-xattrs` | Delete the `._*` files, or make the archive again as in [Copy the volume files](#copy-the-volume-files) |
| New rows keep reaching the Railway database after `railway down --service cron` | Railway deploys a cron service again at each scheduled time | Clear the service's **Cron Schedule** in its **Settings**, then run `railway down` again |
| The background worker or cron job runs the web server | The process has no custom start command | Set the Railway service's start command as its **Custom start command** |

## Deploy with the API

This sequence does what [Create the databases](#create-the-databases) to [Verify the data and the application](#verify-the-data-and-the-application) do in the dashboard, with the [Sevalla API](https://docs.sevalla.com/developer-tools/api.md) 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](https://docs.sevalla.com/developer-tools/cli.md) command and the [Sevalla MCP server](https://docs.sevalla.com/ai-agents/overview.md) tool that do the same. You need:

* An [API key](https://docs.sevalla.com/developer-tools/api-keys.md) 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](#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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
railway config pull --json --include-variables > railway-config.json
for service in web worker cron; do railway variable list --service "$service" --json; done \
  | jq -s 'add | with_entries(select((.key | startswith("RAILWAY_") | not) and .key != "PORT"
      and (.value | test("railway\\.internal|rlwy\\.net|railway\\.app") | not)))' > config.json
jq -c . config.json
API=https://api.sevalla.com/v3
REPO=https://github.com/acme/storefront
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"NODE_ENV":"production","UPLOADS_DIR":"/data"}
```

Both files contain secrets: don't commit them.

**1. Choose the location**

[List clusters](https://api-docs.sevalla.com/v3/resources/list-clusters) (`listClustersV3`) returns every location with its region ID in `location`. Save the one you chose in [Take stock of the Railway project](#take-stock-of-the-railway-project), here `eu-frankfurt-1` (Frankfurt, Germany):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
CLUSTER_ID=$(curl -s "$API/resources/clusters" -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.location == "eu-frankfurt-1") | .id')
```

CLI: `sevalla resources clusters`. MCP: `get_platform_options`.

**2. Look up the sizes**

[List process resource types](https://api-docs.sevalla.com/v3/resources/list-process-resource-types) (`listProcessResourceTypesV3`), [List database resource types](https://api-docs.sevalla.com/v3/resources/list-database-resource-types) (`listDatabaseResourceTypesV3`) and [List disk resource types](https://api-docs.sevalla.com/v3/resources/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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
S1_ID=$(curl -s "$API/resources/process-resource-types" -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.name == "s1") | .id')
DB_SIZE_ID=$(curl -s "$API/resources/database-resource-types" -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.name == "db1") | .id')
DISK_SIZE_ID=$(curl -s "$API/resources/disk-resource-types" -H "Authorization: Bearer $SEVALLA_API_KEY" \
  | jq -r '.[] | select(.name == "ps1") | .id')
```

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](https://api-docs.sevalla.com/v3/resources/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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
VERSIONS=$(curl -s "$API/resources/database-versions?cluster_id=$CLUSTER_ID" -H "Authorization: Bearer $SEVALLA_API_KEY")
PG_VERSION=$(echo "$VERSIONS" | jq -r '.[] | select(.type == "postgresql") | .versions[-1]')
REDIS_VERSION=$(echo "$VERSIONS" | jq -r '.[] | select(.type == "redis") | .versions[-1]')
```

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](https://api-docs.sevalla.com/v3/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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
PG_PASSWORD=$(openssl rand -hex 16)
PG_ID=$(curl -s -X POST "$API/databases" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg cluster "$CLUSTER_ID" --arg size "$DB_SIZE_ID" --arg version "$PG_VERSION" --arg password "$PG_PASSWORD" \
    '{display_name: "orders-db", type: "postgresql", version: $version, cluster_id: $cluster,
      resource_type_id: $size, db_name: "storefront", db_user: "storefront", db_password: $password}')" \
  | jq -r .id)
REDIS_PASSWORD=$(openssl rand -hex 16)
REDIS_ID=$(curl -s -X POST "$API/databases" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg cluster "$CLUSTER_ID" --arg size "$DB_SIZE_ID" --arg version "$REDIS_VERSION" --arg password "$REDIS_PASSWORD" \
    '{display_name: "storefront-redis", type: "redis", version: $version, cluster_id: $cluster,
      resource_type_id: $size, db_name: "storefront", db_password: $password}')" \
  | jq -r .id)
```

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](https://api-docs.sevalla.com/v3/databases/get-database) (`getDatabaseV3`) shows `status` `ready` when a database is up, with its internal hostname and port:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for id in "$PG_ID" "$REDIS_ID"; do
  for i in $(seq 1 60); do
    [ "$(curl -s "$API/databases/$id" -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.status // empty')" = ready ] && break
    sleep 10
  done
done
PG=$(curl -s "$API/databases/$PG_ID" -H "Authorization: Bearer $SEVALLA_API_KEY")
REDIS=$(curl -s "$API/databases/$REDIS_ID" -H "Authorization: Bearer $SEVALLA_API_KEY")
```

CLI: `sevalla databases get "$PG_ID"`. MCP: `get_database`.

**6. Create the application**

[Create application](https://api-docs.sevalla.com/v3/applications/create-application) (`createApplicationV3`) creates the application with a web process at **S1**, which can have custom domains, and doesn't deploy it:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
APP_ID=$(curl -s -X POST "$API/applications" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg cluster "$CLUSTER_ID" --arg repo "$REPO" \
    '{display_name: "storefront", cluster_id: $cluster, source: "privateGit", git_type: "github",
      repo_url: $repo, default_branch: "main"}')" \
  | jq -r .id)
```

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](https://api-docs.sevalla.com/v3/applications/create-internal-connection) (`createApplicationInternalConnectionV3`), once per database:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for id in "$PG_ID" "$REDIS_ID"; do
  curl -s -X POST "$API/applications/$APP_ID/internal-connections" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq -n --arg db "$id" '{target_id: $db, target_type: "database"}')" | jq -r .id
done
```

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](https://api-docs.sevalla.com/v3/applications/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`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{
  jq -n --arg host "$(echo "$PG" | jq -r .internal_hostname)" --arg port "$(echo "$PG" | jq -r .internal_port)" --arg password "$PG_PASSWORD" \
    '{key: "DATABASE_URL", value: "postgres://storefront:\($password)@\($host):\($port)/storefront"}'
  jq -n --arg host "$(echo "$REDIS" | jq -r .internal_hostname)" --arg port "$(echo "$REDIS" | jq -r .internal_port)" --arg password "$REDIS_PASSWORD" \
    '{key: "REDIS_URL", value: "redis://:\($password)@\($host):\($port)/0"}'
  jq -c 'to_entries[] | {key, value}' config.json
} | jq -c '. + {is_runtime: true, is_buildtime: true}' | while IFS= read -r body; do
  curl -s -X POST "$API/applications/$APP_ID/env-vars" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" -d "$body" | jq -r .key
done
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DATABASE_URL
REDIS_URL
NODE_ENV
UPLOADS_DIR
```

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](https://api-docs.sevalla.com/v3/applications/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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
jq -c --arg size "$S1_ID" '(.resources[] | select(.name == "web")) as $web
  | {resource_type_id: $size, scaling_strategy: {type: "manual", config: {instanceCount: 1}}} as $common
  | (.resources[] | select(.type == "service" and .name != "web" and .source.repo == $web.source.repo)
      | $common + {display_name: .name}
        + if .deploy.cronSchedule then {type: "cron", entrypoint: .deploy.startCommand, schedule: .deploy.cronSchedule}
          else {type: "worker", entrypoint: "exec \(.deploy.startCommand)"} end),
    ($web.deploy.preDeployCommand // empty
      | $common + {display_name: "pre-deploy", type: "job", entrypoint: join(" "), job_start_policy: "beforeDeployment"})' \
  railway-config.json | while IFS= read -r body; do
  curl -s -X POST "$API/applications/$APP_ID/processes" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" -d "$body" \
    | jq -c '{display_name, type, entrypoint}'
done
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"display_name":"cron","type":"cron","entrypoint":"node cron.js"}
{"display_name":"worker","type":"worker","entrypoint":"exec node worker.js"}
{"display_name":"pre-deploy","type":"job","entrypoint":"npm run migrate"}
```

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](https://api-docs.sevalla.com/v3/applications/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}}`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
PROCESSES=$(curl -s "$API/applications/$APP_ID/processes" -H "Authorization: Bearer $SEVALLA_API_KEY")
WORKER_HOST=$(echo "$PROCESSES" | jq -r '.data[] | select(.display_name == "worker") | .internal_hostname')
curl -s -X POST "$API/applications/$APP_ID/env-vars" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg url "http://$WORKER_HOST:8080" '{key: "WORKER_URL", value: $url, is_runtime: true, is_buildtime: true}')" \
  | jq -r .key
```

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](https://api-docs.sevalla.com/v3/applications/attach-or-update-process-disk) (`updateProcessDiskV3`) on the web process, at the volume's mount path:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
WEB_ID=$(echo "$PROCESSES" | jq -r '.data[] | select(.type == "web") | .id')
MOUNT_PATH=$(jq -r '.resources[] | select(.name == "web") | [.volumeAttachments[].mountPath] | first' railway-config.json)
curl -s -X PUT "$API/applications/$APP_ID/processes/$WEB_ID/disk" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d "$(jq -n --arg size "$DISK_SIZE_ID" --arg path "$MOUNT_PATH" '{resource_type_id: $size, mount_path: $path}')" \
  | jq -c '{size_gib, mount_path}'
```

```json theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
{"size_gib":10,"mount_path":"/data"}
```

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](https://api-docs.sevalla.com/v3/applications/trigger-deployment) (`triggerDeploymentV3`) deploys the latest commit of the branch:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
DEPLOYMENT_ID=$(curl -s -X POST "$API/applications/$APP_ID/deployments" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" -d '{}' | jq -r .id)
```

CLI: `sevalla apps deployments trigger "$APP_ID"`. MCP: `trigger_deployment`.

**13. Wait for the deployment**

[Get deployment](https://api-docs.sevalla.com/v3/applications/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:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for i in $(seq 1 120); do
  STATUS=$(curl -s "$API/applications/$APP_ID/deployments/$DEPLOYMENT_ID" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.status // empty')
  case $STATUS in success|failed|cancelled|skipped) break ;; esac
  sleep 15
done
echo "$STATUS"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
success
```

[Get application deployment logs](https://api-docs.sevalla.com/v3/applications/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](#stop-the-railway-services). [Toggle external connection](https://api-docs.sevalla.com/v3/databases/toggle-external-connection) (`toggleDatabaseExternalConnectionV3`) turns a database's external connection on, or off when it's on. Then [Get database](https://api-docs.sevalla.com/v3/databases/get-database) returns `external_hostname` and `external_port`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for id in "$PG_ID" "$REDIS_ID"; do
  curl -s -X POST "$API/databases/$id/external-connection/toggle" -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r .message
  for i in $(seq 1 60); do
    [ -n "$(curl -s "$API/databases/$id" -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r '.external_hostname // empty')" ] && break
    sleep 5
  done
done
PG=$(curl -s "$API/databases/$PG_ID" -H "Authorization: Bearer $SEVALLA_API_KEY")
REDIS=$(curl -s "$API/databases/$REDIS_ID" -H "Authorization: Bearer $SEVALLA_API_KEY")
SEVALLA_DATABASE_URL="postgres://storefront:$PG_PASSWORD@$(echo "$PG" | jq -r .external_hostname):$(echo "$PG" | jq -r .external_port)/storefront"
for i in $(seq 1 30); do psql "$SEVALLA_DATABASE_URL" -c 'SELECT 1' > /dev/null 2>&1 && break; sleep 10; done
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
External connection toggle started
External connection toggle started
```

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](#copy-the-postgresql-data) and [Copy the Redis data](#copy-the-redis-data), with the values from the API:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
pg_dump "$(railway variable list --service Postgres --json | jq -r .DATABASE_PUBLIC_URL)" \
  --format=custom --no-owner --no-privileges --file=railway.dump
pg_restore --dbname="$SEVALLA_DATABASE_URL" --clean --if-exists --no-owner --no-privileges --jobs=4 railway.dump
redis-cli -u "$(railway variable list --service Redis --json | jq -r .REDIS_PUBLIC_URL)" --rdb railway.rdb
printf '[rdb_reader]\nfilepath = "%s/railway.rdb"\n\n[redis_writer]\ncluster = false\naddress = "%s:%s"\nusername = ""\npassword = "%s"\ntls = false\n' \
  "$PWD" "$(echo "$REDIS" | jq -r .external_hostname)" "$(echo "$REDIS" | jq -r .external_port)" "$REDIS_PASSWORD" > shake.toml
./redis-shake shake.toml
```

**16. Close the external connections**

[Toggle external connection](https://api-docs.sevalla.com/v3/databases/toggle-external-connection) again, after [Copy the volume with the API](#copy-the-volume-with-the-api) and the checks:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
for id in "$PG_ID" "$REDIS_ID"; do
  curl -s -X POST "$API/databases/$id/external-connection/toggle" -H "Authorization: Bearer $SEVALLA_API_KEY" | jq -r .message
done
```

**17. Turn on auto-deploy**

[Update application](https://api-docs.sevalla.com/v3/applications/update-application) (`updateApplicationV3`):

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
curl -s -X PATCH "$API/applications/$APP_ID" \
  -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
  -d '{"auto_deploy": true}' | jq -r .auto_deploy
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
true
```

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](https://api-docs.sevalla.com/v3/applications/add-application-domain) (`addApplicationDomainV3`) adds each custom domain; [List application domains](https://api-docs.sevalla.com/v3/applications/list-application-domains) then returns the DNS records in `dns_records`.

### Copy the volume with the API

[Run a command in a process](https://api-docs.sevalla.com/v3/applications/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](#copy-the-volume-files) has made `volume.tar.gz` and `railway-files.txt`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
exec_web() {
  curl -s -X POST "$API/applications/$APP_ID/processes/$WEB_ID/exec" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq -n --arg script "$1" '{command: ["sh", "-c", $script], timeout: 60}')"
}
exec_web 'rm -f /data/.volume.tar.gz' | jq -r .exit_code
split -b 96000 volume.tar.gz volume.part.
for part in volume.part.*; do
  curl -s -X POST "$API/applications/$APP_ID/processes/$WEB_ID/exec" \
    -H "Authorization: Bearer $SEVALLA_API_KEY" -H "Content-Type: application/json" \
    -d "$(jq -n --arg data "$(base64 < "$part" | tr -d '\n')" \
      '{command: ["sh", "-c", "printf %s \"$1\" | base64 -d >> /data/.volume.tar.gz", "sh", $data]}')" \
    | jq -r .exit_code
done | sort | uniq -c
```

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`:

```bash theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
exec_web 'tar -xzf /data/.volume.tar.gz -C /data --strip-components=1 --no-same-owner && rm /data/.volume.tar.gz' | jq -r .exit_code
exec_web "cd /data && find . -type f -not -path './lost+found/*' -exec sha256sum {} + | LC_ALL=C sort -k 2" \
  | jq -j .stdout | diff railway-files.txt - && echo "Files match"
```

```text theme={"theme":{"light":"github-dark-default","dark":"github-dark-default"}}
0
Files match
```

CLI: `sevalla apps processes exec "$WEB_ID" --app-id "$APP_ID" -- sh -c '<script>'`. MCP: `sevalla_api_update` with `execProcessCommandV3`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.