Migrate to Zenifra

Migrate from Railway to Zenifra

This guide shows where to find each setting of a Railway service and where to enter the same value in the Zenifra console. It covers services built from a Git repository or a Dockerfile, variables and reference variables, pre-deploy, cron, volumes, and Railway databases. For the general plan, with data copy and DNS switch, see Migrate to Zenifra.

Keep the Railway project running while you deploy and validate the application on Zenifra. Only remove the services after switching DNS.

How the concepts map

On RailwayOn Zenifra
ProjectOrganization with several projects
Service from a repository or imageHTTP project
Service without Public Networking (worker)Project with Exposure Private
Service with a Cron ScheduleScheduled job
Railpack or NixpacksRuntime and Version (Node.js, Python, Bun, Go, or Java)
Dockerfile builderOCI image
Variables and Shared VariablesEnvironment Variables of each project
ReplicasDesired Instances or auto-scaling
VolumePersistent storage
Postgres, MySQL, RedisPostgreSQL, MariaDB, and Cache
PR EnvironmentsPreview Environments

1. Copy the settings from Railway

In the Railway dashboard, open the project, click the service, and open the Settings tab.

Source and build

Railway fieldWhat it does
SourceThe service's repository and branch, or the deployed image
Root DirectoryApplication folder inside the repository (monorepo). The default is /
BuilderRailpack (automatic detection) or Dockerfile
Custom Build CommandBuild command, if set
Watch PathsFolders that trigger a new build. No equivalent: every change on the branch deploys

Deploy

Railway fieldWhat it does
Custom Start CommandCommand that starts the application
Pre-deploy commandRuns before each deploy, usually migrations
Healthcheck pathPath checked before the deploy goes live, such as /health
Cron ScheduleCron expression, when the service is a scheduled task
ReplicasNumber of instances

If the Custom Build Command or Custom Start Command is empty, Railpack chose the commands on its own. In that case, check the commands used in the logs of the last build and deploy, or use the build and start scripts from package.json.

Networking

In Settings › Networking, check whether the service has Public Networking, the Railway-generated domain, and the Custom Domains. Services without public networking are workers or internal services.

Variables

Open the service's Variables tab and click Raw Editor to see every variable in .env or JSON format and copy them at once. With the Railway CLI, in the linked project folder:

railway variable list --service my-service --kv > railway.env

The file contains secrets: do not commit it, and delete it after the migration. When reviewing it:

  • Reference variables, such as ${{ Postgres.DATABASE_URL }} or ${{ shared.API_KEY }}, show up resolved in the CLI. On Zenifra, each project has its own variables, so replace database URLs with those of the new databases and add shared variables to every project that uses them.
  • Railway variables, such as RAILWAY_PUBLIC_DOMAIN, RAILWAY_PRIVATE_DOMAIN, and RAILWAY_ENVIRONMENT_NAME, do not exist on Zenifra. If the code uses one, create your own variable with the equivalent value.
  • PORT: Railway sets the port. See Port.

railway.json or railway.toml

If the repository has a railway.json or railway.toml, the file's values take precedence over the dashboard. Write down build.buildCommand, build.dockerfilePath, deploy.startCommand, deploy.preDeployCommand, deploy.healthcheckPath, and deploy.cronSchedule.

2. Create the project on Zenifra

In the console, click + Create project, choose HTTP Application and the plan. Under Advanced Settings, choose Git repository as the Project Source, the Git provider GitHub (or Forgejo), the same repository, and the same branch. Then fill in:

On RailwayIn the Zenifra console
Language detected by RailpackRuntime and Version
Root DirectoryRoot directory. / on Railway is . on Zenifra
Custom Build CommandBuild Command, without the dependency installation
Pre-deploy commandBeginning of the Start Command; see Pre-deploy
Custom Start CommandStart Command
Healthcheck pathCheck path, under Edit › Application healthcheck (from the Premium plan up)
ReplicasDesired Instances
VariablesEnvironment Variables
Public Networking offExposure Private - no public route

Zenifra does not detect the language: you choose the Runtime and Version at creation. Use the same major version Railpack used, which it reads from engines.node, .nvmrc, or .node-version for Node.js and from .python-version or runtime.txt for Python.

Differences in dependency installation

Railpack detects the package manager on its own. On Zenifra:

  • Node.js installs with npm ci and needs a committed package-lock.json. If the service uses pnpm or Yarn, generate and commit a package-lock.json before migrating.
  • Bun installs with bun install --frozen-lockfile.
  • Python installs from requirements.txt. If the service uses Poetry, uv, pdm, or Pipenv, export a requirements.txt, for example with uv export --format requirements-txt > requirements.txt.

