Migrating from Fly.io
This guide is for teams running applications on Fly.io that want to move to Convox on their own cloud account (AWS, GCP, Azure, DigitalOcean) or on bare metal. Both platforms run containers from a Dockerfile, so the build itself maps over directly. The main work is translating fly.toml into a convox.yml, moving secrets, and re-pointing managed datastores.
The practical reasons to migrate are usually one of: you want the cluster to run inside your own cloud account, you want standard Kubernetes underneath instead of Fly Machines, or you want a single manifest that covers services, databases, scheduled jobs, and scaling in one place.
Prerequisites
- A running Convox Rack. See Installation.
- The
convoxCLI installed and logged in. - A
Dockerfilefor your app. If your Fly app used CNB Buildpacks (the[build] builderkey) rather than a Dockerfile, you will need to add one. See Dockerfile. If your Fly app already used[build] dockerfileor a prebuilt[build] image, you can reuse it as-is.
Concept Mapping
| Fly.io concept | Convox equivalent |
|---|---|
app name |
The Convox App name (set with convox apps create / -a) |
[build] dockerfile / [build] image |
Service build or image |
[build] builder (Buildpacks) |
A Dockerfile (Convox builds from a Dockerfile) |
[processes] (process groups) |
One Service per process, each with its own command |
[http_service] |
A Service with a port behind the default rack load balancer |
[http_service] internal_port |
Service port |
[http_service] force_https |
Service tls.redirect (default true) |
[[services]] + [[services.ports]] (raw TCP/UDP) |
Service port / ports, or a custom Balancer |
min_machines_running / auto_stop_machines |
scale.min / scale.max (set min: 0 for scale-to-zero) |
[[vm]] cpus / memory |
scale.cpu (1000 units = 1 CPU) / scale.memory (MB) |
[env] |
environment (non-secret defaults) |
flyctl secrets set |
convox env set |
| Fly Postgres / managed datastore | A Convox Resource or external DB |
[[mounts]] (Fly Volumes) |
A Volume |
[deploy] release_command |
Service initContainer or convox run |
| Scheduled work (supercronic / scheduled Machines) | A Timer |
| Internal-only service (no public handler) | Service internal: true |
convox.yml
Before: fly.toml
app = "myapp"
primary_region = "ord"
[build]
dockerfile = "Dockerfile"
[env]
RAILS_ENV = "production"
LOG_LEVEL = "info"
[processes]
web = "bundle exec rails server -b 0.0.0.0"
worker = "bundle exec sidekiq"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 2
processes = ["web"]
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
cpus = 1
Secrets on Fly are set out of band:
flyctl secrets set SECRET_KEY_BASE=... DATABASE_URL=...
After: convox.yml
environment:
- RAILS_ENV=production
- LOG_LEVEL=info
services:
web:
build: .
command: bundle exec rails server -b 0.0.0.0
port: 3000
scale:
count: 2
cpu: 250
memory: 512
worker:
build: .
command: bundle exec sidekiq
scale:
count: 1
cpu: 250
memory: 512
Notes on the translation:
- Each Fly
[processes]entry becomes its own Convox Service with the samecommand. Both share the onebuild: .because Fly runs every process group from the same image. - The
webservice gets aport, which puts it behind the rack load balancer and terminates TLS automatically. Theworkerhas noport, so it is not exposed, which matches a Fly process group with no service mapping. force_https = trueis the Convox default (tls.redirect), so you do not need to set anything for it.min_machines_running = 2becomesscale.count: 2. To reproduce Fly'sauto_stop_machinesscale-to-zero behavior, usescale.min: 0with an autoscale trigger instead of a static count.[[vm]]sizing maps toscale.cpu(in CPU units, where1000is one full CPU) andscale.memory(in MB). A Flyshared-cpu-1xwith512mbis roughlycpu: 250,memory: 512; size these against observed usage rather than the Fly preset name.
The static
countinconvox.ymlis only applied on the first deploy. After that, change replica counts withconvox scaleor an autoscale block. See Autoscaling.
Environment and Secrets
Fly splits configuration into [env] (plaintext, in fly.toml) and secrets (flyctl secrets set, stored encrypted and never written to the file). Convox treats both as environment variables; the difference is where the value lives.
- Non-secret values from
[env]go into theenvironment:block ofconvox.ymlwith inline defaults, for example- LOG_LEVEL=info. - Secret values that were set with
flyctl secrets setare declared by name (no value) inconvox.ymland have their values set withconvox env set:
environment:
- SECRET_KEY_BASE
- STRIPE_API_KEY
$ convox env set SECRET_KEY_BASE=... STRIPE_API_KEY=... -a myapp
Setting SECRET_KEY_BASE, STRIPE_API_KEY... OK
Release: RABCDEFGHI
Setting environment variables creates a new Release. Promote it (or run convox deploy) to apply the change. Declaring a variable name with no default makes it required before a release can promote, which is a useful guard against shipping with a missing secret.
To list what Fly currently has set, run flyctl secrets list (names only) and flyctl config show (for [env]), then re-create those values with convox env set.
Datastores
Fly Postgres (and other Fly datastores) are separate apps that hand your app a connection string, usually as the DATABASE_URL secret. On Convox you have two options.
Run the datastore as a Convox Resource. Declare it in convox.yml and link it to the services that need it. Convox injects connection environment variables based on the resource name:
resources:
database:
type: postgres
services:
web:
build: .
port: 3000
resources:
- database
worker:
build: .
command: bundle exec sidekiq
resources:
- database
A postgres resource named database injects DATABASE_URL, DATABASE_USER, DATABASE_PASS, DATABASE_HOST, DATABASE_PORT, and DATABASE_NAME. For production on AWS you can switch the same resource to a managed RDS instance with type: rds-postgres, without changing application code. See Resource for the full list of types (postgres, mysql, mariadb, redis, memcached, and their rds- / elasticache- managed variants) and the overlay pattern for using containerized databases in dev and managed databases in production.
Keep an external database. If you are migrating data into an existing managed database outside the rack, or want to point at Convox Cloud Databases, set the connection URL directly as an environment variable and do not declare a matching resource:
$ convox env set DATABASE_URL=postgres://user:pass@host:5432/dbname -a myapp
If you set an environment variable that matches a resource's injected URL (for example DATABASE_URL for a resource named database), Convox will not start the containerized resource and your service uses the external endpoint instead. See Resource Overlays.
To move the actual data, dump from Fly Postgres with pg_dump (connect through flyctl proxy to reach the Fly database) and load into the target. For a Convox Resource you can load through convox resources import or by proxying to it with convox resources proxy.
Scheduled Jobs and Workers
Fly has no scheduled-job key in fly.toml. Recurring work is typically run either by a long-lived worker process group (a [processes] entry) or by a cron tool such as supercronic baked into the image. Convox replaces both patterns with first-class primitives.
Long-lived workers map to a worker Service with no port, exactly like the worker example above.
Recurring/cron jobs map to a Timer. A Timer runs a command on a cron schedule against a named service. You can point it at an existing service or at a small template service scaled to zero so it only consumes resources when the job runs:
services:
worker:
build: .
command: bundle exec sidekiq
jobs:
build: .
scale:
count: 0
timers:
nightly-cleanup:
command: bin/cleanup
schedule: "0 3 * * *"
service: jobs
schedule uses standard cron syntax and all times are UTC. See Timer for the full attribute set, including concurrency and parallelCount.
Deploy and Cutover
-
Add
convox.yml(and aDockerfileif you were on Buildpacks) to your repository. -
Create the app and set secrets before the first deploy so required variables exist:
$ convox apps create myapp $ convox env set SECRET_KEY_BASE=... DATABASE_URL=... -a myapp -
Build and deploy. Use
convox deployto build, create a Release, and promote it in one step:$ convox deploy -a myappIf you need to run a one-off migration before traffic shifts (the equivalent of Fly's
[deploy] release_command), split it into two steps so you can run the migration against the new release before promoting:$ convox build -a myapp $ convox run web bin/migrate -a myapp $ convox releases promote RBCDEFGHIJ -a myappYou can also run migrations automatically on every deploy with a service
initContainer. See Deploying Changes. -
Get the Convox URL for the web service and verify the app end to end while Fly is still serving production traffic:
$ convox services -a myapp SERVICE DOMAIN PORTS web web.myapp.0a1b2c3d4e5f.convox.cloud 443:3000 -
Cut over DNS last. Point your custom domain at the Convox load balancer only after the app is verified healthy on Convox. Until DNS changes, Fly continues to serve traffic, so there is no downtime window during the migration itself. After DNS has propagated and traffic has drained from Fly, scale the Fly app down and decommission it. For attaching custom domains to a Convox service, see Networking.
Gotchas / What Is Different
- Regions. Fly's
primary_regionand edge placement have no direct equivalent. A Convox rack runs in one cloud region; for multi-region you run multiple racks. There is noprimary_regionkey inconvox.yml. - One image, many services. Fly process groups all run the same image and differ only by command. Convox can do the same (
build: .on each service), but it can also build different services from different paths or Dockerfiles. There is no shared globalcommand; each service sets its own. - Scale-to-zero is opt-in. Fly stops idle Machines by default with
auto_stop_machines. Convox runs a staticcountunless you configurescale.min: 0with an autoscale trigger. See Scale to Zero. - Internal vs public. On Fly, a process group is public only if it has a
[http_service]or[[services]]mapping. On Convox, a service is public if it has aport. Useinternal: trueto keep a service with a port reachable only inside the rack. - TLS. Convox provisions and renews certificates for service domains automatically and redirects HTTP to HTTPS by default. You do not configure handlers the way Fly does with
["tls", "http"]. - Process listen address. Make sure your app binds to
0.0.0.0and theportyou declare (Convox sets thePORTenvironment variable), the same requirement Fly has withinternal_port. - Persistent volumes are per-replica. A Fly Volume attaches to one Machine. A Convox Volume attaches per process; if you need shared read-write storage across replicas on AWS, use the EFS-backed
volumeOptions.awsEfsoption rather than assuming a single shared disk.
See Also
- convox.yml for the full manifest reference
- Service for service attributes
- Resource for databases and caches
- Timer for scheduled jobs
- Environment Variables for secrets and configuration
- Autoscaling for scale and scale-to-zero
- Deploying Changes for the deploy flow
- Dockerfile for building from a Dockerfile