Docker Ruby Images

In this article I will explore Docker more hands-on and describe various features of Docker using a few Ruby images as examples. You can read my previous article for an intro to Docker and virtualization.

Docker images are read only. When you pull a docker image and run it, the
AuFS creates another layer that is read/write and all the changes to the container running off that image are saved to that layer.

There are two ways of building a Docker image: one is by running a base image from the docker index and dropping into a bash shell (docker run -i -t ubuntu /bin/bash) and the other is through a Dockerfile - this is meant to be a very portable and reproducible way to build images. As a result, attaching a volume from the host to the docker container is not possible through a Dockerfile because it reduces portability.

Ruby Docker images

I decided to learn Docker by building a few Ruby images. I'm a big fan of rbenv and I use it both in production and development. Based on what I read about Docker I think the best base image for ruby would be the latest ubuntu distro with rbenv and all the necessary packages installed. Once I have this based image I can spin off other images with Dockerfile and install the ruby version I need. Let's walk through these steps.

Creating the base Rbenv image

Let's first build the image using a Dockerfile.

A Dockerfile is a text file that contains instructions on how to build the image. This is a great way to document all the steps required to build an image: from the OS version to all the packages, environment variables and so on. You can read the full list of commands you can add to a Dockerfile.

Let's create a Dockerfile and start adding a few commands (you can see the full Dockerfile in this gist). Note: this Dockerfile is heavily based on deepack's Dockerfile

 # Select Ubuntu as the base image 
 FROM ubuntu 

 MAINTAINER Razvan Draghici <tzumby@gmail.com> 

 # Update Apt 
 RUN apt-get -y update 
 RUN apt-get -y install curl git-core python- software-properties

Here I selected ubuntu as the OS. Note that I didn't specify a version so Docker will use the latest release. The rest is pretty self explanatory: installing the required apt packages to compile Ruby and finally getting rbenv setup will all the required ENV variables.

To build the image run docker build .

 docker build .

This will start downloading all the required images for building your image. You will notice how Docker downloads a lot of images: these are all various layers that comprise the full ubuntu image. After the images are downloaded Docker will start running your Dockerfile command one by one.

When the build is done you'll see the success message: Successfully built 64011d370303 (you will get a different image id). You can list all the images currently downloaded in your system by running docker images

Your newly created image should be listed at the top. Here's mine - it has the same image ID that I saw at the end of the build 64011d370303. Notice how the REPOSITORY and TAG are empty. This is because we didn't specify a repository and a tag when creating the build. Let's do this now (make sure you are using your own image ID):

 docker tag 64011d370303 tzumby/rbenv:1.0

Now running docker images should list that image with the repository and tag filled out. You can ommit the 1.0 tag from the command and Docker will simply call that latest.

Let's run that image and check that rbenv was installed correctly. We are going to use the docker run. The -t allocates a pseudo terminal, the -i runs the image in interactive mode (this will keep STDIN open even if the ptty is not attached and will allow you to detach the tty using ctrl-p or ctrl-q). The last argument is the command to run, in our case /bin/bash:

 docker run -t -i tzumby/rbenv /bin/bash

Once in you can run the rbenv command to check if it's installed:

 root@2db8759f4bc5:/$ rbenv rbenv 0.4.0-98-g13a474c 
 Usage: rbenv <command></command> []

In order to return to your host you can either exit the ptty or detach it, let's try detaching by pressing ctrl-q. To see the currently running containers you can run the docker ps command. Note: if you add -a as an argument you will get a list of all containers (even the ones not currently running).

 ~/ops/docker_ruby $ docker ps 
 CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS  NAMES 2db8759f4bc5 tzumby/rbenv:1.0 "/bin/bash" 3 minutes ago Up 3 minutes drunk_stallman

You can re-attach the ptty on that container by running docker attach

 docker attach 2db8759f4bc5

If you now type exit the container will stop running and you will not see it in the list of containers after running docker ps.

Docker Repositories

One of my favourite features of Docker is the Docker index. This is a repository for your images, you can tag versions and store public or private images. You can use the hosted Docker.io or host your own registry.

I will used Docker.io. Login into docker: docker login and provide your credentials (you can register an account at docker.io). Now push your image to your repository:

 docker push tzumby/rbenv

Notice how Docker is uploading all the AuFS layers to their servers. Some of the layers are already present and uploaded so they are skipped. This is a great feature and greatly speeds up both pushing and downloading new images.

Let's suppose I forgot to install a package on my image. Since Docker images are immutable I cannot modify the image I pushed to the repo. What I can do is update my Dockerfile and build another image then push that image to replace the existing rbenv image. I added a new line to my Dockerfile to install rcconf (a simple text gui tool to list the rc.d services running at startup) and saved the file.

 ... 
 RUN apt-get install rcconf 
 ...

Now I can build the image again, this time let's add the -t parameter to specify a repository name for the resulting image. I'll add a 1.1 tag to denote that this is a new version:

 docker build -t tzumby/rbenv:1.1 .

Run docker images to see the image we just created. To push this to the docker.io repo simply re-run the push command:

 docker push tzumby/rbenv

Most of the images should be already pushed and skipped, except the image layer container our newly installed rcconf package. If you check your Docker.io repositories you should see the newly pushed image along with all the tags.

Installing different Ruby versions

Now that I showed you how to create an image using a Dockerfile we're going to go over the second method for creating images: directly running an image and installing the packages manually.

We have a base rbenv image and we can use this as a starting point to build images with different Ruby versions installed. I chose this just to exemplify, you can easily just have a base rbenv image, install a few ruby versions and set them as global using rbenv global as per your requirements.

Let's run the tzumby/rbenv image (Note that if we don't have to specify the 1.1 tag as Docker will automatically run our latest image):

 docker run -t -i tzumby/rbenv:1.1 /bin/bash

Now that we have a bash shell we can install any ruby version we want using rbenv install:

 rbenv install 2.1.0

The install will take a while to download and compile ruby so you can safely detach the ptty (ctrl-q) and then re-attach later to check that everything ran smoothly. You can use the base rbenv image to build any ruby versions you need.

Conclusion

This concludes my Docker article. In the future I will follow up with a write-up about how this could be incorporated to deploy full fledged Rails apps. If you are interested in that topic you can check out a few resources and projects. Projects like Dokku or Deis and Panamax.io leverage Docker to build a Heroku-like Paas. Deis is of particular interest to me as it uses CoreOS. I will write more about that in the future.