See Runtimes for each language's details.

Services with a Dockerfile or image

Services that use the Dockerfile builder, or that deploy a ready-made image, come in as a Docker Image: build the image, push it to a registry, and enter the image in the project. See Deploy via OCI image and Private registry. The GitHub Action automates the build and deploy on every push.

3. Adjust what differs between the platforms

Port

Railway sets the PORT variable and expects the application to listen on it. On Zenifra, the port is the project's Port field. Since your code already reads PORT, set both to the same value, for example Port 3000 and the variable PORT=3000. The application must listen on 0.0.0.0.

Pre-deploy

On Railway, the pre-deploy command runs once, in a separate container, before the deploy goes live. On Zenifra, put the command at the beginning of the Start Command, such as npm run migrate && npm start. It then runs every time an instance starts, so it must be idempotent. For long migrations or multiple instances, run the migration manually before deploying.

Private networking between services

On Railway, services talk to each other through service-name.railway.internal. On Zenifra, replace those addresses with the target project's URL, *.clients.zenifra.com or its domain, and protect internal routes with authentication. Zenifra databases and managed services already have their own connection URLs, shown in the console.

Cron

A service with a Cron Schedule becomes a scheduled job. Create the job with Task source GitHub Repository, the same Runtime, the service's start command in Task command, and the same Cron expression: both platforms use UTC. As on Railway, the command must exit on its own when the task finishes.

Workers

Services without public networking, such as queue consumers and bots, become projects with Exposure Private - no public route and the same Start Command.

4. Migrate databases and volumes

Postgres and MySQL

Railway databases are private by default. To copy the data from your machine, enable the database's public access in Settings › Networking. Railway then creates the DATABASE_PUBLIC_URL (Postgres) or MYSQL_PUBLIC_URL (MySQL) variable. Railway bills this traffic as network egress.

Create the database on Zenifra first, then copy the data:

# PostgreSQL
pg_dump --format=custom --no-owner --no-acl "$DATABASE_PUBLIC_URL" > dump.pgdump
pg_restore --no-owner --no-acl --jobs=4 \
  --dbname="postgresql://user:password@host:port/database?sslmode=verify-full&sslrootcert=system" \
  dump.pgdump

# MySQL to MariaDB
mysqldump --single-transaction --routines --triggers -h HOST -P PORT -u USER -p DATABASE > dump.sql
mariadb --ssl -h ZENIFRA_HOST -P PORT -u USER -p DATABASE < dump.sql

The MySQL host, port, and user are in MYSQL_PUBLIC_URL. After the copy, turn off the database's public access on Railway. See Migrate the database to validate the copy.

Redis

Create a managed cache or a Key-Value database and replace REDIS_URL. Cache and session data are usually rebuilt by the application. If Redis holds queues, let the Railway workers drain them before the switch.

MongoDB

Zenifra does not offer managed MongoDB. Keep the database at a MongoDB provider and point the project's MONGO_URL variable to it.

Volumes

On Railway, each service can have one volume mounted at a path. On Zenifra, enable persistent storage when you create the project and use the same path in Directory to Persist. This path cannot be changed later.

To copy the files, download them from the volume with the Railway CLI and send them to the new project, for example through a temporary password-protected import route. Files used as media or uploads can go to Armazém, which is S3-compatible.

5. Validate and switch DNS

  1. Open the project's *.clients.zenifra.com URL and test login, main flows, e-mails, workers, and webhooks.
  2. Check Build logs and the application logs for errors.
  3. Follow the switch-day checklist: lower the DNS TTL, run the final data copy, add the domain on Zenifra, and update DNS.
  4. Remove the service's Custom Domain on Railway once DNS points to Zenifra.
  5. Keep the Railway services for a few days for rollback, and only then remove the project.

Common issues

SymptomLikely causeFix
npm ci failsService uses pnpm or Yarn, without package-lock.jsonGenerate and commit a package-lock.json
Python build does not install dependenciesService uses Poetry, uv, pdm, or PipenvExport a requirements.txt
Application does not startEmpty Start CommandEnter the command Railpack used
Variable with a literal ${{ ... }}Reference variable copied without being resolvedReplace it with the real value, such as the new database URL
Error connecting to *.railway.internalRailway's private network does not exist on ZenifraUse the project or database URL on Zenifra
Files disappeared after a deployVolume was not recreatedEnable persistent storage with the same path

Need help?

Write to [email protected] with the list of services in your Railway project, or reach us on WhatsApp at +55 (11) 5199-3013.

Last updated on

On this page