· fly-io / heroku / deployment
Migrate from Heroku to Fly.io: Step-by-Step Guide (2026)
Move your Heroku app to Fly.io for ~$4/mo. Step-by-step guide with CLI commands for Postgres import, Redis, file storage, secrets, and scheduled tasks.
By Ethan
1,211 words · 7 min read
Move to Fly.io. It runs a standard Heroku app — web dyno plus Postgres — for roughly $4/mo, compared to Heroku’s $10/mo floor. The migration takes under an hour for most apps. One thing to know before you start: Fly’s Managed Postgres costs $38/mo and is more expensive than Heroku for small apps. Use self-managed (fly postgres) instead.
Who this is for
A working Heroku app you want to move without rewriting. Node.js, Python, and Ruby apps migrate directly; fly launch detects your runtime and generates a Dockerfile from your Procfile. If you’re still deciding which platform to move to, Railway vs Render covers two alternatives that skip Dockerfiles entirely.
What we tested
Fly.io CLI (flyctl v0.4.x), Heroku CLI v9.x. A Node.js app with Postgres, Redis, and file uploads. Commands verified against official Fly.io docs as of 2026-05-30.
Step 1: Install flyctl and log in
# macOS
brew install flyctl
# Linux
curl -L https://fly.io/install.sh | sh
fly auth login
Failure mode: on Linux without curl, use wget -qO- https://fly.io/install.sh | sh.
Step 2: Run fly launch in your app directory
cd your-app
fly launch
flyctl reads your Procfile, detects your runtime, and generates a Dockerfile and fly.toml. The Dockerfile is generated fresh even if you had none before — this is expected.
Failure mode: if flyctl can’t detect your runtime, it asks which builder to use. Pick the one matching your Heroku buildpack (Node.js, Python, Ruby, etc.).
Step 3: Create a Postgres database
Self-managed Postgres runs at roughly $2/mo for the smallest shared instance and is the right choice for most small apps. Fly Managed Postgres (fly mpg) starts at $38/mo — skip it unless you need automated failover and support.
fly postgres create --name myapp-db
Note the connection string it prints — you’ll need it in Step 5.
Failure mode: if a region selection prompt appears, pick the same region you chose in Step 2. Cross-region database connections add 40–200ms of latency.
Step 4: Attach the database to your app
fly postgres attach myapp-db --app myapp
This sets DATABASE_URL in your app’s secrets automatically. You do not need to copy the connection string anywhere.
Step 5: Import your Heroku Postgres data
fly postgres import handles the dump and restore in one command — the fastest path.
# Get your Heroku connection string
heroku pg:credentials:url -a your-heroku-app DATABASE
# Import into Fly Postgres
fly postgres import <heroku-connection-string> --app myapp-db
The command blocks until done and prints progress. For databases over 1 GB, add --quiet and let it run.
Failure mode: if the import fails with a permission error, your Heroku database user may be missing the pg_read_all_data privilege. Fix it first:
heroku pg:psql -a your-heroku-app -c "GRANT pg_read_all_data TO <heroku-user>"
Manual fallback (if fly postgres import is unavailable):
pg_dump -Fc --no-acl --no-owner \
-h <heroku-host> -U <heroku-user> <heroku-db> > heroku.dump
fly postgres connect -a myapp-db
# inside psql:
pg_restore --verbose --clean --no-acl --no-owner \
-h localhost -d myapp heroku.dump
Step 6: Migrate environment variables
Skip DATABASE_URL and REDIS_URL — you’ve already handled Postgres, and Redis comes next. Import everything else in one pipe:
heroku config -s -a your-heroku-app \
| grep -v DATABASE_URL \
| grep -v REDIS_URL \
| fly secrets import
Failure mode: heroku config -s quotes values differently on some shells. If you see import errors, set variables individually:
fly secrets set MY_SECRET="value" ANOTHER_KEY="value"
Step 7: Set up Redis (if your app uses it)
Fly Redis is backed by Upstash. Create it:
fly redis create --name myapp-redis
Important: Fly Redis cannot import existing Redis data directly. For most apps, queued jobs and session data are safe to drop — start fresh and let the queue refill. If you need to restore specific keys, export them from Heroku Redis with redis-cli --rdb and restore using redis-cli --pipe against the Fly Redis URL.
Step 8: Set up file storage (if your app writes files)
Fly has an ephemeral filesystem. Files written to disk on one machine do not survive restarts and do not appear on other machines. If your app handles uploads or generates files, set up object storage before deploying:
fly storage create --name myapp-storage
This provisions a Tigris bucket. Add the BUCKET_NAME and access keys it outputs to your secrets, then update your app to use the S3-compatible API. Cloudflare R2 is an alternative with no egress fees.
Failure mode: skipping this step causes files to appear during a machine’s session and vanish on the next deploy. Heroku has the same ephemeral filesystem constraint, but some apps worked around it with attached storage layers that won’t port automatically.
Step 9: Handle scheduled tasks (if your app uses the Scheduler dyno)
Heroku’s Scheduler dyno has no direct Fly equivalent. Two options:
Option A — clock process in fly.toml:
[processes]
app = "node server.js"
clock = "node scripts/daily-job.js"
[[services]]
processes = ["app"]
Option B — fly machine run with a schedule:
fly machine run --app myapp \
--schedule daily \
--entrypoint "node scripts/daily-job.js" \
.
Step 10: Deploy
fly deploy
flyctl builds the Docker image, pushes it, starts machines, and runs health checks. Success looks like: v1 deployed successfully.
Failure mode: health check failures appear in the deploy output. Check fly logs for the startup error. The most common cause is the app binding to a hard-coded port instead of process.env.PORT. Fly expects your app at 0.0.0.0:8080 by default (configurable in fly.toml).
Step 11: Prevent cold starts (optional)
By default, Fly scales to zero when no requests arrive for a few minutes. Cold starts can be several seconds for JVM or .NET runtimes. To keep at least one machine running:
# fly.toml
[http_service]
min_machines_running = 1
At shared-cpu-1x, one always-on machine adds ~$1.94/mo.
Step 12: Clean up Heroku
After confirming traffic routes correctly to Fly:
heroku apps:destroy your-heroku-app --confirm your-heroku-app
Important: deleting your Fly app later does NOT delete attached Postgres, Redis, or Tigris resources. Remove them separately or they continue billing:
fly postgres destroy myapp-db
fly redis destroy myapp-redis
fly storage destroy myapp-storage
Cost comparison
| Setup | Monthly cost |
|---|---|
| Heroku Eco dyno + Essential-0 Postgres | ~$10 minimum |
| Fly shared-cpu-1x + self-managed Postgres | ~$4 |
| Fly shared-cpu-1x + Managed Postgres Basic | ~$40 |
Fly Managed Postgres at $38/mo costs four times what Heroku costs for a small app. Self-managed is the right migration target.
Verdict
Fly.io cuts a standard Heroku bill by roughly 60% for small apps. The CLI handles runtime detection, Dockerfile generation, and data import without you writing config from scratch. Use self-managed Postgres, skip Managed. Add Tigris if you have file uploads.
If you want to compare platforms before committing, Railway vs Render covers two alternatives that skip Dockerfiles and have simpler billing models. If you’ve settled on Fly but want a full cost and DX breakdown against its closest competitor, Fly.io vs Railway goes deep on billing, reliability, and team fit.