Moving from Fly.io
Move a Fly app to Deplane with one command. deplane import fly reads the app on Fly.io and the fly.toml in your code, then shows what moves and what stays. It then creates the Deplane app with its variables, workers, cron jobs and release command. Fly.io keeps serving your users until you switch.
The command runs on your own computer with your Fly.io token, which never leaves it. It only reads Fly.io: nothing there is changed. The command and its flags are in the CLI reference, and every error has a fix in Troubleshooting and error codes. It works like Moving from Heroku, which explains the switch-over in more detail.
Before you start
- Install the Deplane CLI and run
deplane login. - Have the app's code in a folder, with its
fly.toml: a checkout of the repository you deploy withfly deploy. - Have
flyctlinstalled and logged in, to create a read-only token.
The Fly.io token. Create a read-only token that expires soon, and pipe it straight in:
fly tokens create readonly --expiry 1h | deplane import fly my-app --check
fly tokens create readonly makes a token that can only read one organization (add --org ORG for another one). The CLI sends it as Fly.io's docs say, Authorization: Bearer FlyV1 fm2_..., prefix and all. You can also paste a token at the hidden prompt, or set FLY_API_TOKEN. A piped token wins over FLY_API_TOKEN, which flyctl uses too. There is no flag for the token. The CLI says which one it used, never the token itself. It is never written to disk, never printed, left out of --debug, and never sent to Deplane. Revoke it when you are done: fly tokens list, then fly tokens revoke ID.
Secrets
Fly.io never gives secret values back: its API and fly secrets list show only names and digests. So the import cannot copy your secrets. It lists them by name instead. Then it creates the app without deploying it, since the app would start without them, and the next steps show what to set:
deplane secrets set DATABASE_URL=... SECRET_KEY_BASE=... --app my-app
cd my-app && deplane launch --app my-app # deploys the app and writes deplane.toml
To see the values, run fly ssh console -a my-app -C env on a running Machine. It prints the Machine's environment, secrets included, in your own terminal. Deplane never runs it for you. The variables in fly.toml's [env] are not secret, and they are copied.
Check first
$ fly tokens create readonly --expiry 1h | deplane import fly shop --check
Fly app shop (org acme, 4 Machine(s)) → Deplane app shop
Code
✓ Deploys /home/me/shop with your Dockerfile: Detected a Dockerfile, port 8080.
Processes
✓ web: process group app, 2 Machine(s) of 1 shared CPU(s), 512 MB on Fly.io → 2 small instance(s).
✓ release_command → release command `bin/rails db:migrate`. ...
✓ worker (process group): 1 Machine(s) of 1 shared CPU(s), 1 GB on Fly.io → worker worker, 1 medium instance(s): `bundle exec sidekiq`.
⚠ Machine nightly (scheduled daily) → cron job nightly, `@daily` (UTC), on a small instance: `bin/rails report`. ...
⚠ Fly.io mounts the volume data at /data: its files are not copied. ...
Data
⚠ DATABASE_URL is a Fly.io secret, so its value (and the database it names) cannot be read: no database is copied. ...
Config vars
✓ 2 copied as secrets: RAILS_ENV, RAILS_LOG_TO_STDOUT.
⚠ Not copied, Fly.io never gives secret values through its API (only names and digests): DATABASE_URL, SECRET_KEY_BASE. ...
Domains
⚠ www.shop.example points at Fly.io (shop.fly.dev). It moves when you switch ...
Plan
✓ Fits your Team plan: after the import the team uses 4 of 40 instances and 2.5 GB of 16 GB memory.
Nothing was changed (--check). Run the command again without --check to import.
✓ is what moves as it is, ⚠ is what you should know or do yourself, and ✗ is what stops the import. --check --json prints the same report as one JSON document; secrets_to_set lists the secrets to set, and deploys says whether the import deploys. Then run the command again without --check (in a terminal it asks first; from a script, add --yes). The flags are the same as for Heroku: --source DIR, --name NAME, --check, --yes.
What moves
| On Fly.io | On Deplane |
|---|---|
fly.toml | Read from the folder you deploy (--source). A fly.toml for another app is a ✗. Without one, the processes and ports are read from the app's Machines. |
The process group that serves HTTP: [http_service], or [[services]] with a port that has the http handler (app by default) | The app's web process. Instances: its number of Machines. Railpack starts your Procfile's web line or your Dockerfile's CMD; the report warns when that differs from the group's command. |
Other process groups ([processes]) with Machines | Workers, with the same command and as many instances as running Machines. Standby Machines (the stopped spare fly deploy adds to a group without services) are not instances: Deplane restarts a failed instance itself. A group without Machines, or with a command Deplane cannot take (more than one line, or over 1000 characters), is listed, not created. |
Scheduled Machines (fly machine run --schedule) of the app's own image | Cron jobs at @hourly, @daily, @weekly or @monthly. Fly.io starts them at some time in the interval; Deplane at its start, in UTC. |
| Machine sizes | By memory: up to 512 MB small, 1 GB medium, 2 GB large. Larger: large, with a warning that Deplane has less memory (2 GB at most). See Limits. |
[deploy] release_command | The release command. |
internal_port | Deplane sends traffic to the port it detects and sets PORT; the report warns when that differs from internal_port. |
[env] | Secrets. Not copied: FLY_*, PRIMARY_REGION and PORT. Values that name Fly.io's private network (.internal, .flycast) are copied with a warning. |
| Secrets | Listed by name, not copied: see Secrets. |
| Certificates (custom domains) | Listed, and moved at the switch. |
| The code | Your folder, built by Railpack, or by your Dockerfile if it has one at its root. |
Postgres
No database is copied, because Fly.io gives no secret values: the CLI cannot read DATABASE_URL. The report says what it found:
- Fly Postgres (an app made with
fly postgres create, attached withfly postgres attach). It is on Fly.io's private network. Deplane's importer can only copy it once it has a public address. Fly.io's guide calls this connecting externally: allocate an IP to the Postgres app (fly ips allocate-v4 --app PG_APP, or IPv6), add a service on port 5432 with thepg_tlshandler to itsfly.toml, and deploy it with the image of its Postgres version. Then copy it withdeplane db import --name APP-db --mode copyusingPG_APP.fly.devas the host (see Import a Postgres database), and rundeplane db attach APP-db --app APP, which setsDATABASE_URL. Fly.io no longer maintains this kind of Postgres. Remove the public address after the switch. - Fly Managed Postgres (
fly mpg). The report names the cluster attached to the app. A Managed Postgres cluster is reachable only inside your Fly.io organization:fly mpg proxyreaches it from your own computer, not from Deplane. Neither Deplane's importer nor the app on Deplane can reach it. To move it, give it a public address with a proxy app in your organization (Fly.io does not support this), then usedeplane db import. Or email support@deplane.io. - A database elsewhere (Neon, Supabase, RDS): set
DATABASE_URLto the same value on Deplane, and the app keeps using it.
Redis. If REDIS_URL is a secret (Upstash Redis on Fly.io sets one), no Redis is created. Create one with deplane redis create APP-redis and deplane redis attach APP-redis --app APP; it starts empty. Or set REDIS_URL to a Redis that Deplane can reach.
What does not move
- Secret values: see Secrets.
- Volumes are not copied. The web group's first mount is listed as a disk to add with
deplane disks create; copy the files yourself. A Deplane disk has 5 GB, and an app with a disk runs one instance. Workers get no disk. - A second process group that serves traffic is not created: a Deplane app has one web process. Import it as its own app.
- Raw TCP or UDP services (a port without the
httphandler, such aspg_tls) do not move: Deplane routes HTTP only. An app that serves no HTTP at all is a ✗. - Machines outside any process group, and scheduled Machines of another image or without a command, are listed and not created.
[[statics]]are served by Fly.io's proxy: have the app serve those files itself.- Auto-stop: a Deplane app runs all the time, or sleeps when idle with
deplane scale --sleep 15mwhere your plan allows it. - A prebuilt image (
[build] image) and buildpacks are not used: Deplane builds your folder. To run an image, usedeplane launch --image REF. - Deplane runs in one region, in Europe.
Switch over
When the Deplane app works:
- Set the secrets and deploy, as the next steps of the import say. Test the app at its Deplane address.
- Workers and cron jobs run on both platforms once the app is deployed. If workers must not run twice (queues, scheduled emails), stop them until you switch:
deplane processes scale NAME 0 --app APP. A cron job cannot be paused: remove it (deplane processes remove NAME --app APP) and add it again at the switch, or destroy the Fly.io scheduled Machine first. - Before the day: run
deplane domains add HOSTNAME --app APPfor each custom domain and add the TXT record it shows. - Move the database, if it moves (see Postgres), and point the app at it with
deplane db attach. - Switch DNS to the target
deplane domains addshowed, and wait for the certificate (deplane domains check HOSTNAME --app APP). - Scale the Fly app to zero (
fly scale count 0 --app APPfor each process group), then destroy it withfly apps destroy APPonce the app has run well on Deplane. - Revoke the Fly.io token:
fly tokens list, thenfly tokens revoke ID.
Limits
The Heroku import's limits apply: 10 workers and cron jobs per app, sizes up to 2 GB, one disk of 5 GB. Secret values and databases are not copied. The import is built on Fly.io's public Machines API; tell support@deplane.io if Fly.io answers something the CLI does not expect.
This page is also available as Markdown. AI agents can start at /llms.txt.