From Vagrant To Docker Speeding up the development cycle
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$
| 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:
$ git clone https://github.com/influenza/blog-snippets.git
$ cd blog-snippets/VagrantToDocker/Vagrant
$ vagrant up
$ 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.
blog comments powered by Disqus