From Vagrant To Docker Continued

In this post, we will take a look at the process of converting a slightly more complicated use of Vagrant and Ansible over to Docker. In a previous post we covered how to adapt a simple, standalone Grails application. This time, we’ve updated the petclinic sample to use a standalone postgres installation as its data source.

Project Setup

Again, be sure to have Vagrant, Docker, Ansible and VirtualBox installed.

I’ve added a VagrantToDockerPartTwo directory to the blog-snippets repository that includes the files we’ll be discussing here.

Updates to Petclinic

In order to make this example more complex, I’ve updated the petclinic sample app to use postgres rather than an in-memory database for persistence. These changes can be seen here.

The Vagrant Version

First, let’s kick off the Vagrant version and see what’s changed.

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

Again, this will take a long time - especially for the first run. While the dependencies download, go ahead and open another terminal up and log in to postgres:

$ cd blog-snippets/VagrantToDockerPartTwo/Vagrant
$ vagrant ssh
vagrant@precise64:~$ psql -U grails petclinic
Password for user grails: <password is 'super secure'>
psql (9.3.4)
Type "help" for help.

petclinic=#

Once the grails app finishes loading and you see ‘Server running.’, go ahead and browse to http://localhost:8080/petclinic to prove to yourself that it’s working. Nothing new here from last time. Now, back in the postgres session, let’s take a look at the data that has been generated by the petclinic boot strap files:

petclinic=# \d+ pet
                                      Table "public.pet"
   Column   |            Type             | Modifiers | Storage  | Stats target | Description 
------------+-----------------------------+-----------+----------+--------------+-------------
 id         | bigint                      | not null  | plain    |              | 
 version    | bigint                      | not null  | plain    |              | 
 birth_date | timestamp without time zone | not null  | plain    |              | 
 name       | character varying(255)      | not null  | extended |              | 
 owner_id   | bigint                      | not null  | plain    |              | 
 type_id    | bigint                      | not null  | plain    |              | 
Indexes:
    "pet_pkey" PRIMARY KEY, btree (id)
Foreign-key constraints:
    "fk1b11f4e39cfaa" FOREIGN KEY (type_id) REFERENCES pet_type(id)
    "fk1b11f7ef25b8b" FOREIGN KEY (owner_id) REFERENCES person(id)
Referenced by:
    TABLE "visit" CONSTRAINT "fk6b04d4bbc7ac4b" FOREIGN KEY (pet_id) REFERENCES pet(id)
Has OIDs: no

petclinic=# select * from person;
 id | version | first_name | last_name |         class          | address | city | telephone 
----+---------+------------+-----------+------------------------+---------+------+-----------
  4 |       0 | James      | Carter    | org.grails.samples.Vet |         |      | 
  5 |       0 | Helen      | Leary     | org.grails.samples.Vet |         |      | 
  6 |       0 | Linda      | Douglas   | org.grails.samples.Vet |         |      | 
  7 |       0 | Rafael     | Ortega    | org.grails.samples.Vet |         |      | 
  8 |       0 | Henry      | Stevens   | org.grails.samples.Vet |         |      | 
  9 |       0 | Sharon     | Jenkins   | org.grails.samples.Vet |         |      | 
(6 rows)

petclinic=# select * from pet_type;
 id | version |  name   
----+---------+---------
 10 |       0 | dog
 11 |       0 | lizard
 12 |       0 | cat
 13 |       0 | snake
 14 |       0 | bird
 15 |       0 | hamster
(6 rows)

As you can see, we now have several tables in the default schema related to the petclinic app, some with data already present!

Now, let’s look at the Vagrant file used for this project. It’s identical to our last version. The meaningful changes here can be seen in the playbook.yml file:

This playbook is substantially larger than the previous version. In order to install and setup the postgres package, I wanted to use the ‘apt_repository’ directive in Ansible, which required some additional python dependencies. Additionally, the database creation and setup (add a user, allow local access, add the DB) requires a number of additional steps. Overall, the playbook is still straight forward.

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

First, make sure that the docker daemon is running. To do this on Arch:

sudo systemctl start docker

In the spirit of Docker’s design principles, we will be using a process per container. We could stack our database instance inside the same container as the grails application, but that would be breaking the model!

Let’s start the postgres container:

$ cd blog-snippets/VagrantToDockerPartTwo/Docker/Postgres
$ sudo docker build -t postgres .
$ sudo docker run -d --name petclinicData postgres

Next, we start up the petclinic application container and link the two together

$ cd blog-snippets/VagrantToDockerPartTwo/Docker/Petclinic
$ sudo docker build -t petclinic2 . # <-- '2' as this is the fork
$ sudo docker run -t -i -p 8080:8080 \
  --name petclinicApp \
  --link petclinicData:database \
  petclinic2 /opt/petclinic/app/grailsw # <-- remember the suffix '2'
grails> run-app

At this point, once the application finishes loading, you can again visit http://localhost:8080/petclinic to see that the application is running as expected.

The commands that we use are a bit different from the ones used last time. We use the new support for container names and linking so that the petclinic application container can use inter-container communication to access the database, rather than needed to hit the host network. Additionally, using names means that we don’t need to keep track of the container UUIDs as they are emitted! I love UUIDs just as much as the next robot, but they can be a bit tricky to remember. For full details on names and linking, check out the docs.

