# 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](cli.md#deplane-import-fly), and every error has a fix in [Troubleshooting and error codes](errors.md#fly_token_missing). It works like [Moving from Heroku](heroku.md), which explains the switch-over in more detail.

## Before you start

1. [Install the Deplane CLI](install.md) and run `deplane login`.
2. Have the app's code in a folder, with its `fly.toml`: a checkout of the repository you deploy with `fly deploy`.
3. Have `flyctl` installed 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:

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

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

```sh
$ 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](workers.md), 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](workers.md) 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](limits.md). |
| `[deploy] release_command` | The [release command](release-command.md). |
| `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](#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 with `fly 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 the `pg_tls` handler to its `fly.toml`, and deploy it with the image of its Postgres version. Then copy it with `deplane db import --name APP-db --mode copy` using `PG_APP.fly.dev` as the host (see [Import a Postgres database](import-postgres.md)), and run `deplane db attach APP-db --app APP`, which sets `DATABASE_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 proxy` reaches 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 use `deplane db import`. Or email support@deplane.io.
- **A database elsewhere** (Neon, Supabase, RDS): set `DATABASE_URL` to 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](#secrets).
- **Volumes** are not copied. The web group's first mount is listed as a [disk](disks.md) 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 `http` handler, such as `pg_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 15m` where your plan allows it.
- **A prebuilt image** (`[build] image`) and buildpacks are not used: Deplane builds your folder. To run an image, use `deplane launch --image REF`.
- Deplane runs in one region, in Europe.

## Switch over

When the Deplane app works:

1. **Set the secrets and deploy**, as the next steps of the import say. Test the app at its Deplane address.
2. **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.
3. **Before the day:** run `deplane domains add HOSTNAME --app APP` for each custom domain and add the TXT record it shows.
4. **Move the database**, if it moves (see [Postgres](#postgres)), and point the app at it with `deplane db attach`.
5. **Switch DNS** to the target `deplane domains add` showed, and wait for the certificate (`deplane domains check HOSTNAME --app APP`).
6. **Scale the Fly app to zero** (`fly scale count 0 --app APP` for each process group), then destroy it with `fly apps destroy APP` once the app has run well on Deplane.
7. **Revoke the Fly.io token:** `fly tokens list`, then `fly tokens revoke ID`.

## Limits

The [Heroku import's limits](heroku.md#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.
