Rails meets docker

So there is this new tool called Docker, and you may have heard great stories about it. But as a web developer using Ruby, you may wonder what this tool changes for you. Skeptical? Please follow this way!

Docker is a big topic and it would be impossible to give a decent introduction to it in a single blog post. But if curious about Docker, you will get a chance to put your hands on it. Along the way, we introduce the concepts that make Docker so special.

This is a step-by-step introduction to Docker, so it works best if you read from the beginning.

Before we start

You may be already familiar with virtual machines, also known as VMs. This is great technology, and it may help you when installing Docker (more on that later on). Also, it may give you a better understanding of what Docker is not! Docker provides containers, and they are very different from VMs. Keep that in mind, and you’ll be safe!

A container runs under the same kernel as the “host operating system” (ie. the one hosting the containers). But it isolates as many things as possible:

  • filesystem
  • Unix processes
  • users and groups
  • memory and other resources
  • network

A container is aimed to run one Unix process, no more. You can circumvent that limit if you want to, but this is not the way it is supposed to be. This is very different from a VM, as the later behaves like a complete operating system, running many processes, including background services and other daemons.

By the way, if you are familiar with Unix operating systems, here is a good approximation: Docker is a chroot environment on steroids. Docker runs one Unix process in a jail, with its own files, including the dynamic libraries. So this is like chroot, but it gives a better isolation, thanks to Linux cgroups.

Docker leverages the Linux Containers (aka ‘lxc’) and behaves like a smart wrapper around its libraries. Even if it was only a wrapper, it would be a great one, providing a good abstraction and making our lives easier.

But Docker is more than that. It is a complete ecosystem capable of:

  • saving the containers into images (like snapshots)
  • managing your images, like git manages your source code
  • sharing your containers, through the Docker index
  • building new images automatically from a script

So Docker is like a toolbox, and there is no way to cover all its subcommands in a single day!

Installing Docker

The Docker installation process is pretty straightforward: just follow the getting started guide. But there is more than one “install path”, and this may be confusing for the newcomers.

Docker officially supports Ubuntu Precise or later. This means that the Docker team provides a repository where you can fetch the lxc-docker ubuntu package from. This is the best scenario: easy-peasy.

But Docker should be able to run on any Linux system, as long as you have:

In this case, you would have to:

  1. install the Linux Containers
  2. install the Go compiler
  3. download Docker source code
  4. compile Docker using Go

Running Mac OS X or Windows? Then you need a Virtual Machine to run Ubuntu in the inside! Hopefully, Docker gives a detailed procedure for both Mac and Windows. In this case, it helps a lot if you already know about Vagrant, VirtualBox or VMWare.

Before we take off

We assume you have Docker installed on a compatible Linux system, be it your own system or a Linux VM. The Docker daemon should be up and running, responding to the docker command line. Here is a quick check:

$ sudo docker -v
Docker version 0.6.3, build b0a49a3

$ sudo docker ps
ID IMAGE COMMAND CREATED STATUS PORTS

The ps subcommand queries the Docker server, so it will fail if it cannot make the connection.

If you don’t like typing sudo all the time, I suggest you have a look at the Why sudo section of the official documentation. It explains how you can avoid that.

A typical Rails application

Now, let’s install a typical open source Rails application. For the sake of the example, we will install the sample Rails app that comes with Getting Started with Rails. We will stick to the development environment and sqlite to make things easier.

If you’re a Ruby On Rails developer, you probably know the steps to install Rails applications on a Vanilla Linux system: install ruby and some developer library packages, clone the project, run bundle install, etc.

Forget about it.

Today, the install we look like apt-get install: Docker will do everything for you!

Standing on the shoulders of Giants

Docker makes easy to download an image from a central registry called the Docker index. Just run docker pull with the name of the image we’ve cooked for you:

$ docker pull fcat/rails-getting-started

fcat is the name of the repository, and the remaining part is the name of the image. An image path is similar to a project path on GitHub.

Pulling an image takes some time, depending on your bandwidth. Docker containers are lighter than VMs, but still.

Once it’s downloaded, the image is stored in /var/lib/docker. This directory may get pretty big, eventually.

Let’s check that the image is available locally:

$ docker images
fcat/rails-getting-started   latest   66b7d0737659   4 minutes ago  12.29 kB (virtual 789.3 MB)

