Using Docker to efficiently create multiple tomcat instances

by Quinten KrijgerAugust 15, 2013

Docker-logoIn my previous blog article I gave a short introduction into Docker (“an open-source engine that automates the deployment of any application as a lightweight, portable, self-sufficient container that will run virtually anywhere”). In this article we’ll check out how to create an image for Tomcat 7 and the Java 7 JDK as dependency.

So, let’s go ahead and do some ‘coding’. First, you need to install docker. Instructions can be found here. I already mentioned you need a Linux system with a modern kernel, so if you happen to be a Mac or Windows user, there are instructions on linked pages on how to use Vagrant to easily setup a virtual machine (VM) to use. For now we’ll work locally, but once you start installing servers you might find the Chef project to install docker useful as well.

As a first step after installation, let’s pick the first example from the Docker getting started page and create an Ubuntu 12.04 container, with completely separated processes, its own file system and its own network interface (but with network connection via the host), and have it print “hello world”. Do this by running

docker run ubuntu /bin/echo hello world

Cool huh? You probably just ran something on a different OS than that of your own machine or (in case you’re on Windows/Mac) the VM in which Docker is running! In this command ubuntu defines the image (found automatically as it is one of the standard images supplied by Docker). The run command creates an instance of the image (a container), feeding it /bin/echo hello world as the command to execute.

The behavior that you just tested is exactly the behavior that I had, or some other fellow doing this blog; no more “but it works on my machine…”.  It probably took some time due to Ubuntu 12.04 being downloaded. However, that is on the first run only; after that we’re talking milliseconds. You can run

docker images

to see that the Ubuntu 12.04 has been downloaded to your system, aliased as precise and latest. So, you could have used docker run ubuntu:precise /bin/echo hello world (notice the use of the tag) as well. Run

docker ps -a -s

to see the containers. -a makes sure you see already closed containers (useful, since echoing hello world doesn’t take that long), and -s gives you the container file size as well. Containers as simple as these are some kBs big only (even when running multiple concurrently), because they both use the same image and share those resources. That is a major advantage over VMs, but I already mentioned that in the introductory blog.

Step 2. Create an image with the Java JDK 7. I chose to do the Oracle version with the added complexity of scripting the automatic license acceptance. Actually, we could go about this two ways.

  1. Run the Ubuntu image interactively (using docker run ubuntu ps -i -t /bin/bash) and do whatever we want in the shell. When done, we could exit the shell, thereby closing the container, and commit the result into a new image using docker commit quintenk/jdk7-oracle. This method has the disadvantage of not being transparant or reproduceable.
  2. Use Dockers Dockerfile Builder: we create a simple script file called “Dockerfile” and have Docker run and commit each command to a new image, allowing us to give a human readable name to the final result. Note that while we end up with 8 images (which you can see later by using the -a option with the docker images command), Docker only saves a diff for each file, so most images use a few kBs of disk space. Now all build steps are completely transparant and reproduceable. You can also find the following Dockerfiles on my github repository.
FROM ubuntu:precise
MAINTAINER Quinten Krijger < qkrijger [at] gmail {dot} com>
# make sure the package repository is up to date
RUN echo "deb precise main universe" > /etc/apt/sources.list

RUN apt-get update && apt-get -y install python-software-properties
RUN add-apt-repository ppa:webupd8team/java
RUN apt-get update && apt-get -y upgrade

# automatically accept oracle license
RUN echo oracle-java7-installer shared/accepted-oracle-license-v1-1 select true | /usr/bin/debconf-set-selections
# and install java 7 oracle jdk
RUN apt-get -y install oracle-java7-installer && apt-get clean
RUN update-alternatives --display java
RUN echo "JAVA_HOME=/usr/lib/jvm/java-7-oracle" >> /etc/environment

The FROM statement is the mandatory first line to let Docker know what to build this image from. MAINTAINER is of course optional. The RUN commands are executed when building the image: for each line, a container is initialized and the command executed after which the result is committed. To actually build the image, you need this in a file called “Dockerfile” (e.g. by cloning my repository) and go to the file location. Then run

docker build -t quintenk/jdk7-oracle .

Notice that Ubuntu was not downloaded again. As an experiment to see how developing a Dockerfile goes in practice you could alter the Dockerfile and change one of the latter lines. If you then build again (maybe without the -t quintenk/jdk7-oracle option) you will find that the caching mechanism is worth gold. Try stuff like that when testing Ansible installation scripts on a local VM and you’ll see the difference!

Step 3. Now that we have an image with Java installed, we can create other images based on it. We’ll try Tomcat 7, but if you make other images as well on the same base, they will share the JDK image resources! For Tomcat I created the following Dockerfile (also in the same git repository):

FROM quintenk/jdk7-oracle
MAINTAINER Quinten Krijger ""

RUN apt-get -y install tomcat7
RUN echo "JAVA_HOME=/usr/lib/jvm/java-7-oracle" >> /etc/default/tomcat7
CMD service tomcat7 start && tail -f /var/lib/tomcat7/logs/catalina.out

(edit: note the comment by Anton on using ‘tail -F’ rather than ‘-f’) You’ll notice the EXPORT command in here, which exposes Tomcats port to the container. The CMD is the command that will be executed when a container is started. So, when we build the image and run it as a daemon using

docker run -d quintenk/tomcat7

it starts Tomcat as a service and then tails the log file. The last step is needed because you need a running foreground process in order for the container to keep running. If you omit it, the container will start Tomcat and shut down immediately after. Now run docker ps to see

ID       d02ed25688be
IMAGE    quintenk/tomcat7:latest
COMMAND  /bin/sh -c service t
PORTS    49153 -> 8080

So, 8080 on the container is exposed to the host port 49153, and you can see Tomcat running at http://localhost:49153. Run another container, and you’ll probably get it under port 49154. This way, we can now start lots of Tomcat containers, all completely isolated and show the results in the same browser.

As a next step, and an actual use case, I would install different versions of the same web application, create images from that and run them at the same time. You can then demonstrate to your customer what you have produced in the last few weeks :). However, I’ve blogged enough for today. If you do give it a spin, you don’t have to expose 8080 again, but I believe you do need to give a new CMD. If you don’t specify a CMD you could run the container in interactive mode with shell, and start tomcat from the command line – maybe less clean, but useful to understand that it is an option.

Note that if you would just like the images and aren’t interested in building them yourself, you can download them from the Docker index, where I published them. To do this, simply run

docker pull quintenk/tomcat7

From there, you could install you own web application using the techniques described in this article.

Have fun!