Docker deployments to Heroku
By: Jahangir Anwari
Tags: Docker Compose, Rails, Heroku, Docker

Heroku by default deploys your application as a slug which is nothing more than a gzipped version of your application bundled with all its dependencies. Deploying your application is as simple as doing a git push.

Why use Docker image in place of slug?

If default Docker deployments are so straightforward why would one want to replace it with a Docker image. Below are a few reasons:

Slug size limit : The maximum slug size allowed by Heroku is 500MB. So if your application and all its dependencies exceed 500MB you are out of luck. This holds true for many Java application.

: The maximum slug size allowed by Heroku is 500MB. So if your application and all its dependencies exceed 500MB you are out of luck. This holds true for many Java application. Base OS layer and installed packages : By using Docker image we have full control over the underlying operating system and the various packages installed. This way you are not limited only to the packages installed by default in Heroku’s buildpack. You can also create containers for languages and frameworks that don’t have an official buildpack.

: By using Docker image we have full control over the underlying operating system and the various packages installed. This way you are not limited only to the packages installed by default in Heroku’s buildpack. You can also create containers for languages and frameworks that don’t have an official buildpack. Dev/Prod parity: Using Docker guarantees the same container technology in development and production. This reduces the risk of a issues surfacing in production because the underlying containers are built using different tools.

Development with Docker container

Let’s assume we want to containerize a Rails 5 application for deployment to Heroku. Rather than repeat the official documentation please see here for detailed steps on dockerizing a Rails application.

For this article we will use a plain vanilla Rails 5 application. To build a Docker image below is our Dockerfile :

Noteworthy points:

Lines #3 and #19 : Two build arguments that gives us the flexibility of customizing the build for a particular environment.

: Two build arguments that gives us the flexibility of customizing the build for a particular environment. Lines #21-23: Clean up the gems to reduce the size of the Docker image.

Clean up the gems to reduce the size of the Docker image. Line #27: Compile the assets only if (deployed container) environment is production.

Docker Compose

To simplify the containers orchestration required for running a Rails application we will use Docker Compose.

Below is the docker-compose.yml file used to build the various dependent images and wire up the dependencies between the different containers (e.g. web and database containers)

Noteworthy points:

Line #6: Maps the location where the PostgreSQL database is stored on “host” machine. This is required so that the database content persists between container restarts.

Maps the location where the PostgreSQL database is stored on “host” machine. This is required so that the database content persists between container restarts. Line #8: Exposing and making available the db (PostgreSQL) container’s default database port (i.e. 5432) on port 6432 of the host machine. This is helpful if you want to avoid having to login to the db container to execute any SQL queries. You can also use a GUI tool like pgAdmin from host machine to view the database running inside the db container.

Exposing and making available the db (PostgreSQL) container’s default database port (i.e. 5432) on port 6432 of the host machine. This is helpful if you want to avoid having to login to the db container to execute any SQL queries. You can also use a GUI tool like pgAdmin from host machine to view the database running inside the db container. Line #13: Customizing the development image to install additional packages that would be used during development.

Customizing the development image to install additional packages that would be used during development. Line #16: The command to execute after the container boots up. This overrides the CMD line present in Dockerfile.

The command to execute after the container boots up. This overrides the CMD line present in Dockerfile. Line #18: Similar to line #8 we are making the Rails application running inside the (web) container available on the host machine on port 3001.

Similar to line #8 we are making the Rails application running inside the (web) container available on the host machine on port 3001. Line #22: Sets up dependency between the db service and web service. This will ensure that db container/service is up and running before attempting to start the web container/service.

Initialize Rails inside container:

$ docker-compose build

$ docker-compose run --rm web rails db:create

$ docker-compose up

On your host machine your Rails application should now be accessible at http://localhost:3001

RoR 5.2 on local development machine

Deployment to Heroku

To retain the simplicity of Heroku deployment using git push for Docker containers we need to add a new file called heroku.yml

Noteworthy points:

Line #3: Tells Docker how to build the image.

Tells Docker how to build the image. Line #5: Specify additional bundler arguments so that it does not include gems that are required only for development and test.

Specify additional bundler arguments so that it does not include gems that are required only for development and test. Line #8: The command to execute once the web dyno is up.

The command to execute once the web dyno is up. Line #12: The Docker image to use to build the dyno for worker service.

During deployment Heroku will use the information in heroku.yml file to build and configure the dyno.

To facilitate Docker container deployments to Heroku we have to use Heroku CLI. Go ahead and install it.

Once you have Heroku CLI installed on your machine let’s create a Heroku app and add the free PostgreSQL addon.

$ heroku update beta

$ heroku plugins:install @heroku-cli/plugin-manifest

$ heroku apps:create --manifest

$ heroku addons:create heroku-postgresql:hobby-dev

Now that we have a Heroku application we can set the various environment variables that are needed to run a Rails 5.2 application in production.

$ heroku config:set RAILS_MASTER_KEY=`cat config/master.key`

$ heroku config:set RACK_ENV=production RAILS_ENV=production

$ heroku config:set RAILS_SERVE_STATIC_FILES=enabled

$ heroku config:set WEB_CONCURRENCY=2

$ heroku config:set LANG=en_US.UTF-8

Once the runtime environment variables are configured we can then deploy the container to Heroku and initialize the database.

$ git push heroku master

$ heroku run rails --trace db:migrate

After above steps are done, our application is now deployed and being served on Heroku.

$ heroku open

RoR 5.2 on Heroku

The Rails 5.2 application used for this article can be found here.