The image is big, close to 800 MB. Hopefully, Docker has an advanced caching system: if the image was to change a little bit, docker pull would only retrieve the missing part, nothing else.

Run it!

We are now ready to go, let’s run!

$ docker run -d -p 5000:3000 fcat/rails-getting-started
b0f8937e0345

There are many things to say about this single command, but it runs and it only took a couple of seconds to do so. Also, remember that everything is there: the application, the gems, the dynamic libraries, the basic setup, etc. And we don’t even have to wait for bundler to install the gems and compile some C extension!

The container exposes port 3000, and it is forwarded to port 5000 on the host operating system. This means you access your application through port 5000, the public port. Here is a quick check:

$ curl -s localhost:5000 | grep Hello
<h1>Hello, Rails!</h1>

It really works!

About docker run

As shown above, the docker run command creates a new container from the fcat/rails-getting-started image. The image is immutable, like a read-only filesystem. But the container is a more like a living thing: it can be running or stopped, and then resumed later on.

The container will run rails server as soon as it starts. This command has been set when the image has been built. This is the entry point of the container.

By the way, rails server is the only process Docker cares about: the container will run as long as this process is running. Conversely, the container will die when the server dies. One process per container: this is the rule.

The console has been detached using the -d option. It makes more sense for a web application.

The -p 5000:3000 option tells Docker to expose private port 3000 as public port 5000. You can safely remove this option and Docker will allocate a public port for you. This is nice: no risk to get the “Port already in use” error anymore. But then, you have to probe Docker to know the public port it has chosen for you.

docker run is kind enough to give us the id of the new container it has just created. The common practice is to store this id to manage the container later on.

$ CONTAINER_ID=$(docker run -d -p 5000:3000 fcat/rails-getting-started)

Running in a VM?

If Docker is running in a VM, don’t forget to forward port 5000 to the outer system, let’s say on port 9000. This may sound confusing, so here is the story:

  • the container exposes private port 3000 to public port 5000
  • inside the container, the Rails server listens at port 3000
  • inside the VM (where Docker lives) the application is reachable at port 5000
  • on the system hosting the VM (where your browser lives), the application is reachable at port 9000

So don’t forget to setup the port forwarding feature in Vagrant or alike.

Basic container management

So there is now a Docker container running on your system. To manage this container, you have to know its unique id.

$ docker ps
ID             IMAGE                               COMMAND             CREATED             STATUS              PORTS
7e34b609c15d   fcat/rails-getting-started:latest   /bin/sh -c /start   5 seconds ago       Up 4 seconds        5000->3000

So id is 7e34b609c15d and we assume it is stored in the $CONTAINER_ID environment variable.

Then, it is easy to grab the log for this container:

$ docker logs $CONTAINER_ID
[2013-10-30 13:20:27] INFO  WEBrick 1.3.1
[2013-10-30 13:20:27] INFO  ruby 1.9.3 (2011-10-30) [x86_64-linux]
[2013-10-30 13:20:27] INFO  WEBrick::HTTPServer#start: pid=8 port=3000

It’s also possible to attach the container to a terminal:

$ docker attach $CONTAINER_ID

Started GET "/" for 172.17.42.1 at 2013-10-30 13:25:31 +0000
Processing by WelcomeController#index as HTML
  Rendered welcome/index.html.erb within layouts/application (1.2ms)
  Completed 200 OK in 7ms (Views: 6.2ms | ActiveRecord: 0.0ms)

^C

Or you can even copy the log file from the container to the host:

$ docker cp $CONTAINER_ID:/rails/log/development.log .

$ tail -n 1 development.log
Started GET "/assets/welcome.js?body=1" for 172.17.42.1 at 2013-10-30 13:26:45 +0000

To stop the container:

$ docker stop $CONTAINER_ID

But remember one thing: you cannot “suspend” a container like a VM. Stopping the container will stop the Rails server.

You can also:

  • wait for a container to exit with docker wait
  • restart a container with docker start

But there is no way to change the options you gave to docker run. This means the container will always run the same command, expose the same ports, etc. So what if you want to change these options? You need to spawn a new container!

Rollback

A Docker image is like a snapshot: when you create a container form an image, this is like rolling back to a known state.

