A fly.toml file configures
one Fly.io app: its build, processes, HTTP service, checks, volume mounts and
release command. A Bex render.yaml 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 and required capabilities before proceeding. Compare costs with Fly.io pricing and Bex 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. 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. 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:
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):
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:00ZThe converter drafts this render.yaml from them:
# 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 dashboardReplace 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,
checked 2026-10-01). The process [http_service] targets becomes a Bex
web service, and the others become
background workers.
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). Ports below 1024 are not mapped: tenant
containers drop Linux capabilities (Docker deploys).
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, 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, checked 2026-10-01). The
converter therefore takes only secret names and marks them sync: false. Enter
each value through Bex's secrets interface.
Volumes and disks
A Fly volume attaches to one Machine and is not replicated (Fly.io volumes, checked 2026-10-01). One mount on one process becomes a persistent disk: 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). 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; 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 and use the DNS records it returns, not the ones that pointed at Fly.io.
See Bex vs Fly.io for a product comparison.