Migrate from Fly.io to Zenifra
This guide shows how to translate each part of a Fly.io app to the Zenifra console. On Fly.io, almost everything lives in fly.toml and the fly CLI, so the guide follows that file section by section. For the general plan, with data copy and DNS switch, see Migrate to Zenifra.
Keep the Fly.io app running while you deploy and validate the application on Zenifra. Only destroy the Machines after switching DNS.
How the concepts map
| On Fly.io | On Zenifra |
|---|---|
| App | HTTP project |
| Machines of the web process | Desired Instances or auto-scaling |
Groups in [processes] | One project per group |
| Dockerfile build | OCI image or Git repository with a runtime |
internal_port | Port |
[env] and secrets | Environment Variables |
release_command | Beginning of the Start Command or a manual step |
| HTTP checks | Healthcheck (from the Premium plan up) |
Volumes in [[mounts]] | Persistent storage |
| Scheduled Machines | Scheduled jobs |
| Fly Postgres or Managed Postgres | Managed PostgreSQL |
| Upstash Redis | Managed cache or Key-Value |
| Tigris | Armazém |
One important difference: on Fly.io, auto_stop_machines can stop Machines without traffic. On Zenifra, the application stays up. With Hourly billing, you pay for the time it runs; see Which HTTP plan to choose.
1. Read fly.toml
Open fly.toml at the repository root and write down each section:
app = "my-app"
primary_region = "gru"
[build]
dockerfile = "Dockerfile"
[env]
NODE_ENV = "production"
[deploy]
release_command = "npx prisma migrate deploy"
[processes]
web = "node server.js"
worker = "node worker.js"
[http_service]
internal_port = 8080
processes = ["web"]
min_machines_running = 1
[[http_service.checks]]
path = "/health"
interval = "15s"
[[mounts]]
source = "data"
destination = "/data"fly.toml section | In the Zenifra console |
|---|---|
[build] dockerfile, image, or builder | Choice of source; see step 2 |
[env] | Environment Variables |
[deploy] release_command | See Release command |
[processes] | One project per group; the command goes in the Start Command |
[http_service] internal_port | Port |
[http_service] min_machines_running and fly scale count | Desired Instances |
[[http_service.checks]] path | Check path, under Edit › Application healthcheck |
[[mounts]] destination | Directory to Persist |
[[vm]] memory and cpus | Choice of plan |
[[statics]] | Serve the files from the application itself |
primary_region | Not applicable: Zenifra runs in Brazil |
Also check with the CLI what is not in the file:
fly scale show -a my-app # number and size of Machines
fly secrets list -a my-app # secret names
fly certs list -a my-app # domains with certificates
fly volumes list -a my-app # volumes2. Choose the project source
Fly.io builds almost every app from a Dockerfile, often generated by fly launch. On Zenifra you have two paths:
Option A: Git repository with a runtime
If the app is Node.js, Python, Bun, Go, or Java, you can set the Dockerfile aside and deploy from the repository. Zenifra generates the build environment on its own and ignores the Dockerfile.
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 repository, and the branch. Then fill in:
| Where it comes from | In the Zenifra console |
|---|---|
Language and version in the Dockerfile FROM, such as node:22 | Runtime and Version |
| App folder in the repository | Root directory |
Build RUN command in the Dockerfile, such as npm run build | Build Command |
web group in [processes] or the Dockerfile CMD | Start Command |
internal_port | Port |
See each language's commands in Runtimes.
Option B: Docker image
To keep the same Dockerfile, or for languages such as PHP, Ruby, Elixir, and .NET, deploy as a Docker Image: build the image, push it to a registry, such as GitHub Container Registry or Docker Hub, and enter the image in the project.
docker build --platform linux/amd64 -t ghcr.io/my-org/my-app:1.0.0 .
docker push ghcr.io/my-org/my-app:1.0.0The image must include linux/amd64. Under Project Source, choose Docker Image, enter the image and, for a private registry, the credentials. See Deploy via OCI image and Private registry. The GitHub Action builds and deploys on every push.
3. Recover variables and secrets
The variables in the [env] section are in fly.toml: copy them directly.
Secrets cannot be read with the CLI: fly secrets list only shows each one's name and a digest. Get the values from their source, such as the team's password manager, your CI, or the .env used with fly secrets import. If you have no other copy, the secrets are in the environment variables of a running Machine, which you can check through fly ssh console, taking care not to leave the values in your terminal history.
When reviewing the variables:
DATABASE_URLandREDIS_URLpoint to Fly.io databases. Replace them with the database URLs on Zenifra.- Tigris credentials (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_ENDPOINT_URL_S3, andBUCKET_NAME) change if you move the files to Armazém. - Fly.io variables, such as
FLY_APP_NAME,FLY_REGION,FLY_MACHINE_ID, andPRIMARY_REGION, do not exist on Zenifra. If the code uses one, create a variable with the equivalent value. - If the code reads
PORT, createPORTwith the same value as the Port field.
4. Adjust what differs between the platforms
Release command
On Fly.io, release_command runs once per deploy, on a temporary Machine, before the app is updated. On Zenifra, put the command at the beginning of the Start Command, such as npx prisma migrate deploy && node server.js. 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.
Processes
Each [processes] group becomes a project on Zenifra, with the same repository or image and the group's command in the Start Command. Groups that receive no traffic, such as workers, use Exposure Private - no public route.
Private networking
On Fly.io, apps talk to each other through my-app.internal or my-app.flycast. 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.
Scheduled tasks
Machines created with fly machine run --schedule (hourly, daily, weekly, or monthly) and processes with cron inside the container become scheduled jobs. On Zenifra, the Cron expression sets the exact time, in UTC, such as 0 6 * * * for every day at 06:00 UTC.
Static files
The [[statics]] section made Fly.io serve a folder from the image directly. On Zenifra, serve those files from the application itself, for example with express.static in Node.js or WhiteNoise in Django.
5. Migrate databases, volumes, and files
Postgres
Create the managed PostgreSQL on Zenifra before the application. To copy the data, open a tunnel to the Fly.io database and run pg_dump on your machine.
Managed Postgres: in one terminal, open the proxy. By default, it listens on localhost:16380:
fly mpg proxy CLUSTER_IDFly Postgres (Postgres app): open the proxy on a free port:
fly proxy 15432:5432 -a my-app-dbIn another terminal, use the user, password, and database from DATABASE_URL, replacing the host and port with the proxy's:
pg_dump --format=custom --no-owner --no-acl \
"postgresql://user:password@localhost:15432/database" > dump.pgdump
pg_restore --no-owner --no-acl --jobs=4 \
--dbname="postgresql://user:password@host:port/database?sslmode=verify-full&sslrootcert=system" \
dump.pgdumpWith Managed Postgres, use port 16380 instead of 15432. 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 Fly.io workers drain them before the switch.
Volumes
On Fly.io, each volume belongs to one Machine and is not replicated. To copy the files to your machine:
fly ssh sftp get -R -a my-app /data ./volume-dataIf the app has more than one Machine with a volume, repeat for each one with -s to pick the Machine, because each volume has different data.
On Zenifra, enable persistent storage when you create the project, with the same path in Directory to Persist, and send the files through a temporary password-protected import route. This path cannot be changed later. Persistent storage data is replicated.
Tigris
Zenifra's Armazém is S3-compatible, like Tigris. Download the bucket's files with the tool you already use and upload them to a bucket in Armazém with the AWS CLI or the S3 SDK. Then replace the endpoint, keys, and bucket name in the project variables. See Connect applications.
6. 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.
- Once DNS points to Zenifra, remove the domain's certificates on Fly.io with
fly certs remove. - Keep the Fly.io app for a few days for rollback. Then destroy the app, volumes, and databases to stop billing.
Common issues
| Symptom | Likely cause | Fix |
|---|---|---|
| Public URL does not respond | Port differs from internal_port, or the app listens on localhost | Use the same port and listen on 0.0.0.0 |
| Image does not start | Image built only for ARM | Build with --platform linux/amd64 |
Error connecting to *.internal | Fly.io's private network does not exist on Zenifra | Use the project or database URL on Zenifra |
| Empty variable | A Fly.io secret was not copied | Recover the value at its source and add it to the project |
| Tables do not exist | release_command does not run on Zenifra | Run the migrations; see Release command |
| Worker stopped | [processes] group without a project | Create a private project for each group |
Need help?
Write to [email protected] with your fly.toml, or reach us on WhatsApp at +55 (11) 5199-3013.
Last updated on
Migrate from Railway to Zenifra
Step-by-step guide to bring Railway services to Zenifra: where to copy Root Directory, build and start commands, and variables, how to translate reference variables, cron, volumes, and databases, and where to enter each value in the console.
Troubleshooting
Common Zenifra issues related to deploy, database, port, domain, and operation.