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 export
s 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:
FROM
: This describes the image we’ll be basing our container off of; in this case, we’re using the Ruby 2.2 image.RUN
: This is how we run commands; in this example,RUN
is used primarily to install various pieces of software with Apt.WORKDIR
: This defines the base directory from which all our commands are executed.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
Dockerfile
s (and Apt!) quickly.
What apps have you run in Docker recently?
Additional Resources
- docker-compose.yml documentation: https://docs.docker.com/compose/compose-file/
- Dockerfile reference: https://docs.docker.com/engine/reference/builder/
- Fine-tuning your
Dockerfile
: http://jonathan.bergknoff.com/journal/building-good-docker-images