Here’s the Postgres Dockerfile in full:

This is a pretty simple Dockerfile, mainly owing to the base image we used. The only changes necessary from that base image are the addition of our grails user and the creation of the petclinic database. Note that this image is not suitable for use in production. It is open to the world with weak passwords.

Next, let’s take a look at the petclinic application’s Dockerfile:

The only notable change here is line 13, where we use a bit of a hack to point the petclinic DataSource file to a linked container rather than localhost.

The reason this works, is that since Docker version v0.11, linked containers are mapped to entries in the container’s /etc/hosts file. When we run the petclinic application specifying that the petclinicData container be linked as database, an entry will be added that aliases database to the appopriate ip address. See this for details.

This is, unfortunately, a hack. Doing a search and replace on a file that is under source control can lead to a number of issues. A better way to handle this situation would be to change the petclinic application to allow for an environment variable override of the database host, and use the ENV directive in our petclinic Dockerfile.

Luckily, I’ve done just that! Now that we can update the database host without editing source code, we can safely link a volume containing the petclinic application on our local machine. In preparation for that, let’s checkout the override-environment branch from the petclinic fork.

$ cd ~/tmp
$ git clone https://github.com/influenza/grails-petclinic.git petclinic-fork
$ cd petclinic-fork
$ git checkout override-environment

Now, we’ll update the Dockerfile to use an environment variable rather than running a search and replace across grails-app/conf/DataSource.groovy.

Here’s the new version:

Note that the only change is line 13 - we now use ENV rather than RUN-ing sed. If you’re following along from scratch (rather than from the blog-snippets repo), be sure to rebuild the petclinic2 image with sudo docker build -t petclinic2 .!.

Let’s remove the previous petclinicApp container:

$ sudo docker rm petclinicApp # <-- container name, not the image name

Ensure the postgres container is still running:

$ sudo docker ps -a | grep petclinicData # Check for up status - if not...
$ sudo docker start petclinicData # <-- names! So awesome :-)

Now let’s run the petclinic application container again, this time linking to our local copy of the application source.

$ cd blog-snippets/VagrantToDockerPartTwo/Docker/Petclinic
$ sudo docker build -t petclinic2 . 
$ sudo docker run -t -i -p 8080:8080 \
  --name petclinicApp \
  --link petclinicData:database \
  --volume ~/tmp/petclinic-fork:/opt/petclinic/app \
  petclinic /opt/petclinic/app/grailsw
grails> run-app

Note - Since we are using our local grails application directory, the dependencies will be downloaded again the first time you run this command

Once the application finishes loading, you can again verify it is working. Additionally, now that we have used a volume to link our local source to the Docker container, we can make updates on the fly locally and see them reflected in the application.

To give this a go, try editing ~/tmp/petclinic-fork/grails-app/views/clinic/index.gsp line 13 to say ‘Display most vegetarians’ instead of ‘Display all veterinarians’. Once the file is saved, reload your browser behold your handiwork.

An Aside: Docker’s Containers and Images

In the section above, we iterated on the initial Dockerfile to create something less hacky and easier to work with. This led to building a new image based on the changes. After the build, we used docker run with a bunch of parameters to wire it up just the way we like it. Since we named the newly running container, we can easily use docker start, docker stop, and docker attach to interact with this same container, meaning we don’t need to type out this giant command line invocation each time we want to interact with it, nor do we need to remember or store container UUIDs.

For instance, let’s say we decide to take a break from our serious petclinic development and exit out of the grails shell:

grails> exit
$ <dropped back to the local host's shell>

After our break, we can easily resume where we left off with:

$ sudo docker start petclinicApp
$ sudo docker attach petclinicApp
grails>

Awesome! No need to specify the volume bindings or container links! If the postgres container isn’t running and we try to start the petclinic2 container (which is linked to it), docker will give us a helpful reminder:

$ sudo docker stop petclinicApp
petclinic2
$ sudo docker stop petclinicData
petclinicData
$ sudo docker start petclinicApp
Error: Cannot start container petclinicApp: Cannot link to a non running container: /petclinicData AS /petclinicApp/database
2014/05/18 16:19:37 Error: failed to start one or more containers

The error tells us in plain english that we can’t start the container we requested, as we cannot link to the non-running container, petclinicData.

This workflow is SO MUCH FASTER than the Vagrant+Ansible+Virtual box based approach.

The Takeaway

With container linking support and the vast catalog of images available in the Docker index, we can easily adapt most Vagrant+Ansible configurations to use Docker instead. The performance benefits of the migration will pay for itself after just a single day of use. Try it out for yourself for a few days. You’ll find that the speed and simplicity of working with docker containers will make working with VMs extremely painful.

Some Cleanup

During the course of the Docker experimentation I’ve been doing, I’ve ended up with a lot of containers that I’m not using. If you want to remove all but the running docker containers, here’s a command to do that:

$ sudo docker ps -a | tail -n +2 | cut -d ' ' -f 1 | xargs -L 1 sudo docker rm

What next?

After my initial post on this topic, I received a lot of feedback about new Vagrant support for using Docker as a vm provider.

For a look at deploying a Grails application into Tomcat running in a Docker container, see this post.