Database rollback in action:

  1. create a new container from the provided Docker image
  2. connect to the web application with a browser
  3. click on “New Post”
  4. enter the credentials: “dhh” and “secret”
  5. submit a blog post
  6. stop the container
  7. repeat step 1

And you have lost your data!

Remember that the “state” we go back to is the filesystem state, nothing more. It’s fine since we’re using sqlite to store the data.

Updating the application

As a Ruby developer, you may want to tweak this Rails application a little bit. So let’s go inside the container.

So the far, the container has been started with the default command: the Rails server. But now, it would be more convenient to run a console. So let’s run Bash:

$ docker run -t -i 5000:3000 fcat/rails-getting-started /bin/bash

rails@f607da72a9fc:/$ whoami
rails

rails@f607da72a9fc:/$ hostname
f607da72a9fc

We’re in!

run -t -i is the easy trick to start an interactive session.

Let’s change some Rails view:

rails@f607da72a9fc:/$ grep Rails /rails/app/views/welcome/index.html.erb
<h1>Hello, Rails!</h1>

rails@f607da72a9fc:/$ sed -i 's/Hello/Ahoy/' /rails/app/views/welcome/index.html.erb

rails@f607da72a9fc:/$ grep Rails /rails/app/views/welcome/index.html.erb
<h1>Ahoy, Rails!</h1>

Then, we run the server, check the result and exit:

rails@80ce58f020f5:/$ /start
=> Booting WEBrick
=> Rails 4.0.0 application starting in development on http://0.0.0.0:3000
=> Run `rails server -h` for more startup options
=> Ctrl-C to shutdown server
[2013-10-30 13:48:31] INFO  WEBrick 1.3.1
[2013-10-30 13:48:31] INFO  ruby 1.9.3 (2011-10-30) [x86_64-linux]
[2013-10-30 13:48:31] INFO  WEBrick::HTTPServer#start: pid=13 port=3000

Started GET "/" for 172.17.42.1 at 2013-10-30 13:48:44 +0000
  ActiveRecord::SchemaMigration Load (0.1ms)  SELECT "schema_migrations".* FROM "schema_migrations"
  Processing by WelcomeController#index as HTML
    Rendered welcome/index.html.erb within layouts/application (14.1ms)
    Completed 200 OK in 277ms (Views: 250.2ms | ActiveRecord: 0.0ms)

^C

[2013-10-30 13:49:00] INFO  going to shutdown ...
[2013-10-30 13:49:00] INFO  WEBrick::HTTPServer#start done.
Exiting

rails@80ce58f020f5:/$ exit

The container is stopped but it sticks around:

$ docker ps -a -n 1
ID             IMAGE                               COMMAND      CREATED        STATUS   PORTS
80ce58f020f5   fcat/rails-getting-started:latest   /bin/bash    2 minutes ago  Exit 0

Now, I’d like to use this container as a new starting point. Among other things, I want to set the container command back to /start, not /bin/bash. I know how to do that using docker run, but it takes an image as an argument, and I only have a container!

Taking a snapshot

We next take a snapshot to create an image out of a container. In Docker terminology, we say we commit a container.

Taking a snapshot is easy:

  1. grab the id of the container you want to “save”
  2. if appropriate, stop the container to get a clean filesystem
  3. commit the container to a new image, or update an existing one

I suppose you still have the pirate version of the “Getting Started” Rails application (ie. the one showing “Ahoy”):

$ docker commit -author="Fabien Catteau" \
  -run '{"Cmd": ["/start"], "PortSpecs": ["3000"], "User": "rails"}' \
  80ce58f020f5 rails-getting-started-pirate

0cfec31bf45a

There is lot to say about the -run option, but you get the idea.

The commit returns the unique id. Here is the proof:

$ docker images | grep pirate
rails-getting-started-pirate   latest   0cfec31bf45a  2 minutes ago  1.619 MB (virtual 790.9 MB)

You now have a pirate version of this amazing Rails-based blog engine. And it is ready to go!

$ docker run -d -p 5000:3000 rails-getting-started

You can share this image with the rest of the world using docker push!

A starting point

Docker can do many things, and we have only scratched the surface. So far, the take-away would be:

  • containers are not VMs
  • containers are living things
  • images are frozen snapshots
  • one process per container, no more

This is just the beginning, and more articles about Docker are on the way. Stay tuned!