Rails on Docker

Josh Clayton

Note: This post was updated March 2016 to use docker-compose instead of fig and Docker Machine instead of boot2docker directly.

I recently volunteered to investigate deployment solutions for one of our clients. There were a number of aspects that we knew would be tricky:

  • the app needed to run behind the client’s firewall
  • the app required a number of other dependencies, including Ragel, Racc, and Solr
  • the app needed to connect to MS SQL Server for data import
  • the app needed to connect to PostgreSQL

Given these requirements, the team and I determined that Docker would be a good first solution to explore.

Why Docker?

Docker is a tool which allows developers to define containers for applications; this allows for control over the operating system and software running the application. If you’re running against an older version of MySQL, a patched version of Ruby, or other dependencies which make setting up a development environment difficult, Docker may simplify development across a team. Even without complicated dependencies, forced encapsulation within a VM ensures development parity across a team and, if deploying with Docker to staging or production, across environments.

Getting started on OS X

I’ll be outlining how to install all dependencies on OS X with Homebrew. I’ll be using VirtualBox to manage my VM; however, Docker Machine supports a number of other drivers in case you’d prefer to use something else.

Install VirtualBox with Homebrew Cask

Homebrew Cask is a great tool to install applications on OS X. To install VirtualBox:

brew install caskroom/cask/brew-cask
brew cask install virtualbox

Install Docker

With VirtualBox installed, we now install Docker and Docker Machine. OS X and Windows don’t support native virtualization, so Docker Machine will use boot2docker under the hood, which is a minimal shim to get around that.

brew install docker docker-machine

Once Docker Machine completes installation, create a machine named default using the VirtualBox driver:

docker-machine create --driver virtualbox default

Finally, to export the Docker environment variables to your shell, run:

eval "$(docker-machine env default)"

You’ll likely want to add the provided exports to your .zshrc, .bashrc, or other appropriate file so they’re available any time you subsequently open a terminal. To do this automatically, add to one of the aforementioned files:

eval `docker-machine env 2>/dev/null`

Install Docker Compose

Once Docker is installed, next up is Docker Compose. Docker Compose provides a configuration syntax (with YAML) and a CLI to manage multiple containers simultaneously.

You can install Docker Compose with Homebrew:

brew install docker-compose

With Docker Compose installed, we’re ready to set up our Rails application to run in a Docker container.

Developing with Docker, Docker Compose, and Rails

With Docker and Docker Compose running on OS X, we can move forward by preparing a Rails app to run in a Dockerized environment. You can find an example Rails application running in Docker here.

Write a Dockerfile

Let’s start with a simple Dockerfile, which lives in Rails’ root:

FROM ruby:2.2.0

RUN apt-get update -qq && apt-get install -y build-essential

# for postgres
RUN apt-get install -y libpq-dev

# for nokogiri
RUN apt-get install -y libxml2-dev libxslt1-dev

# for capybara-webkit
RUN apt-get install -y libqt4-webkit libqt4-dev xvfb

# for a JS runtime
RUN apt-get install -y nodejs

ENV APP_HOME /myapp
RUN mkdir $APP_HOME
WORKDIR $APP_HOME

ADD Gemfile* $APP_HOME/
RUN bundle install

ADD . $APP_HOME

There’s only a handful of commands that we’re using with Docker, but let’s go through them:

  1. FROM: This describes the image we’ll be basing our container off of; in this case, we’re using the Ruby 2.2 image.
  2. RUN: This is how we run commands; in this example, RUN is used primarily to install various pieces of software with Apt.
  3. WORKDIR: This defines the base directory from which all our commands are executed.
  4. ADD: This copies files from the host machine (in our case, relative to Dockerfile on OS X) to the container.

Each command generates a new cached image, which can be built upon later; files with higher churn, or packages that may change, should be placed further down in the Dockerfile to leverage the cache as much as possible.

To build this Docker image, from Rails root run:

docker build .

This will build an image with the appropriately installed software - but we’re not done.

Configure Docker

Even though we’re able to successfully build a Docker image to house our Rails app, we have other dependencies. In this case, we’ll also need to configure a database like Postgres, and ensure that the database container and the application container can talk to one another.

This is where Docker Compose really shines.

Here’s an example docker-compose.yml file:

version: '2'
services:
  db:
    image: postgres:9.4.1
    ports:
      - "5432:5432"

  web:
    build: .
    command: bin/rails server --port 3000 --binding 0.0.0.0
    ports:
      - "3000:3000"
    links:
      - db
    volumes:
      - .:/myapp

In docker-compose.yml, we’re describing two containers. The first is db, which is based on another image (postgres:9.4.1) and exposes port 5432 on port 5432 to the outside world.

The second is web, which uses the Dockerfile (build: .), spins up a Rails server when docker-compose up is run, exposes port 3000 (the Rails app) on port 3000, links the database container, and bases the directory /myapp (WORKDIR from the Dockerfile) off of the Rails app on the host machine.

With docker-compose.yml complete, run:

docker-compose build

This will build all the necessary containers.

Configure the database

Because we’re connecting to a container running Postgres for our database, we’ll need to update our config/database.yml, leveraging the appropriate host based on the name in the docker-compose.yml.

development: &default
  adapter: postgresql
  database: backbone_data_bootstrap_development
  min_messages: WARNING
  pool: 5
  username: postgres
  host: db

test:
  <<: *default
  database: backbone_data_bootstrap_test

Be sure to use the correct environment variables and host names available to your Rails app.

With these settings in place, create and set up the database:

docker-compose run web rake db:create db:setup

Serve the app

To bring the Rails app up, it’s as simple as docker-compose up. You should be able to access the application via a web browser by visiting the IP address of Docker Machine (you can find this out with docker-machine ip default) on port 3000.

Interact via a Rails console

Running a Rails console is as simple as:

docker-compose run web rails console

The structure of the commands here is docker-compose run {CONTAINER_NAME} {COMMAND}. Note that running commands with docker-compose run are in new containers and not a running container (e.g. if you’re already running docker-compose up).

Run specs

If your tests don’t require a JavaScript driver, you can run tests with:

docker-compose run web rake

If your app does use a JavaScript driver for any tests (I’m using capybara-webkit in the example app), I recommend looking into the headless gem. Its only dependency is xvfb, and requires minimal tweaks to work:

# spec/support/headless.rb

RSpec.configure do |config|
  config.around type: :feature do |example|
    Headless.ly do
      example.run
    end
  end
end

This runs all feature specs within a Headless block.

With Konacha, it was a bit trickier:

# config/initializers/konacha.rb
if defined?(Konacha)
  Konacha.configure do |config|
    require 'capybara/webkit'
    config.driver = :webkit
  end
end

This configures Konacha to use the capybara-webkit driver.

Next, we need to wrap the konacha:run rake task to run within the Headless gem:

# Rakefile
namespace :konacha do
  task :run_with_headless do
    require "headless"

    Headless.ly do
      Rake::Task["konacha:run"].invoke
    end
  end
end

task default: ["konacha:run_with_headless", :spec]

Wrap up

This is an example of just one application running inside Docker; your apps likely have different sets of dependencies, so you may become comfortable with Dockerfiles (and Apt!) quickly.

What apps have you run in Docker recently?

Additional Resources