Automatically Run Migrations When Deploying to Heroku
Have you ever deployed an application to Heroku, only after an exception notification realizing that you forgot to run migrations? This is how we managed to solve this problem once and for all.
The Problem
In our most recent project, we started using Heroku GitHub Integration. The application is deployed automatically after each successful build. After one of our first deploys we realized the main issue with this - the migrations aren’t run during the deploy, leaving our staging environment broken.
When you deploy a Rails application to Heroku, there are several things you want to happen before the new version goes live: the gem dependencies need to be installed with bundle install
, the assets need to be precompiled using rake assets:precompile
, etc.
The default Ruby Buildpack supplied by Heroku does all of that automatically. However, to many an unsuspecting developer’s peril (and ours as well), it does not run rake db:migrate
during the deploy, resulting in application errors after a change to the database schema is deployed but not run.
Other Solutions
There are many different solutions to this problem. Below are some of them, one of which you might already be using:
Naive Solution
One is to never forget to run the migrations after deploying:
$ git push heroku master && heroku run rake db:migrate && heroku restart
While you may never forget to run that sequence of commands, your new teammate might. It also does not play with tools that allow deploying to Heroku automatically (for example after a successful build).
The Deploy Script
Another way would be to put the commands above into a script file (like deploy.sh
) and use that file every time you deploy. This approach is also based on convention, so it’s still prone to unaware people deploying the application in a broken state. Another problem with it is that with that particular sequence of commands, there’s still a window of time in between deploying the code and running the migrations, when the application is broken.
Custom Fork
Yet another approach would involve forking the Ruby Buildpack and adding the code necessary to run the migrations on every deploy. It’s more elegant than the custom deploy script, because it keeps the standard git-push based deploy process known to every Heroku user, but it also introduces a problem: the need to maintain the fork as the original buildpack is updated. The forked buildpack is bound to fall behind the original, which in turn may cause you problems in the future.
Our Solution
The UNIX philosophy states that programs should do one thing and do it well. That’s why we made buildpack-ruby-db-migrate to do just one thing: run the rake db:migrate
task on every deploy.
Because it does just one thing, there’s very little reason for our buildpack to change. However, doing just one thing is not enough when it comes to deploying Rails applications - we still need to install gems, precompile assets and so on. That’s where the excellent heroku-buildpack-multi comes in. As the name suggests, it allows using multiple buildpacks on Heroku.
First you configure your app to use the Multi Buildpack:
$ heroku config:add BUILDPACK_URL=https://github.com/ddollar/heroku-buildpack-multi.git
Then you configure the buildpacks that you wish to use in the .buildpacks
file in the root directory of your project:
# .buildpacks contents:
https://github.com/heroku/heroku-buildpack-ruby.git
https://github.com/gunpowderlabs/buildpack-ruby-db-migrate.git
All you need to do after that is commit the .buildpacks file to your repository, push to Heroku and that’s it. From now on, you’ll never forget to run the migrations on deploy and your application will never exist in a broken, deployed-but-not-migrated state for even a moment.
Caveat emptor
For most applications, the migrate-on-deploy strategy works very well. However, if you have massive amounts of data, migrations can take your application offline for a long time. In such case, you would probably be better off running them while keeping the application online (which is beyond the scope of this article).