Introduction to Docker with Rails

Docker is a lightweight framework for containerizing applications. Employing the Linux cgroups, Docker allows containers to be run in a virtual environment that is completely isolated from the host system and other containers, without the overhead of hardware virtualization. It’s a lot like chroot applied not only to filesystems, but also the process and memory space.

The Docker community is full of wide-eyed idealists who see the world as a place of default configurations and simple use cases using only the latest things. The Docker examples include Node.js, CouchDB, and Riak, among others. No mention of, say, Rails, despite it being one of the most popular frameworks over the past 5 years. I’ll try to fill that void.

What this tutorial covers

This blog post is an introduction to Docker for folks who use Rails. You don’t have to know anything about Docker, but you do need to know how to get a Rails app running. I’ll walk you through setting up Docker for development on a Mac, creating a Dockerfile, and running that Dockerfile on your development Docker host.

If you don’t have a Rails application, go make one. There are numerous tutorials on creating a simple Rails app. This tutorial assumes you have an existing Rails app that you want to deploy with Docker.

Setting up Docker

Setting up a development environment for Docker is pretty easy using their provided instructions—the Provisioning instructions are what you want. Some things I mention below are using those instructions on a Mac; namely, I’m using boot2docker, which automates the management of a virtual machine that acts as your Docker server.

One note: boot2docker creates a VirtualBox VM that uses NAT, but it does not map any ports to be accessible from the host machine. After issuing boot2docker init, you should run:

VBoxManage controlvm boot2docker-vm natpf1 "rails,tcp,127.0.0.1,3000,,3000"

Which will map port 3000 on your local machine to port 3000 on the Docker VM. Later, we will have our Docker container listen on port 3000 and without this mapping it will be inaccessible.

Creating a Docker file

The only thing you need to Dockerize your app is a Dockerfile. This defines how docker build will assemble the container for your app and how docker run will execute it. The beginning of the Docker file defines what base image to use:

FROM ubuntu

Like a lot of Rails developers, we use a Ruby runtime manager to ensure consistency across different developer systems. In our case, we use RVM, but a similar approach could be used for rbenv. The first thing we need to do is install RVM in our container and install the Ruby version we use:

RUN curl -sSL https://get.rvm.io | bash -s stable
RUN /bin/bash -c -l 'rvm requirements'
RUN /bin/bash -c -l 'rvm install 1.9.3-p448'
RUN /bin/bash -c -l 'rvm use 1.9.3-p448'
RUN /bin/bash -c -l 'gem install bundler --no-ri --no-rdoc'
RUN /bin/bash -c -l 'bundle config path "$HOME/bundler"'

The first command is the familiar one-line install for RVM. With it installed, we run the subsequent commands using the rvm-shell so that they have the rvm environment. After installing the Ruby that our app prefers, we install Bundler.

The last bit of setup is to pull the Rails application code into the container and install the gems that it needs:

ADD . pl-site
WORKDIR pl-site
RUN /bin/bash -c -l 'bundle install'

The final bit of our Dockerfile tells Docker what port to expose and how to run the Rails server:

EXPOSE 80
CMD /bin/bash -c -l 'ruby script/rails server -p 80'

The entirety of the Dockerfile looks like this:

FROM ubuntu

RUN apt-get update -q
RUN apt-get install -qy curl

RUN curl -sSL https://get.rvm.io | bash -s stable
RUN /bin/bash -c -l 'rvm requirements'
RUN /bin/bash -c -l 'rvm install 1.9.3-p448'
RUN /bin/bash -c -l 'rvm use 1.9.3-p448'
RUN /bin/bash -c -l 'gem install bundler --no-ri --no-rdoc'

RUN /bin/bash -c -l 'bundle config path "$HOME/bundler"'

ADD . pl-site
WORKDIR pl-site
RUN /bin/bash -c -l 'bundle install'

EXPOSE 80
CMD /bin/bash -c -l 'ruby script/rails server -p 80'

Building the container

With the Dockerfile written, the container can be built with docker build:

docker build -t container_name .

This command will take a while to run and produce a lot of output as Docker assembles your container: downloading the base image, installing RVM & gems, and putting your code in place.

Running the container

With the container built, you can run it on your development docker instance:

docker run -p 3000:80 container_name

This runs the container, mapping port 3000 on your local machine to 80 on the container, where the Rails server is listening.

Stopping the container

The docker run command executes in the foreground but ignores SIGINT (i.e. ctrl+c) by default. You can stop & remove the container with this one liner:

docker rm -f `docker ps -lq`

Resetting the Docker host

Docker aggressively caches throughout the build process—each step of your Dockerfile is saved so that subsequent runs of docker build can skip parts that have completed successfully. Unfortunately the caching, like that in so many projects, can cause problems when you’re developing. Sometimes it can be fixed by clearing the cache of Docker steps with docker rm `docker ps --no-trunc -a -q`. More often than not, though, I’ve had to go a step further and blow away the Docker host altogether. Since it is a VM controlled by boot2docker, removing & recreating it is easy. I’ve settled on this one liner that I run whenever I want to really rebuild the container:

boot2docker stop && boot2docker delete && boot2docker init && \
VBoxManage controlvm boot2docker-vm natpf1 "rails,tcp,127.0.0.1,3000,,3000" \
&& boot2docker start

Code

The example code used here is on GitHub. Note that there is an ultra-simple branch, echo, that makes an echo server in Docker.

Posted by Drew Stephens @dinomite ·

Drew’s expertise lies in high-traffic web services, big data, and building hyper-efficient software teams. At Clearspring, Drew created and supported APIs that handled more than 6 million daily requests, monitored detailed metrics from processing clusters with hundreds of nodes, and delivered thousands of events per second to users in real-time. Drew is skilled in systems administration and takes automation seriously, applying the Unix adage "if you do it twice, you're doing it wrong" ruthlessly. As a certified Scrum Master, Drew worked alongside Ryan to build a kaizen-based development organization at Genius.com capable of delivering high-quality products as-promised and on-time. Drew spends his time away from computers lifting heavy things and racing cars that are well past their prime.

About Palomino Labs

Palomino Labs unlocks the potential of software to change people and industries. Our team of experienced software developers, designers, and product strategists can help turn any idea into reality.

See the Palomino Labs website for more information, or send us an email and let's start talking about how we can work together.