Migrating Heroku Database Apps to Fly

How do you handle databases when you migrate an application from Heroku to Fly? Migrating Heroku apps to Fly gives your app a real boost in performance using Turboku, but how do you move the database connection of an app over?

I was recently asked how the database migration in the original Turboku demo worked. For that original demo, the migration was practically automatic. We already knew that you could access a Heroku database from outside Heroku. Since then, Changes in how Heroku and the Postgres driver handle things have meant you need to be a little more proactive with your database connecting code. So, if we're setting out to make an app that migrates without changes to Fly, what do we need to do?

Start With the DATABASE_URL

When you attach a Postgres addon to your Heroku app, it also creates a URL which contains everything you need to connect to the database. You can get the value of DATABASE_URL from the environment and that has all the credentials you need to connect to the Postgres database. It also contains the host, port and database name to connect to. For most situations, it can be used without modification. The demo uses Massivejs here as a layer on top of the node-postures (aka pg) package and connecting looks something like this:

const massive = require('massive');

var connectionString = process.env.DATABASE_URL;


(async () => {
    try {
        const db = await massive(connectionString);
     } catch (e) {
        console.log("No DB",e)
    }
})();

Now, if we wanted to connect to Heroku Postgres from outside Heroku, the number one requirement is that you switch to SSL. Now, you could do that by appending a string with the required change to the end of the connection string. But experience tells me that connection strings can change and change in sometimes unpredictable ways. So...

Turning on SSL

The first thing we need to do is to parse the connection string. To handle that, we'll use the pg-connection-string package and we'll extract the parse function:

var parse = require('pg-connection-string').parse;

This is the same package the pg library uses to parse connection strings so you shouldn't have any compatibility issues with it. We can now take the connection string and parse it:

var connectionString = process.env.DATABASE_URL

connector=parse(connectionString);

And now we can set the ssl field:

connector.ssl={ "sslmode": "require", "rejectUnauthorized":false };

Ah! You may say, all you need to do is set ssl to true. And in a perfect world, that would be correct. Firstly, any value in the ssl setting equates to ssl being set to true. Secondly, we need to tweak the SSL configuration because Heroku Postgres has servers with a self-signed certificate in their chain of server certificates. If we just had SSL turned on, the client driver would try and verify the server's identity and throw an error when it saw that self-signed certificate. You can read more about this in this github issue.

The fix, now, is to set the sslmode to require which requires SSL to be enabled and then rejectUnauthorized to false to stop that certificate verification check. The best part is we don't need to do anything else to connect; we can use our parsed connector rather than the connection string on most connection calls to Postgres:

const db = await massive(connector);

Putting It Together

Putting all this together, we get this code:

const massive = require('massive');

var connectionString = process.env.DATABASE_URL

if (connectionString==undefined) {
    // Fallback to a local Postgres
    connector=parse("postgres://postgres@localhost/postgres");
    connector.ssl=false;
} else {
    connector=parse(connectionString);
    connector.ssl={ "sslmode": "require", "rejectUnauthorized":false };
}

(async () => {
    try {
        const db = await massive(connector);
        app.set('db', db);
        tables=db.listTables()
    } catch (e) {
        console.log("No DB",e)
    }
})();

There's a little extra code to help out when testing locally against a local Postgres - don't define DATABASE_URL and it'll fall back to a non-SSL localhost connection. In our example code, flydictionary, the rest of the code a simple dictionary with the ability to search or add words and save them in the database.

Pushing to Heroku

If you want to install this app, grab the flydictionary code, log in to Heroku and deploy the app with heroku create then push the app up to Heroku with git push heroku master.

The app will start running but stop immediately as there's no database for it to take to - Use:

heroku addons:create heroku-postgresql

This will create a Postgres database and attach it to your app. Now you can run heroku open to open a browser onto your newly created app. Add some words to the dictionary.

Coming to Fly

Bringing Heroku web apps to Fly is an uncomplicated process thanks to the specialized Heroku migration support.

All you need to do is go to the https://fly.io/heroku. Log in to Heroku there. That will show the available apps on the page and you can select the app you just created on Heroku.

Then you press the Turboku! button and the app will be automatically migrated over to Fly. That includes migrating the Heroku DATABASE_URL environment variable which will be turned into a Fly secret.

Things to remember with a Heroku database connection: Heroku only has two regions: the US and Europe. Database performance will be a function of how close your selected Fly regions are to your Heroku database region. Consider caching within Fly regions if performance is mission-critical.

Fly Closer

Thanks to the connection changes we made to the application, it'll just start running on Fly and we can treat it like other Fly applications, adding and removing regions, scaling up, scaling down and more, while getting the benefit of being closer to the user. And remember that with Turboku, apps migrated this way are automatically updated when changes are pushed to Heroku.

Want to learn more about Fly? Head over to our Fly Docs for lots more, including a Hands On where you can get a free account and deploy your first app today.