From Vagrant To Docker

In this post we will cover moving from a simple Vagrant / VirtualBox / Ansible development environment to a one based on Docker.

Background

During SCALE 12x I was lucky enough to attend a talk on Docker given by Jérôme Petazzoni, one of the Docker contributors.

In this talk, he demonstrated the performance benefits and functionality to be had by using this lightweight container project over a traditional virtual machine. I was really impressed with how quickly machines could be provisioned and the performance seen inside the Docker containers. (I was also happy to see that Jérôme is an awesomewm user!)

Now for a little context - ever since I started working on multi-service applications, I’ve been excited about the possibility of developing inside a virtual machine. I wanted to know that the same provisioning code I was writing (using Chef at the time) was also used on the system I was working with. This would help me discover things like missing dependencies locally, before the build broke on the continuous integration server (or worse, during deployment!).

I’m now lucky enough to work in an environment where the developers use Vagrant in their local development. This is a god send for keeping down the complexity of managing dependencies, database migrations, and a host of other issues. The tradeoff, unfortunately, is the painfully terrible performance of these virtual machines. Even when provisioned with a generous amount of resources, the performance was still terrible! This led me to a habit of locally compiling and running unit tests, but using the virtual machine to run the application or perform integration tests. This is not ideal. Here’s an example of the performance difference (athas is my machine, precise64 is the virtual machine):

rdahlgren@athas:/code/project $ time ./grailsw clean
| Application cleaned.

real  0m12.072s
user  0m24.017s
sys 0m0.387s

vagrant@precise64:/opt/project$ time ./grailsw clean
| Application cleaned.

real  0m17.983s
user  0m25.882s
sys 0m8.025s

That’s 49% longer on the virtual machine - JUST TO CLEAN. It’s far worse when doing actual work, such as running the application.

You can see why I was excited when I first saw Docker!

Project Setup

To demonstrate how this migration path goes, I’ve prepared a sample project for you to follow along at home. You’ll find the relevant files here. In this project, we use the Grails Petclinic sample. This demonstrates a machine that uses the JDK, grails, and provides a web server on port 8080.

To follow along, you will need to be sure that you have Vagrant, Docker, Ansible and VirtualBox installed.

The Vagrant Version

Let’s take a look at the vagrant-based project in action. To get the application up and running, follow these steps:

rdahlgren@athas:/code/project $ git clone https://github.com/influenza/blog-snippets.git
rdahlgren@athas:/code/project $ cd blog-snippets/VagrantToDocker/Vagrant
rdahlgren@athas:/code/project $ vagrant up
rdahlgren@athas:/code/project $ vagrant ssh 
vagrant@precise64:~$ cd /opt/petclinic/app
vagrant@precise64:/opt/petclinic/app$ ./grailsw run-app

Note that run-app will likely take a long time - possibly as long as a few minutes. This is the delay that we hope to drastically reduce by switching to Docker. Once the application is running, you’ll find the cute Petclinic available at http://localhost:8080/petclinic. You can make changes locally, watch the grails app automatically reload, and develop as you please. This is functional, but the performance is pretty poor.

Vagrant version, in detail

Let’s take a look at the contents of the Vagrant directory in detail. Here we have the Vagrantfile, which is used to specify the virtual machine:

This is a pretty standard Vagrantfile with some notable changes:

  • We pass port 8080 through to the host machine so we can view the grails app locally
    • config.vm.network "forwarded_port", guest: 8080, host: 8080 # web app
  • We use ansible to configure new machines
    • config.vm.provision "ansible" ...

Note that this provision block specifies the playbook and inventory path. The inventory simple points to our vagrant host, and the playbook file specifies what needs to be done to a new machine.

Here’s the playbook, in its entirety:

The high level goals of this playbook are simple, install OpenJDK 7, install git, clone the petclinic repository, and make sure $JAVA_HOME is set correctly (required for the grails wrapper to work).

These are pretty simple goals - let’s see how we can do this in Docker!

Vagrant Cleanup

Before moving on to the Docker version, make sure that you stop the vagrant VM. It binds to the same port we will be using for our Docker version, so we need to free up that port.

$ vagrant halt

This will save the VM’s state and halt execution.

The Docker Version

Let’s get the docker version running! First, make sure that the docker daemon is running. To do this on Arch (assuming you installed via pacman like a good user):

sudo systemctl start docker

Now let’s create an image from the provided Dockerfile and run it:

$ git clone https://github.com/influenza/blog-snippets.git
$ cd blog-snippets/VagrantToDocker/Docker
$ sudo docker build -t petclinic .
$ sudo docker run -i -p 8080:8080 petclinic /opt/petclinic/app/grailsw
grails> run-app

This command will have the same affect as ./grailsw run-app did when used above.

To exit the image:

grails> exit

Let’s take a look at the Dockerfile that is defining the behavior we see here:

You can see that the Dockerfile is a series of steps needed to take the image from the base image (specified with FROM) to a ready-to-use image. This doesn’t map 1:1 with Ansible’s way of doing things, which means that the conversion process is not linear.

A major piece missing from this setup is the sharing of our application directory. The Docker image has its own filesystem, so local edits will have no affect on the image itself.

Luckily, we can change some things to work around this. Let’s make a local copy of the petclinic application.

$ mkdir ~/tmp/docker_petclinic
$ git clone https://github.com/grails-samples/grails-petclinic.git ~/tmp/docker_petclinic

Now we can run the image and mount a host directory as a container volume:

$ sudo docker run -i -p 8080:8080  -v ~/tmp/docker_petclinic:/opt/petclinic/app petclinic
grails> run-app

You’ll notice when you ‘run-app’ this time, the dependencies will be downloaded again. This is downloading them to the shared volume on the host machine. Once everything stabilizes and the application is running, let’s try making a change locally and seeing if the image will pick it up.

$ vim ~/tmp/docker_petclinic/grails-app/views/clinic/index.gsp

Update the text ‘Display all veterinarians’ to say ‘Display (nearly) all veterinarians’. This is more honest since our petclinic application isn’t tracking all veterinarians in practice. Upon saving the file, reload http://localhost:8080/petclinic and you’ll see your change!

There we have it, a basic functional grails development setup using Docker!

A Final Comparison

To compare apples to apples, let’s see how long it takes to run the petclinic tests on each of our environments.

vagrant@precise64:/opt/petclinic/app$ time ./grailsw test-app
... Test output elided ...

real  1m54.969s
user  1m37.630s
sys 0m35.074s

rdahlgren@athas:/code/project$ time sudo docker run  -v ~/tmp/docker_petclinic:/opt/petclinic/app petclinic '/opt/petclinic/app/grailsw test-app'

real  0m39.725s
user  0m0.010s
sys 0m0.030s

That’s a difference of 75 seconds - over a minute!

Next Steps

In the next post, we will look at a more complex use case requiring a Postgres database. This will show how to set up an environment requiring multiple docker containers.