posted on

Docker , Rails


Until recently I used Chef to configure and maintain servers which worked well enough but this was time consuming to run, and to recreate servers locally using virtual machines wasn't ideal on larger projects. Recently I've been setting up some projects with Docker as this is gaining more and more traction and looked like a great alternative to Chef. So far my experience of Docker has been a great one so I wanted to share how to get a simple Rails application running with Docker.

Docker enables you to create an image of your server setup and then run this on a host system. The host need know nothing about your application or the executables/libraries you are using it only needs docker installed.

This allows me to run an exact copy of the applications infrastructure in staging and production. Deployment is also simplied as we can now pull and run a prebuilt image rather than waiting for Bundler to run and then assets to compile.

In this example we're not going to focus on deployment but instead on how you can use Docker to simplify your development environment.

Getting Started

Docker only runs natively on Linux so to install on OS X or Windows you need setup a Linux virtual machine or use Boot2Docker. I'm running OS X so I'll walk through installing Boot2Docker on OS X.

Firstly download and install the latest version of Boot2Docker from https://github.com/boot2docker/osx-installer/releases/latest.

After you've installed the .pkg open up a terminal and run:

$ boot2docker init
$ boot2docker start
$ $(boot2docker shellinit)

Now you need to find the Host VM:

$ boot2docker ip

Keep a note of this as you'll need it later when you want to access the container.

A Simple Dockerfile for Rails

I've created a sample Rails application with Dockerfile. You can clone this from

$ git clone http://github.com/invisiblelines/docker-rails-example

Aside from the Dockerfile in the root of the project this is a standard Rails project. The Rails application isn't really of any concern though as we're really interested in the Dockerfile.

A Dockerfile is simply a text file containing all the commands used to build a Docker image. It uses a simple syntax in the format of INSTRUCTION args to build up the layers of your image. Each layer is cached so that if you change your Dockerfile only the lines from your change onwards will be replayed to build the container. This makes rebuilding a container a fairly speedy operation.

There are very few instructions to use when creating a Dockerfile and we will be use most of these in our example Dockerfile below. However you can consult the full Dockerfile reference for more details.

FROM invisiblelines/ruby:2.1.4
MAINTAINER Kieran Johnson <[email protected]>

ENV RAILS_ENV development
ENV PORT 5000

RUN curl -sL https://deb.nodesource.com/setup | sudo bash - && \
    apt-get -qqy install nodejs -y; \
    apt-get clean -y; \
    apt-get autoremove -y

RUN apt-get -qq update;\
  apt-get -qqy install sqlite libsqlite3-dev; \
  apt-get clean -y; \
  apt-get autoremove -y

RUN adduser web --no-create-home --shell /bin/bash --disabled-password --gecos ""

RUN mkdir -p /var/bundle && chown -R web:web /var/bundle
RUN mkdir -p /var/www && chown -R web:web /var/www

ADD Gemfile /var/www/
ADD Gemfile.lock /var/www/

RUN bundle config --global path /var/bundle
RUN su -c "cd /var/www && bundle install --path /var/bundle" -l web

USER web

WORKDIR /var/www

EXPOSE $PORT

CMD bundle exec foreman start

Lets run through this in managable sections.

Firstly I've specified a base image to build upon. This is a an image I prepared that has Ruby 2.1.4, Bundler and some common libraries require by gems pre-installed. In turn this is build on top of a plain Ubuntu 14.04.1 install.

FROM invisiblelines/ruby:2.1.4

I've then specified the maintainer for this image - me.

MAINTAINER Kieran Johnson <[email protected]>

I then set environment variables for RAILS_ENV and PORT. These can be overriden when running the container by using the -e, eg: -e PORT=8080 -e RAILS_ENV=staging.

ENV RAILS_ENV development
ENV PORT 5000

The latest node.js is then installed from nodesource so we can use the asset pipeline.

RUN curl -sL https://deb.nodesource.com/setup | sudo bash - && \
    apt-get -qqy install nodejs -y; \
    apt-get clean -y; \
    apt-get autoremove -y

Next I install sqlite3 and its development package so we can install the sqlite3 gem.

RUN apt-get -qq update;\
  apt-get -qqy install sqlite libsqlite3-dev; \
  apt-get clean -y; \
  apt-get autoremove -y

I then add a user to run the application so its not running as root.

RUN adduser web --no-create-home --shell /bin/bash --disabled-password --gecos ""

I've then create directories for /var/bundle and /var/www and set the owner to the 'web' user.

RUN mkdir -p /var/bundle && chown -R web:web /var/bundle
RUN mkdir -p /var/www && chown -R web:web /var/www

I then add the Gemfile and Gemfile.lock, set the bundle path globally and run bundle install.

ADD Gemfile /var/www/
ADD Gemfile.lock /var/www/

RUN bundle config --global path /var/bundle
RUN su web -c "cd /var/www && bundle install --path /var/bundle"

The user is then set to the web user

USER web

The working working directory for the image is set to /var/www which will be home to the application code.

WORKDIR /var/www

I expose the port I previously set an environment variable for.

EXPOSE $PORT

And finally start the application server using Foreman.

CMD bundle exec foreman start

Building the Image

Now we've been through the Dockerfile, we just need to build an image so it can be run. From the root of the Rails application run:

$ docker build -t myapp --rm .

This command breaks down to the following:

  • docker build is the command to build an image
  • -t myapp gives this image a meaningful name
  • --rm removes the intermediate containers created when building this image
  • . specifies the location of the Dockerfile

Running Rails in the Docker Container

We can now start a container running this image by running:

$ docker run --name myapp -i -t --rm -p 5000:5000 -v /path/to/rails/root:/var/www myapp

You should now see the Puma server starting.

To breakdown this command -

  • docker run is the command to run a container
  • --name myapp gives the container a meaningful name
  • -i keeps STDIN open
  • -t attaches a pseudo-TTY
  • --rm tells docker to delete this container after its stopped running
  • -p 5000:5000 maps port 5000 of the container to the host system port 5000
  • -v /path/to/rails/root:/var/www maps the local directory to /var/www
  • myapp specifies the name of the image we want to run

If we now visit http://<boot2docker_ip>:5000 you will see the example application running.

To kill the container, just Ctrl-C and this will stop the container and remove it. Alternatively you can run docker kill myapp from another terminal.

Running Commands in the Container

Now we have the container running we can begin developing our Rails application. However we'll quickly want to run commands within the context of the application. To do this we need to fire up another tab in the terminal and run:

$ docker run -i -t --rm -v /path/to/app/root:/var/www myapp /bin/bash

You'll see here we can override the command we specified in the Dockerfile and run any command we wish from the root of the application. Here I'm just running Bash but I could run anything else from here (specs, migrations, etc) eg:

$ docker run -i -t --rm -v /path/to/app/root:/var/www myapp bundle exec rake db:migrate

You'll also see that as I'm just running a one off command I haven't named the container and there's no need to map the port (which is already in use)

Wrapping Up

And thats it. We now have a Ruby on Rails application running in a Docker container. I appreciate it looks like a lot of work just to get to this stage but once you understand the basic Docker commands you can quickly create Dockerfiles and manage containers.

The next step is to build a more useful setup with containers for PostgreSQL and Nginx but we'll save that for another time.