This is a mobile version, full one is here.

Yegor Bugayenko
29 July 2014

Every Build in Its Own Docker Container

Docker is a command line tool that can run a shell command in a virtual Linux, inside an isolated file system. Every time we build our projects, we want them to run in their own Docker containers. Take this Maven project for example:

$ sudo docker run -i -t ubuntu mvn clean test

This command will start a new Ubuntu system and execute mvn clean test inside it. Rultor.com, our virtual assistant, does exactly that with our builds, when we deploy, package, test and merge them.

Why Docker?

What benefits does it give us? And why Docker, when there are many other virtualization technologies, like LXC, for example?

Well, there are a few very important benefits:

Let’s discuss them in details.

Image Repository

Docker enables image sharing through its public repository at hub.docker.com. This means that after I prepare a working environment for my application, I make an image out of it and push it to the hub.

Let’s say, I want my Maven build to be executed in a container with a pre-installed graphviz package (in order to enable dot command line tool). First, I would start a plain vanilla Ubuntu container, and install graphviz inside it:

$ sudo docker run -i -t ubuntu /bin/bash
root@215d2696e8ad:/# sudo apt-get install -y graphviz
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following extra packages will be installed:
...
root@215d2696e8ad:/# exit
$ sudo docker ps -a
CONTAINER ID        IMAGE               COMMAND             CREATED              STATUS                     PORTS               NAMES
215d2696e8ad        ubuntu:14.04        /bin/bash           About a minute ago   Exited (0) 3 seconds ago                       high_mccarthy

I have a container that stopped a few seconds ago. Container’s ID is 215d2696e8ad. Now, I want to make it reusable for all further tests in Rultor.com. I have to create an image from it:

$ sudo docker commit 215d2696e8ad yegor256/beta
c5ad7718fc0e20fe4bf2c8a9bfade4db8617a25366ca5b64be2e1e8aa0de6e52

I just made my new commit to a new image yegor256/beta. This image can be reused right now. I can create a new container from this image and it will have graphviz installed inside!

Now it’s time to share my image at Docker hub, in order to make it available for Rultor:

$ sudo docker push yegor256/beta
The push refers to a repository [yegor256/beta] (len: 1)
Sending image list
Pushing repository yegor256/beta (1 tags)
511136ea3c5a: Image already pushed, skipping
d7ac5e4f1812: Image already pushed, skipping
2f4b4d6a4a06: Image already pushed, skipping
83ff768040a0: Image already pushed, skipping
6c37f792ddac: Image already pushed, skipping
e54ca5efa2e9: Image already pushed, skipping
c5ad7718fc0e: Image successfully pushed
Pushing tag for rev [c5ad7718fc0e] on {https://registry-1.docker.io/v1/repositories/yegor256/beta/tags/latest}

The last step is to configure Rultor to use this image in all builds. To do this, I will edit .rultor.yml in the root directory of my GitHub repository:

docker:
  image: yegor256/beta

That’s it. From now on, Rultor will use my custom Docker image with pre-installed graphviz, in every build (merge, release, deploy, etc.)

Moreover, if and when I want to add something else to the image, it’s easy to do. Say, I want to install Ruby into my build image. I start a container from the image and install it (pay attention, I’m starting a container not from ubuntu image, as I did before, but from yegor256/beta):

$ sudo docker run -i -t yegor256/beta /bin/bash
root@7e0fbd9806c9:/# sudo apt-get install -y ruby
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following extra packages will be installed:
...
root@7e0fbd9806c9:/# exit
$ sudo docker ps -a
CONTAINER ID        IMAGE                  COMMAND             CREATED             STATUS                     PORTS               NAMES
7e0fbd9806c9        yegor256/beta:latest   /bin/bash           28 seconds ago      Exited (0) 2 seconds ago                       pensive_pare
215d2696e8ad        ubuntu:14.04           /bin/bash           10 minutes ago      Exited (0) 8 minutes ago                       high_mccarthy

You can now see that I have two containers. The first one is the one I am using right now; it contains Ruby. The second one is the one I was using before and it contains graphviz.

Now I have to commit again and push:

$ sudo docker commit 7e0fbd9806c9 yegor256/beta
6cbfb7a6b18a2182f42171f6bb5aef67c4819b5c2795edffa6a63ba78aaada2d
$ sudo docker push yegor256/beta
...

Thus, this Docker hub is a very convenient feature for Rultor and similar systems.

Versioning

As you saw in the example above, every change to a Docker image has its own version (hash) and it’s possible to track changes. It is also possible to roll back to any particular change.

Rultor is not using this functionality itself, but Rultor users are able to control their build configurations with much better precision.

Application-Centric

Docker, unlike LXC or Vagrant, for example, is application-centric. This means that when we start a container—we start an application. With other virtualization technologies, when you get a virtual machine—you get a fully functional Unix environment, where you can login through SSH and do whatever you want.

Docker makes things simpler. It doesn’t give you SSH access to container, but runs an application inside and shows you its output. This is exactly what we need in Rultor. We need to run an automated build (for example Maven or Bundler), see its output and get its exit code. If the code is not zero, we fail the build and report to the user.

This is how we run Maven build:

$ sudo docker run --rm -i -t yegor256/rultor mvn clean test
[INFO] ------------------------------------------------------------------------
[INFO] Building jcabi-github 0.13
[INFO] ------------------------------------------------------------------------
[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ jcabi-github ---
[INFO]
...

As you can see, Maven starts immediately. We don’t worry about the internals of the container. We just start an application inside it.

Furthermore, thanks to the --rm option, the container gets destroyed immediately after Maven execution is finished.

This is what application-centric is about.

Our overall impression of Docker is highly positive.

ps. A compact version of this article was published at devops.com