Migrate from Heroku

This guide runs you through how to migrate a basic Elixir application off of Heroku and onto Fly.io. It assumes you’re running the following services on Heroku:

  • Postgres database
  • Custom domain
  • Background worker, like Oban

If your application is running with more services, additional work may be needed to migrate your application off Heroku.

Migrating your app

The steps below run you through the process of migrating your Phoenix app from Heroku to Fly.

Provision and deploy Phoenix app to Fly

From the root of the Elixir app you’re running on Heroku, run fly launch and select the options to provision a new Postgres database.

When you run fly launch from the newly-created project directory, the launcher provides some defaults for your new app, and gives you the option to tweak the settings.

Run:

cd my_app_name
fly launch

You’ll get a summary of the defaults chosen for your app:

Organization: MyOrgName              (fly launch defaults to the personal org)
Name:         my_app_name            (derived from your directory name)
Region:       Secaucus, NJ (US)      (this is the fastest region for you)
App Machines: shared-cpu-1x, 1GB RAM (most apps need about 1GB of RAM)
Postgres:     <none>                 (not requested)
Redis:        <none>                 (not requested)

? Do you want to tweak these settings before proceeding? Yes
Opening https://fly.io/cli/launch/bea626e2d179a083a3ba622a367e24ec ...

Type y at the prompt to open the Fly Launch page, and make the following changes to your app config:

  • Change the default app name and region, if needed.
  • For Databases, select Fly Postgres, give the Postgres database app a name (for example, your app name with -db appended) and choose a configuration.

Once you confirm your settings, you can return to the terminal, where the launcher will:

  • Run the Phoenix deployment setup task
  • Build the image
  • Set secrets required by Phoenix (SECRET_KEY_BASE, for example)
  • Deploy the application in your selected region
Waiting for launch data... Done
Created app 'my_app_name' in organization 'personal'
Admin URL: https://fly.io/apps/my_app_name
Hostname: my_app_name.fly.dev
Set secrets on my_app_name: SECRET_KEY_BASE
Creating postgres cluster in organization personal
Creating app...
Setting secrets on app my_app_name-db...
Provisioning 1 of 1 machines with image flyio/postgres-flex:15.3@sha256:44b698752cf113110f2fa72443d7fe452b48228aafbb0d93045ef1e3282360a6
Waiting for machine to start...
Machine 2865550c7e96d8 is created
==> Monitoring health checks
  Waiting for 2865550c7e96d8 to become healthy (started, 3/3)

Postgres cluster my_app_name-db created
  Username:    postgres
  Password:    EChe3BrhCjsPQEI
  Hostname:    my_app_name-db.internal
  Flycast:     fdaa:2:45b:0:1::1d
  Proxy port:  5432
  Postgres port:  5433
  Connection string: postgres://postgres:EChe3BrhCjsPQEI@my_app_name-db.flycast:5432

Save your credentials in a secure place -- you won't be able to see them again!

Connect to postgres
Any app within the MyOrgName organization can connect to this Postgres using the above connection string

Now that you've set up Postgres, here's what you need to understand: https://fly.io/docs/postgres/getting-started/what-you-should-know/
Checking for existing attachments
Registering attachment
Creating database
Creating user

Postgres cluster my_app_name-db is now attached to my_app_name
The following secret was added to my_app_name:
  DATABASE_URL=postgres://aa_hello_elixir2:Er6pLzUBuhKcbBl@my_app_name-db.flycast:5432/aa_hello_elixir2?sslmode=disable
Postgres cluster my_app_name-db is now attached to my_app_name
Generating rel/env.sh.eex for distributed Elixir support
Preparing system for Elixir builds
Installing application dependencies
Running Docker release generator
Wrote config file fly.toml
Validating /Users/anderson/test-elixir-gs/hello_elixir2/fly.toml
✓ Configuration is valid
==> Building image
Remote builder fly-builder-black-pine-7645 ready
Remote builder fly-builder-black-pine-7645 ready
==> Building image with Docker
--> docker host: 20.10.12 linux x86_64

...

--> Pushing image done
image: registry.fly.io/my_app_name:deployment-01HPMGHTG8XSYH3ZCV82SF5CEZ
image size: 126 MB

Watch your deployment at https://fly.io/apps/my_app_name/monitoring

