Nigel Ramsay

Software Engineer from Wellington, New Zealand

Adding Docker to a Rails application

16 March 2017

I will explain why Docker is a good choice for development of Rails applications. Next, I will give a step-by-step walkthrough of adding Docker to a fairly standard Rails application.

This content is based on a presentation that I gave at WellRailed, a Wellington, New Zealand based Ruby on Rails meetup in December 2016.

What is Docker?

Docker is a platform that allows you to run containers. These containers, when compared to traditional VMs, are a much faster and more efficient mechanism for isolating the different parts of your application from each other.

This image from IBM gives a good comparison:

Why even bother?

  1. Applications often have aged dependencies. You may need to run an older version of Ruby, or a specific version of Postgresql and/or Redis.
  2. Matching Operating System. It is common for applications to be developed using Mac OSX and then deployed onto Linux. Docker allows us to develop using a matching operating system, and identify any OS differences, before we deploy.
  3. Version differences. You may need to run multiple applications on your development computer. Application X may use Postgresql 9.6 and Application Y may use Postgresql 9.1. Is there an equivalent to RVM for Postgresql or Redis etc?
  4. The “I don’t want MongoDB” on my laptop problem. Some application dependency choices are often best forgotten. With Docker you don’t need to have infrequently used dependencies continuously running in the background of your laptop.

Other reasons for Docker

  1. Declared dependencies. This will allow you to run a consistent development environment with both your production environment, as well as the other developers on your team.
  2. From Zero to Running in a few minutes. No longer do you need to follow a set of step-by-step configuration instructions.
  3. Less database bloat. Over time, older database instances and similar artifacts will be scattered over your laptop. These consume space, and can be difficult to track down. With Docker volumes, it is very easy to purge unused container artifacts, and keep disk space consumption to a minimum.
  4. Quicker laptop boot times. If you solely use Docker, you no longer need to have a collection of databases, data stores and search engines being started by OSX/Linux/Windows on boot.

Goals for using Docker

A fast start from ZERO in 3 steps:

  1. Checkout fresh project from Git
  2. Edit/check an environment file
  3. Start the app

And when finished development:

  1. Stop the app
  2. Nothing else is running and consuming resources

How to add Docker to a Rails application

Dependencies

First off, we should itemise the dependencies in this application:

  1. Ruby
  2. Postgres
  3. Redis
  4. Sidekiq

Before starting, you should download Docker for your OS. It is free and called Docker Community Edition these days.

Configure the Postgresql Dependency

Docker requires us to define the different dependencies on the application, and this is done in the docker-compose.yml file. This file lives in the root directory of your Rails application.

For our Postgresql dependency, we will define the following

image — this the container definition that is downloaded from https://hub.docker.com/

container — this can be thought of as the VM that runs, and is based on the downloaded image.

volume — this is the persistent storage which Postgresql will store the data files.

Here is the Postgresql section within the new docker-compose.yml file:

version: '3'
services:  
  postgresql:    
    image: postgres:9.4.1    
    ports:      
      - "5432:5432"    
    volumes:      
      - postgresql-data:/var/lib/postgresql/data
volumes:  
  postgresql-data: {}

Try starting Postgresql

With the docker-compose.yml file saved, we can start up the database by issuing this command:

docker-compose up

See if it works by adjusting the database.yml file:

development:
  adapter: postgresql
  encoding: unicode
  pool: 5
  database: adjuster_development
  url: <%= ENV['POSTGRESQL_URL'] || 'postgresql://postgres@localhost:5432' %>
test:
  adapter: postgresql
  encoding: unicode
  pool: 5
  database: adjuster_test
  url: <%= ENV['POSTGRESQL_URL'] || 'postgresql://postgres@localhost:5432' %>

Next, try creating the database:

rake db:create

Next, run a few tests to see it working:

rake

............................................FFFFFFF............................
............

This fails due to the missing Redis container. Let’s fix that

Add to docker-compose.yml file:

redis:
  image: redis:2.8
  ports:
    - "6379:6379"
  volumes:
    - redis-data:/var/lib/redis/datavolumes:
  redis-data: {}

Then restart docker-compose:

docker-compose down; docker-compose up

Update Rails application sidekiq.rb to use the new Redis container:

Sidekiq.configure_server do |config|
  config.redis = {
    url: "redis://#{ENV['REDIS_HOST' || '127.0.0.1']}:#{ENV['REDIS_HOST']}/12"
  }
endSidekiq.configure_client do |config|
  config.redis = {
    url: "redis://#{ENV['REDIS_HOST' || '127.0.0.1']}:#{ENV['REDIS_HOST']}/12"
  }
end

Run the tests a second time:

rake

...............................................................................
..............................

Win!

Run Rails in a container

We’ve been running Rails from the host operating system, in my case Mac OSX. Now we want to try getting Rails running within a new web container, as defined in the docker-compose.yml file.

Make the following additions:

web:
  build: .
  ports:
    - "3000:3000"
  volumes:
    - .:/app
    - bundle-cache:/bundle
  depends_on:
    - redis
    - postgresql
  command: bin/docker_web
volumes:
  bundle-cache: {}

Add a .env file:

POSTGRESQL_URL=postgresql://postgres@postgresql:5432
REDIS_ADDRESS=redis

Add the new .env file to your .gitignore file.

Next, add a Dockerfile that describes the dependencies and installation steps of your Rails app:

FROM ruby:2.2.1

# Set an environment variable to store where the app is installed to 
# inside of the Docker image.
ENV BUNDLE_PATH /bundle
ENV LANG C.UTF-8
ENV INSTALL_PATH /app

RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ trusty-pgdg main" > /etc/apt/sources.list.d/pgdg.list
RUN apt-key adv --keyserver hkp://p80.pool.sks-keyservers.net:80 --recv-keys B97B0AFCAA1A47F044F244A07FCC7D46ACCC4CF8

# Install dependencies:
# - build-essential: To ensure certain gems can be compiled
# - bundler: ensure most recent version is installed
# - nodejs: Compile assets
RUN apt-get update && apt-get install -qq -y build-essential nodejs postgresql-client-9.5 --fix-missing --no-install-recommends
RUN gem install bundler

RUN curl -sL https://deb.nodesource.com/setup_6.x | bash
RUN apt-get install -qq -y nodejs

# This sets the context of where commands will be ran in and is 
# documented on Docker's website extensively.

RUN mkdir -p $INSTALL_PATH
WORKDIR $INSTALL_PATH
ADD . $INSTALL_PATH

Next, add the launch script in bin/docker_web

#! /bin/bash  
export RAILS_ENV=development  
bundle check || bundle installjobs=10

# Initialise development database  
FILE=$INSTALL_PATH/tmp/database_initialised_development.txt  
if [ ! -f "$FILE" ]; then  
  echo "Creating and loading development databases"  
  RAILS_ENV=development bundle exec rake db:setup  
  touch "$FILE"  
fi

# Initialise test database  
FILE=$INSTALL_PATH/tmp/database_initialised_test.txt  
if [ ! -f "$FILE" ]; then  
  echo "Creating and loading test databases"  
  RAILS_ENV=test bundle exec rake db:create db:schema:load  
  touch "$FILE"  
fi

echo "Migrating and refreshing reference data"  
bundle exec rake db:migrate  
RAILS_ENV=test bundle exec rake db:migrate

npm install

rm -f tmp/pids/server.pid  
bundle exec rails s -p 3000 -b 0.0.0.0

Don’t forget to make this executable:

chmod +x bin/docker_web

Time to try out the application:

docker-compose down
docker-compose up
open http://0.0.0.0:3000/

With a little luck, the application should be running.

Adding Sidekiq

Next step is to add the Sidekiq worker. Add the configuration to the docker-compose.yml file:

sidekiq:  
  build: .  
  volumes:  
    - .:/app  
    - bundle-cache:/bundle  
  depends_on:  
    - redis  
    - postgresql  
  command: bin/docker_sidekiq

Add the Sidekiq boot script in bin/docker_sidekiq

#! /bin/bash

export RAILS_ENV=development

bundle check

if (($? > 0)); then  
  echo '***********************************************************'  
  echo 'Await web container to install updated gems, then restart Docker'  
  echo '***********************************************************'  
  exit 1
fi

bundle exec sidekiq

Don’t forget to make this executable:

chmod +x bin/docker_sidekiq

Time to try out the application again:

docker-compose down
docker-compose up

You should see the Sidekiq application also startup and connect to Redis.

Debugging with Docker

Docker can be a little slow on OSX. I believe this is related to how the file system is mounted within the Docker containers.

What you can do, is use Docker to run all of the non-web portions of your application. Then start Rails within the host operating system. We will often run our specs within the host OS too.

Using Pry

If you need to do some debugging within your Rails application inside of the container, you can use the pry-remote gem, and then when you hit a breakpoint:

docker-compose exec web bash  
pry-remote

Running multiple containers in parallel

Occasionally, you may need to run a few different Rails applications at the same time. For example, when debugging an data sharing fault on the AddressFinder system, we might want both the AddressFinder API application and the AddressFinder Portal application running together on the same laptop.

This can pose a challenge, as both apps may be configured to expose their ports on 127.0.0.1 and you will end up with a clash.

On solution is to attach all the ports for App #1 to 127.0.0.1 and the ports for App #2 to 127.0.0.2. To achieve this, you’ll first need to have create the 127.0.0.2 address as a loopback alias. We have written a tiny gem that will automate this — it’s called the loopback_alias gem.

You would then update your docker-compose.yml file to refer to the new IP address:

postgresql:  
  image: postgres:9.4.1  
  ports:    
    - "$HOST_IP:5432:5432"  
  volumes:    
    - postgresql-data:/var/lib/postgresql/data

and add this line to your .env file:

HOST_IP=127.0.0.2

Other useful containers

Mailcatcher

We make good use of the Mailcatcher gem in development, and it is very easy to drop this into your stack. Just add this to your docker-compose.yml.

mailcatcher:  
  image: schickling/mailcatcher  
  ports:  
    - "1080:1080"  
    - "1025:1025"

and the following to your development.rb file:

config.action_mailer.delivery_method = :smtp  
config.action_mailer.smtp_settings = {   
  address: ENV['SMTP_ADDRESS'] || '127.0.0.1'  
  port: ENV['SMTP_PORT'] || 1025   
}

Middleman

We’ve defined Docker configuration for all of our static websites that are generated with the Middleman gem.

Procfile

If you are declaring some background tasks in your Procfile for running in production, then these processes could also be added as Docker containers for running in development. You would do this in a similar manner to how the Sidekiq process is configured (above).

Final words

We’ve found adding Docker to be the biggest productivity gain we’ve had for some time at Abletech. Our team are able to quickly get going on new projects, and we know we’re all developing within a consistent environment.

Team members, of varying experience with infrastructure, have been able to checkout Docker-based applications and become productive without needing someone to help.

This year, we look forward to exploring the possibilities of using Docker in production as tools such as Docker Swam and Kubernetes become more mature.