---
id: platform/migrate-from-fly
title: Migrate from Fly.io
description: Map fly.toml processes, ports, checks, mounts and release commands to a Bex render.yaml, and see which Fly.io settings have no Bex equivalent.
keywords: [bex, migrate, fly.io, fly.toml, render.yaml, migration guide]
last_updated: 2026-10-05
---

A [`fly.toml`](https://docs.fly.io/reference/configuration/) file configures
one Fly.io app: its build, processes, HTTP service, checks, volume mounts and
release command. A Bex [render.yaml](./app-resource.md) describes managed
services, datastores and disks. Most of a fly.toml maps, but Fly's regions,
Machine sizes, proxy settings and static-file serving do not, and moving a file
moves no data.

Bex is in active development and its upstream project does not yet recommend
production workloads. Begin with a non-production rehearsal and check
[compatibility](./compatibility.md) and required capabilities before proceeding.
Compare costs with [Fly.io pricing](https://fly.io/pricing/) and
[Bex pricing](/pricing) directly; this guide does not restate either.

## Use the converter

Paste your `fly.toml`, and optionally the output of `fly secrets list`, into
the [Fly.io converter](/tools/fly-to-render-yaml). It is a browser starting
point: it drafts a render.yaml by the rules below, lists every element it could
not map with the reason, and checks the draft with the
[render.yaml checker](/tools/render-yaml-checker). `bex blueprints validate`
remains the authoritative check. Nothing you paste leaves your browser.

## How fly.toml maps to Bex

| fly.toml | Bex |
| --- | --- |
| `[build] dockerfile`, or no build settings | Docker runtime (`dockerfilePath`) |
| `[build] image` | Image runtime; the image's own command runs |
| `builder`, `buildpacks` | Not mapped: a native runtime when the process commands show one, else Docker |
| Process the HTTP service targets | Web service |
| Other processes, or every process when there is no HTTP service | Background worker |
| Process behind a TCP `[[services]]` entry | Private service (`pserv`); public TCP ports and UDP are not mapped |
| `internal_port` (Fly's default is 8080) | `PORT`, unless it is 3000, Bex's injected default |
| HTTP check with a `path` | `healthCheckPath` |
| `auto_stop_machines`, `auto_start_machines`, `min_machines_running` | Explained: only free-tier web services sleep when idle |
| One `[[mounts]]` entry on one process | A disk (paid plan, single instance); `initial_size` from 10 to 10,000 GB becomes `sizeGB` |
| A second mount, a shared mount, a mount on a process kept above one Machine | Not mapped, with the reason |
| `[deploy] release_command` | `preDeployCommand` on the web service |
| `[env]`, secret names from `fly secrets list` | One environment group; secrets become `sync: false` |
| `primary_region`, `[[vm]]`, `[[statics]]`, `[metrics]`, `[[restart]]`, `swap_size_mb`, `kill_signal`, `force_https`, `concurrency` | Not mapped, with the reason |

## Example

This is the converter page's Rails sample. It builds a production Dockerfile,
prepares the database as a release command, and serves HTTP on port 8080:

```toml
app = "ledger"
primary_region = "fra"
kill_signal = "SIGTERM"
kill_timeout = 30
swap_size_mb = 512
console_command = "/rails/bin/rails console"

[build]
  dockerfile = "Dockerfile.production"
  build-target = "runtime"

  [build.args]
    RUBY_VERSION = "3.3.5"

[deploy]
  release_command = "./bin/rails db:prepare"
  strategy = "bluegreen"

[env]
  RAILS_ENV = "production"
  RAILS_LOG_TO_STDOUT = "enabled"
  PORT = "8080"
  SECRET_KEY_BASE = "do-not-commit-this-placeholder"

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "suspend"
  min_machines_running = 1

  [http_service.concurrency]
    type = "requests"
    soft_limit = 200
    hard_limit = 250

  [[http_service.checks]]
    grace_period = "10s"
    interval = "30s"
    method = "GET"
    timeout = "5s"
    path = "/up"

[[statics]]
  guest_path = "/rails/public"
  url_prefix = "/"
```

Its secrets, as `fly secrets list` prints them (names and digests, no values):

```text
NAME                    DIGEST                  CREATED AT
DATABASE_URL            a1b2c3d4e5f6a7b8        2026-09-01T10:00:00Z
SECRET_KEY_BASE         0f1e2d3c4b5a6978        2026-09-01T10:00:00Z
STRIPE_API_KEY          9988776655443322        2026-09-12T08:30:00Z
```

The converter drafts this render.yaml from them:

```yaml
# Draft render.yaml for Bex, converted from Fly.io by bex.co/tools.
# A starting point, not a migration: replace the placeholders, enter secrets
# in the dashboard, choose plans, and move data, domains and add-on
# credentials separately. Then run `bex blueprints validate`.
services:
  # From fly.toml line 1: app
  # plan: not set; choose one at https://bex.co/pricing
  - type: web
    name: ledger
    runtime: docker
    repo: https://github.com/YOUR-ORG/YOUR-REPO  # replace with your repository
    branch: YOUR-BRANCH  # replace with your deploy branch
    dockerfilePath: Dockerfile.production
    preDeployCommand: ./bin/rails db:prepare
    healthCheckPath: /up
    envVars:
      - key: PORT
        value: "8080"
      - fromGroup: ledger-config
envVarGroups:
  # From fly.toml line 19: env
  - name: ledger-config
    envVars:
      - key: RAILS_ENV
        value: production
      - key: RAILS_LOG_TO_STDOUT
        value: enabled
      - key: SECRET_KEY_BASE
        sync: false  # set this secret in the dashboard
      - key: DATABASE_URL
        sync: false  # set this secret in the dashboard
      - key: STRIPE_API_KEY
        sync: false  # set this secret in the dashboard
```

Replace the repository and branch placeholders, choose a plan, and enter each
`sync: false` value before you validate it. `SECRET_KEY_BASE` is in both `[env]`
and the secrets; as on Fly.io the secret wins, so the draft leaves the `[env]`
value out and asks for the secret instead. The
converter lists the region, kill and swap settings, `console_command`, the build
target and arguments, `force_https`, `concurrency`, the deploy strategy and
`[[statics]]` as not mapped, each with its reason.

## Processes, ports and checks

Without `[processes]`, Fly runs every Machine with the same command; with it,
each process group runs its own command
([Fly.io configuration reference](https://docs.fly.io/reference/configuration/),
checked 2026-10-01). The process `[http_service]` targets becomes a Bex
[web service](./web-services.md), and the others become
[background workers](./background-workers.md).

Fly's `internal_port` defaults to 8080, and Bex's injected `PORT` defaults to
3000. Bex does not detect ports, so the draft sets `PORT` to the Fly port. An
HTTP check's `path` becomes `healthCheckPath`, which Bex probes with an HTTP GET
([health checks](./health-checks.md)). Ports below 1024 are not mapped: tenant
containers drop Linux capabilities ([Docker deploys](./docker-deploys.md)).

Fly's `release_command` runs a one-off task before Machines are created or
updated, and fails the deploy on a non-zero exit. The draft makes it the web
service's [pre-deploy command](./pre-deploy-commands.md), which runs in the new
image without the service's disk.

## Secrets and environment

Fly's `[env]` is for non-sensitive values; secrets are set with the secrets
command, take precedence over `[env]`, and their values cannot be read back
([Fly.io secrets](https://docs.fly.io/apps/secrets/), checked 2026-10-01). The
converter therefore takes only secret names and marks them `sync: false`. Enter
each value through Bex's [secrets interface](./secrets.md).

## Volumes and disks

A Fly volume attaches to one Machine and is not replicated
([Fly.io volumes](https://docs.fly.io/volumes/overview/), checked 2026-10-01).
One mount on one process becomes a [persistent disk](./persistent-disks.md):
one disk per paid service, a single instance, 10 to 10,000 GB. The disk starts
empty; copy files with an application-appropriate export and import.

## What does not map

Fly's `auto_stop_machines` and `min_machines_running` let the Fly proxy stop
idle Machines. On Bex only free-tier web services sleep when idle, and there is
no render.yaml idle field ([idle services](./idle-services.md)). Regions, Machine
sizes, `[[statics]]`, `[metrics]`, restart policies, swap and kill signals have
no render.yaml setting; the converter lists each one. Fly Postgres, Upstash,
Tigris, certificates and IP addresses are not in fly.toml at all.

## Move data and traffic

Rehearse the data transfer and the cutover with steps 4 and 5 of
[Migrate from Render](./migrate-from-render.md); they apply to any source host.
Stop every source writer,
including Fly worker processes, before the final transfer. Add your domains
through [Bex domain setup](./custom-domains.md) and use the DNS records it
returns, not the ones that pointed at Fly.io.

See [Bex vs Fly.io](/compare/fly-io) for a product comparison.