Provisioning ips for my_app_name
  Dedicated ipv6: 2a09:8280:1::2a:bc0b:0
  Shared ipv4: 66.241.124.79
  Add a dedicated ipv4 with: fly ips allocate-v4

Running my_app_name release_command: /app/bin/migrate

-------
 ✔ release_command 784eee4c294298 completed successfully
-------
This deployment will:
 * create 2 "app" machines

No machines in group app, launching a new machine
Creating a second machine to increase service availability
Finished launching new machines
-------
NOTE: The machines for [app] have services with 'auto_stop_machines = true' that will be stopped when idling

-------
Checking DNS configuration for my_app_name.fly.dev

Visit your newly deployed app at https://my_app_name.fly.dev/

Make sure to note your Postgres credentials from the output.

That’s it! Run fly apps open to see your deployed app in action.

Try a few other commands:

  • fly logs - Tail your application logs
  • fly status - App deployment details
  • fly status -a postgres-database-app-name - Database deployment details
  • fly deploy - Deploy the application after making changes

There’s still work to be done to move more Heroku stuff over, so don’t worry if the app doesn’t boot right away. Also check out:

Transfer Heroku secrets

To see all of your Heroku env vars and secrets, run:

heroku config -s | grep -v -e "DATABASE_URL" | fly secrets import

This command exports the Heroku secrets, excludes DATABASE_URL and imports them into Fly.

Verify your Heroku secrets are in Fly.

fly secrets list
NAME                          DIGEST                            CREATED AT
DATABASE_URL                  24e455edbfcf1247a642cdae30e14872  14m29s ago
LANG                          95a7bb7a8d0ee402edde95bb78ef95c7  1m24s ago
MIX_ENV                       fd89784e59c72499525556f80289b2c7  1m26s ago
SECRET_KEY_BASE               5afb43c2ddbba6c02ffa7e2834689692  1m22s ago

Transfer the Database

Any new data created by your Heroku app during this database migration won’t be moved over to Fly.io. Consider taking your Heroku application offline or place in read-only mode if you want to be confident that this migration will move over 100% of your Heroku data to Fly.io.

Set the HEROKU_DATABASE_URL variable in your Fly.io environment.

fly secrets set HEROKU_DATABASE_URL=$(heroku config:get DATABASE_URL)

Alright, lets start the transfer remotely on the Fly.io instance.

fly ssh console

Then from the remote Fly SSH console transfer the database.

pg_dump -Fc --no-acl --no-owner -d $HEROKU_DATABASE_URL | pg_restore --verbose --clean --no-acl --no-owner -d $DATABASE_URL

You may need to upgrade your Heroku database to match the version of the source Fly.io database. Refer to Heroku’s Upgrading the Version of a Heroku Postgres Database for instructions on how to upgrade, then try the command above again.

After the database transfers unset the HEROKU_DATABASE_URL variable.

fly secrets unset HEROKU_DATABASE_URL

Then launch your Heroku app to see if its running.

fly apps open

If you have a Redis server, there’s a good chance you need to set that up.

Custom Domain & SSL Certificates

After you finish deploying your application to Fly.io and have tested it extensively, read through the Custom Domain docs and point your domain at it.

In addition to supporting CNAME DNS records, Fly.io also supports A and AAAA records for those who want to point example.com (without the www.example.com) directly at their App on Fly.io.

Cheat Sheet

Old habits die hard, especially good habits like deploying frequently to production. Below is a quick overview of the differences you’ll notice initially between Fly.io and Heroku.

Commands

flyctl commands are a bit different than Heroku, but you’ll get use to them after a few days.

Task Heroku Fly.io
Deployments git push heroku fly deploy
IEx console heroku console fly ssh console --pty -C "/app/bin/app remote"
Database migration heroku rake db:migrate fly ssh console -C "/app/bin/migrate"
Postgres console heroku psql fly postgres connect -a <name-of-database-app-server>
Tail log files heroku logs fly logs
View configuration heroku config fly ssh console -C "printenv"
View releases heroku releases fly releases
Help heroku help fly help

Check out the flyctl docs for a more extensive inventory of flyctl commands.

Deployments

By default Heroku deployments are kicked off via the git push heroku command. Fly.io works a bit differently by kicking of deployments via fly deploy—git isn’t needed to deploy to Fly.io. The advantage to this approach is your git history will be clean and not full of commits like git push heroku -am "make app work" or git push heroku -m "ok it will really work this time".

