Docker-logo

In August I wrote a blog post on the creation of tomcat images. Since then, Docker has evolved much, and so has my own knowledge of it. I’d like to update you on what I found to be a good way to manage container processes. After reading this article, I hope you will find good use of the Supervisor image that you can find in my repository on github.

The Docker command

One of the points that came up in aforementioned post was that Docker (only) supports running a single foreground process. We are generally accustomed to something like upstart to have services be initialized at start up, but Docker does not run anything by default, which may be somewhat unexpected if you start out with Docker. You have to specify the proces you want to run. This behavior is contrary to that of a virtual machine an the advantage is that this keeps the container as lightweight as possible. You can start the container specifying the command option at the end of the run command, e.g,

docker run ubuntu echo "hello world"

Alternatively, you can use the CMD directive in a Dockerfile to have Docker run a command by default. E.g. if you build the image hello_world_printer using

docker build -t "hello_world_printer" .

from the directory containing the Dockerfile

FROM ubuntu
CMD echo "hello world"

you would achieve  exactly the same by running

docker run hello_world_printer

Note that, since you can override the CMD directive command line, this really is a run time directive. Fun fact is that on a Linux container you could just invoke upstart and get much of the same behavior as a regular virtual machine.

Running multiple commands

A common wish is to run multiple processes. E.g. an ssh server (to be able to connect to the running container) and your actual application. You could run the container with something like

docker run ... /usr/sbin/sshd && run_your_app_in_foreground

which might come in handy during development. Now, when the application process quits, the container is automatically shut down since that was our only foreground process. While you can fix that using /usr/bin/sshd -D the important point here is that setting up the initialization in the run command is not very neat. And, when your container becomes more complicated, the run command tends to grow.

So, to run more complicated containers, many people use complicated bash scripts. The bash script typically runs a foreground process and often spawns one or more (renegade) daemon processes. A major improvement of this approach over only using the Docker CLI is that the bash scripts are source controlled: the start-up scripts live in your Docker image, which is the new deliverable of you software project. Still, managing processes in bash scripts is simply no fun at all and prone to errors.

… using Supervisor

Supervisor-logo
A better way is to use Supervisor. Supervisor allows for better control of our processes: it is very simple and clear code; it can restart processes after a crash; it allows for restartable process groups and a command line or web interface to manage processes. Of course, with great power comes great responsibility: extensively using Supervisor features is a code smell indicating that you might be better off chopping up your image into several smaller ones.

Personally, I like how Supervisor allows me to cleanly code the process start-up. The clearest use case as I see it arises when child images expand the process set. For instance, if you often use SSH, it might be logical to have an SSH base image. In that case, implementing start-up of the SSH daemon in all the extending images is a form of code duplication. I’ll now show you what I found to be a good way to approach this.

The Supervisor base image

First of all, since I am now using Supervisor by default all my images extend from a base image containing only Supervisor and an updated version of Ubuntu. You can find the Dockerfile here. This base image contains a configuration file /etc/supervisor.conf:

[supervisord]
nodaemon=true

[include]
files = /etc/supervisor/conf.d/*.conf

This configuration makes Supervisor itself run as a foreground process, which will keep our containers up and running. Secondly, it includes all conf files in the /etc/supervisor/conf.d/ directory, starting whatever programs are defined in there.

Extending the base image

tomcat-stack

So, the idea is simple. All child containers add their service to the supervisor configuration by adding their specific service.sv.conf to the specified directory. Then, starting the container using

docker run child_image_name "supervisor -c /etc/supervisor.conf"

automatically starts all the specified processes. You can extend the image with multiple layers, each time having the option of adding one or more services to the configuration directory. Effectively, the Supervisor start command replaces upstart in a Docker compliant fashion.

As an example, let’s take a look at the Tomcat stack from aforementioned blog post, which is now updated.

  1. Firstly, as discussed, we have the Supervisor base image extending from Ubuntu
  2. Then, we have a JDK image, which installs Java on top of Supervisor. Java is just a library for other services, so we do not define a service in this layer. Common tasks as setting the JAVA_HOME environment variable are the responsibility of this layer.
  3. The Tomcat image install Tomcat onto the stack and exposes port 8080. This layer does include a service, namely Tomcat, which is defined in tomcat.sv.conf:
    [program:webapp]
    command=/bin/bash -c "env > /tmp/tomcat.env && cat /etc/default/tomcat7 >> /tmp/tomcat.env && mv /tmp/tomcat.env /etc/default/tomcat7 && service tomcat7 start"
    redirect_stderr=true
    

    The run command for the Tomcat service is not as clean as I’d like it to be and could very well be put in a dedicated script. What it does is prepending the environment variables, such as container linking parameters, to /etc/default/tomcat7, so we can use those in following configuration lines as in the following example. It might be more neat to use an available key-value store such as etcd, but that is beyond the scope of this article.
    Of course, we only have the default installation files in here, and no actual web application yet.

  4. Your web application would extend Tomcat with an actual application installation. When starting Supervisor, it will automatically start Tomcat.

Example Tomcat webapp Dockerfile

An actual web application installation is beyond the scope of this article, but as a finishing touch, let me give a partial example Dockerfile of how to use this stack. This example is pretty Java Tomcat specific, so if you’re not interested in that, stop reading now and have fun with other stuff 🙂

Suppose, we have a web app that uses Elasticsearch:

FROM quintenk/tomcat:7

# Install dependencies for project that do not change per check-in
# RUN apt-get -y install ...

RUN rm -rf /var/lib/tomcat7/webapps/*

# append configuration to /etc/default/tomcat7, such as:
...
RUN echo 'DOCKER_OPTS="-DELASTICSEARCH_SERVER_URL=${ELASTICSEARCH_PORT_9200_TCP_ADDR}"' >> /etc/default/tomcat7
RUN echo 'CATALINA_OPTS="... ${DOCKER_OPTS}"' >> /etc/default/tomcat7

# add configuration files such as log4j.properties and chown root:tomcat7 them

# Assume the project has been built and that ROOT.war is in our Docker build directory (containing the Dockerfile). For caching purposes, add this as one of the last steps
ADD ROOT.war /var/lib/tomcat7/webapps/
RUN chown root:tomcat7 /var/lib/tomcat7/webapps/ROOT.war

CMD supervisord -c /etc/supervisor.conf

In this code, the variables for elasticsearch (a search index), are set because the Supervisor configuration for Tomcat prepends all variables to the /etc/default/tomcat7 file at start-up time. Of course, we would need to start the webapp with a link to the elasticsearch container: e.g.

docker run -link name_of_elasticsearch_instance:elasticsearch -d name_of_webapp_image "supervisor -c /etc/supervisor.conf"

You now end up with a web application that has access to the ELASTICSEARCH_SERVER_URL. You can use this in a properties file like

elastic.unicast.hosts=${ELASTICSEARCH_SERVER_URL}

which exposes the property to your application. If you are a Java developer and have read through the last section as well, this is the time where I wish you happy coding!