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 Railway | On Zenifra |
|---|---|
| Project | Organization with several projects |
| Service from a repository or image | HTTP project |
| Service without Public Networking (worker) | Project with Exposure Private |
| Service with a Cron Schedule | Scheduled job |
| Railpack or Nixpacks | Runtime and Version (Node.js, Python, Bun, Go, or Java) |
| Dockerfile builder | OCI image |
| Variables and Shared Variables | Environment Variables of each project |
| Replicas | Desired Instances or auto-scaling |
| Volume | Persistent storage |
| Postgres, MySQL, Redis | PostgreSQL, MariaDB, and Cache |
| PR Environments | Preview 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 field | What it does |
|---|---|
| Source | The service's repository and branch, or the deployed image |
| Root Directory | Application folder inside the repository (monorepo). The default is / |
| Builder | Railpack (automatic detection) or Dockerfile |
| Custom Build Command | Build command, if set |
| Watch Paths | Folders that trigger a new build. No equivalent: every change on the branch deploys |
Deploy
| Railway field | What it does |
|---|---|
| Custom Start Command | Command that starts the application |
| Pre-deploy command | Runs before each deploy, usually migrations |
| Healthcheck path | Path checked before the deploy goes live, such as /health |
| Cron Schedule | Cron expression, when the service is a scheduled task |
| Replicas | Number 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.envThe 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, andRAILWAY_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 Railway | In the Zenifra console |
|---|---|
| Language detected by Railpack | Runtime and Version |
| Root Directory | Root directory. / on Railway is . on Zenifra |
| Custom Build Command | Build Command, without the dependency installation |
| Pre-deploy command | Beginning of the Start Command; see Pre-deploy |
| Custom Start Command | Start Command |
| Healthcheck path | Check path, under Edit › Application healthcheck (from the Premium plan up) |
| Replicas | Desired Instances |
| Variables | Environment Variables |
| Public Networking off | Exposure 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 ciand needs a committedpackage-lock.json. If the service uses pnpm or Yarn, generate and commit apackage-lock.jsonbefore migrating. - Bun installs with
bun install --frozen-lockfile. - Python installs from
requirements.txt. If the service uses Poetry, uv, pdm, or Pipenv, export arequirements.txt, for example withuv 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.sqlThe 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
- Open the project's
*.clients.zenifra.comURL and test login, main flows, e-mails, workers, and webhooks. - Check Build logs and the application logs for errors.
- Follow the switch-day checklist: lower the DNS TTL, run the final data copy, add the domain on Zenifra, and update DNS.
- Remove the service's Custom Domain on Railway once DNS points to Zenifra.
- Keep the Railway services for a few days for rollback, and only then remove the project.
Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
npm ci fails | Service uses pnpm or Yarn, without package-lock.json | Generate and commit a package-lock.json |
| Python build does not install dependencies | Service uses Poetry, uv, pdm, or Pipenv | Export a requirements.txt |
| Application does not start | Empty Start Command | Enter the command Railpack used |
Variable with a literal ${{ ... }} | Reference variable copied without being resolved | Replace it with the real value, such as the new database URL |
Error connecting to *.railway.internal | Railway's private network does not exist on Zenifra | Use the project or database URL on Zenifra |
| Files disappeared after a deploy | Volume was not recreated | Enable 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
Migrate from Render to Zenifra
Step-by-step guide to bring a Render Web Service to Zenifra: where to copy the Build Command, Start Command, Root Directory, and variables in Render and where to enter each value in the Zenifra console.
Migrate from Fly.io to Zenifra
Step-by-step guide to bring a Fly.io app to Zenifra: how to translate fly.toml (internal_port, processes, release_command, mounts, checks), recover secrets, copy Postgres, volumes, and Tigris, and where to enter each value in the console.