To achieve the desired git push behavior, we recommend setting up fly deploy as the final command in your continuous integration pipeline, as outlined for GitHub in the Continuous Deployment with Fly.io and GitHub Actions docs.

Deploy via git

Heroku’s default deployment technique is via git push heroku. Fly.io doesn’t require a git commit, just run fly deploy and the files on your local workstation will be deployed.

Fly.io can be configured to deploy on git commits with the following techniques with a GitHub Action.

Databases

Fly.io and Heroku have different Postgres database offerings. The most important distinction to understand about using Fly.io is that it automates provisioning, maintenance, and snapshot tasks for your Postgres database, but it does not manage it. If you run out of disk space, RAM, or other resources on your Fly Postgres instances, you’ll have to scale those virtual machines from the Fly CLI.

Contrast that with Heroku, which fully manages your database and includes an extensive suite of tools to provision, backup, snapshot, fork, patch, upgrade, and scale up/down your database resources.

The good news for people who want a highly managed Postgres database is they can continue hosting it at Heroku and point their Fly.io instances to it!

Heroku’s managed database

One command is all it takes to point Fly Apps at your Heroku managed database.

fly secrets set DATABASE_URL=$(heroku config:get DATABASE_URL)

This is a great way to get comfortable with Fly.io if you prefer a managed database provider. In the future if you decide you want to migrate your data to Fly, you can do so pretty easily with a few commands.

Fly Postgres

The most important thing you’ll want to be comfortable with using Fly.io’s database offering is backing up and restoring your database.

As your application grows, you’ll probably first scale disk and RAM resources, then scale out with multiple replicas. Common maintenance tasks will include upgrading Postgres as new versions are released with new features and security updates.

See here for a more comprehensive guide for what’s required when running your Postgres databases on Fly.io.

Pricing

Heroku and Fly.io have very different pricing structures. You’ll want to read through the details on Fly.io’s pricing page before launching to production. The sections below serve as a rough comparison between Heroku’s and Fly.io’s plans as of August 2022.

Please do your own comparison of plans before switching from Heroku to Fly.io. The examples below are illustrative estimates between two very different offerings, which focuses on the costs of app & database servers. It does not represent the final costs of each plan. Also, the prices below may not be immediately updated if Fly.io or Heroku change prices.

Free Plans

Heroku will not offer free plans as of November 28, 2022.

Fly.io offers free usage for up to 3 full time VMs with 256MB of RAM, which is enough to run a Elixir app and Postgres database to get a feel for how Fly.io works.

Plans for Small Elixir Apps

Heroku’s Hobby tier is limited to 10,000 rows of data, which gets exceeded pretty quickly requiring the purchase of additional rows of data.

Heroku Resource Specs Price
App Dyno 512MB RAM $7/mo
Database 10,000,000 rows $9/mo
Estimated cost $16/mo

Fly.io pricing is metered for the resources you use. Database is billed by the amount of RAM and disk space used, not by rows. The closest equivalent to the Heroku Hobby tier on Fly.io looks like this:

Fly.io Resource Specs Price
App Server 1GB RAM ~$5.70/mo
Database Server 256MB RAM / 10Gb disk ~$3.44/mo
Estimated cost ~$9.14/mo

Plans for Medium to Elixir Apps

There’s too many variables to compare Fly.io’s and Heroku’s pricing for larger Elixir applications depending on your needs, so you’ll definitely want to do your homework before migrating everything to Fly.io. This comparison focuses narrowly on the costs of app & database resources, and excludes other factors such as bandwidth costs, bundled support, etc.

Heroku Resource Specs Price Quantity Total
App Dyno 2.5GB RAM $250/mo 8 $2,000/mo
Database 61GB RAM / 1TB disk $2,500/mo 1 $2,500/mo
Estimated cost $4,500/mo

Here’s roughly the equivalent resources on Fly:

Fly.io Resource Specs Price Quantity Total
App Server 4GB RAM / 2X CPU ~$62.00/mo 8 ~$496/mo
Database Server 64GB RAM / 500GB disk ~$633/mo 2 ~$1,266/mo
Estimated cost ~$1,762/mo

Again, the comparison isn’t realistic because it focuses only on application and database servers, but it does give you an idea of how the different cost structures scale on each platform. For example, Heroku’s database offering at this level is redundant, whereas Fly.io offers 2 database instances to achieve similar levels of redundancy.