· 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

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

SetupMonthly 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.