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.