Java Ecosystem, Kotlin, Distributed Systems, Sociology of Software Development

Building a Dropwizard Microservice with Docker and Maven

Posted on Sep 20, 2015

Dropwizard produces a fat jar containing every dependency your microservice needs to run. This includes a web server. This way, no web server needs to be installed and configured on the target machine. However, there is some infrastructure left (like the JRE) which still has to be installed before the deployment. That’s where Docker enters the stage. With Docker we can produce an artifact containing really everything we need to run our microservice. In this post, we take a look at how we can integrate Docker into our Maven build, run our tests against the container and push the image to a repository.

Building a Dropwizard Microservice with Docker and Maven

Microservices and Docker

(Dropwizard) Microservices and Docker fit pretty well together. You receive the full control over the execution environment of your microservice (environment variables,  runtime parameters, heap space arguments, file system, JRE). We can test with the environment that will finally be deployed into production. This increases the reliability of tests and significantly reduces the risk when deploying the application in production.

Moreover, the build is getting straightforward: _One _Dropwizard project produces one artifact (the runnable fat jar), which can be wrapped into _one_ Docker image that runs independently on a target machine. Moreover, no infrastructure (JRE, web or application server) has to be installed before the deployment. The image contains everything necessary. This independence and the small deployment unit speeds up and simplifies our build and deployment. This is a huge benefit and makes the implementation of Continuous Delivery easy.

Preconditions: The Fat Jar

Let’s assume that you have already a Dropwizard project, which produces a nice runnable fat jar. So calling mvn package creates target/app-0.0.1-SNAPSHOT.jar.

The Dockerized Maven Build

The Dockerized Maven Build

The Dockerized Maven Build

  • Building a Docker image containing the microservice fat jar.
  • Running the integration tests against the created container. Therefore, we have to start the container before the integration tests, run the integration tests and stop the container afterwards.
  • Our microservice needs a database (in this case a MongoDB). Therefore, we also have to run a container with MongoDB before the integration tests can run. In order to provide our microservice access to the database, we link both container together.
  • Finally, we push our built and tested microservice image to a Docker registry (like Docker Hub).

Here we go.

Step 1. Building the Docker Image with the docker-maven-plugin

There are a lot of docker-maven-plugins available. I’m using the docker-maven-plugin with the groupId org.jolokia, because it seems to be actively developed with a lot of commits and contributors. Let’s configure the docker-maven-plugin to build the image with our microservice called “prozu”. Therefore the plugin has to create a Dockerfile first. The Dockerfile is configurated at two points. The first is the plugin configuration in the pom:

<properties>
    <docker.registry.name></docker.registry.name><!-- leave empty for docker hub; use e.g. "localhost:5000/" for a local docker registry -->
    <docker.repository.name>${docker.registry.name}phauer/${project.artifactId}</docker.repository.name>
</properties>
...
<plugin>
    <groupId>org.jolokia</groupId>
    <artifactId>docker-maven-plugin</artifactId>
    <version>0.13.3</version>
    <configuration>
        <images>
            <image>
                <alias>${project.artifactId}</alias>
                <name>${docker.repository.name}:${project.version}</name>
                <build>
                    <from>java:8-jre</from>
                    <maintainer>phauer</maintainer>
                    <assembly>
                        <descriptor>docker-assembly.xml</descriptor>
                    </assembly>
                    <ports>
                        <port>8080</port>
                        <port>8081</port>
                    </ports>
                    <cmd>
                        <shell>java -jar \
                            /maven/${project.build.finalName}.jar server \
                            /maven/docker-config.yml</shell>
                    </cmd>
                </build>
                <run>
                <!-- later more -->
                </run>
            </image>
        </images>
    </configuration>
</plugin>

This configuration tells the docker-maven-plugin to create a Dockerfile based on the image “java:8-jre” (so we get a JRE8), expose the ports 8080 and 8081 (the standard ports of a Dropwizard service and its admin backend) and finally provide the command to start the runnable fat jar.

Note: For this simple demo the classical version numbering (“0.1.3”) is good enough. However, there are better approaches. See Version Numbers for Continuous Delivery with Maven and Docker for details.

But how do we tell the plugin which files should be put into the container? This is done by means of the assembly descriptor docker-assembly.xml which is placed under src/main/docker .

<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
    <id>${project.artifactId}</id>
    <files>
        <file>
            <source>target/${project.build.finalName}.jar</source>
            <outputDirectory>/</outputDirectory>
        </file>
        <file>
            <source>src/main/resources/docker-config.yml</source>
            <outputDirectory>/</outputDirectory>
        </file>
    </files>
</assembly>

This way, the plugin adds the Dropwizard jar and the configuration yml to the Dockerfile. The resulting Dockerfile can be found in target/docker/  and looks like this:

FROM java:8-jre
MAINTAINER phauer
EXPOSE 8080 8081
COPY maven /maven/
CMD java -jar \
    /maven/prozu-service-0.0.1-SNAPSHOT.jar server \
    /maven/docker-config.yml

The folder maven contains everything we specified in the assembly descriptor and is also located under target/docker.

Now we can build the docker container using

mvn docker:build

Afterwards you can check the existence of the image in the local repository using docker images or run it manually with docker run -d -p 8080:8080 -p 8081:8081 phauer/prozu-service:0.0.1-SNAPSHOT (or use -it instead of -d to run it in the foreground and to see the output).

Step 2. Running the Microservice Container

But we want to start our container with Maven. For this we have to add the following <run> block right under the <build> block above.

<properties>
    <docker.host.address>localhost</docker.host.address><!-- this is not localhost when using boot2docker! -->
    <prozu.port>8080</prozu.port>
    <prozu.port.admin>8081</prozu.port.admin>
</properties>
...
<run>
    <namingStrategy>alias</namingStrategy>
    <ports>
        <port>${prozu.port}:8080</port>
        <port>${prozu.port.admin}:8081</port>
    </ports>
    <volumes>
        <bind>
            <volume>${user.home}/logs:/logs</volume>
        </bind>
    </volumes>
    <wait>
        <url>http://${docker.host.address}:${prozu.port.admin}/ping</url>
        <time>10000</time>
    </wait>
    <log>
        <prefix>${project.artifactId}</prefix>
        <color>cyan</color>
    </log>
</run>

In the <run> block we define the parameters for running our microservice container. We map the ports 8080 and 8081 of our microservice within the container to the same ports on our host machine so we can access the service. Moreover, we bind the folder logs within the container (this is where our microservice places its log files) to ~/logs so we can access the log files on our host machine. To enable logging in Dropwizard we have to add an appender in the configuration yml file:

logging:
  level: INFO
  appenders:
    - type: console
      threshold: ALL
      timeZone: UTC
      target: stdout
      logFormat: "%-6level [%d{HH:mm:ss.SSS}] [%t] %logger{5} - %X{code} %msg %n"
    - type: file
      currentLogFilename: /logs/prozu.log
      threshold: ALL
      archive: true
      archivedLogFilenamePattern: /logs/prozu-%d.log
      archivedFileCount: 5
      timeZone: UTC
      logFormat: "%-6level [%d{HH:mm:ss.SSS}] [%t] %logger{5} - %X{code} %msg %n"

Finally, the docker-maven-plugin needs to know, how it can detect if the container has finished starting up. This is done in the configuration.  We tell to poll the ping URL of the admin backend until it receives an answer. Please note that the docker host is not ‘localhost’ if you are using boot2docker on Windows or Mac OS X. The IP of the docker host/the VM is shown at startup of the docker daemon or by running docker-machine ip default.

Now we can start our container by calling mvn docker:start. However, our microservice needs a running database.

Step 3. Linking with the Database Container, Exchanging Ports and Running both Containers

First, we configure our database container. For that we just define another <image>. There is no <build> block necessary, because we use a predefined image from Docker Hub.

<image>
    <alias>mongodb</alias>
    <name>mongo:2.6.11</name>
    <run>
        <namingStrategy>alias</namingStrategy>
        <cmd>--smallfiles</cmd>
        <wait>
            <log>waiting for connections on port</log>
            <time>10000</time>
        </wait>
        <log>
            <prefix>MongoDB</prefix>
            <color>yellow</color>
        </log>
    </run>
</image>

Now we can start the MongoDB container, but it’s not accessible by the microservice yet. One solution would be to bind MongoDB’s port 27017 to a port on the host system. This way the microservice could access MongoDB by using this port. In this case we have to take care of the mircoservice (within the container), which should always get the right IP of the host system and the port. Moreover, sometimes you don’t want to expose the MongoDB on the whole host system. So let’s try another approach for connecting containers: linking.  Linking allows containers to exchange information (like IPs or ports) without exposing them on the host system. Fortunately, the docker-maven-plugin allows us to comfortably link containers together. The only thing we have to do is to add an element in the <run> block of our microservice image configuration.

<run>
    ...
    <links>
        <link>mongodb:db</link>
    </links>
</run>

Now there are additional environment variables available within the microservice container. For instance the variable $DB_PORT_27017_TCP_ADDR contains the IP and $DB_PORT_27017_TCP_PORT contains the port. The only thing left to do, is to pass this information to our microservice. We could use the configuration yml file for this or just use system properties. We are doing the latter and adjust our starting command:

<cmd>
    <shell>java -DdbHost=$DB_PORT_27017_TCP_ADDR \
        -DdbPort=$DB_PORT_27017_TCP_PORT -jar \
        /maven/${project.build.finalName}.jar server \
        /maven/docker-config.yml</shell>
</cmd>

In your Java code we can access the system properties by calling System.getProperty("dbPort").

Now we can run mvn docker:start and the docker-maven-plugin will first start the MongoDB container and afterwards the microservice container. mvn docker:stop will stop both containers. That’s nice!

Step 4: Run the Integration Tests against the Container

Next we have to tell maven to start both containers before the integration tests and stop them afterwards. This is easy. Just add the following executions to the docker-maven-plugin:

<execution>
    <id>start</id>
    <phase>pre-integration-test</phase>
    <goals>
        <goal>build</goal>
        <goal>start</goal>
    </goals>
</execution>
<execution>
    <id>stop</id>
    <phase>post-integration-test</phase>
    <goals>
        <goal>stop</goal>
    </goals>
</execution>

Besides, we have to configure the failsafe plugin to execute the integration tests. I like to use a self-defined marker annotation (IntegrationTest) to distinguish between unit and integration tests.

package de.philipphauer.prozu.di;

import java.lang.annotation.*;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface IntegrationTest {}

Moreover, we pass the URL of our service to the test with the system property service.url.

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.18.1</version>
    <configuration>
        <phase>integration-test</phase>
        <includes>
            <include>**/*.java</include>
        </includes>
        <groups>de.philipphauer.prozu.di.IntegrationTest</groups>
        <systemPropertyVariables>
            <service.url>http://${docker.host.address}:${prozu.port}/</service.url>
        </systemPropertyVariables>
    </configuration>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <version>2.18.1</version>
    <configuration>
        <excludedGroups>de.philipphauer.prozu.di.IntegrationTest</excludedGroups>
    </configuration>
</plugin>

A simple integration test looks like this:

@Category(IntegrationTest.class)
public class EmployeeResourceTestViaDocker {
    @Test
    public void testConnection() throws IOException {
        String baseUrl = System.getProperty("service.url");
        URL serviceUrl = new URL(baseUrl + "employees");
        HttpURLConnection connection = (HttpURLConnection) serviceUrl.openConnection();
        int responseCode = connection.getResponseCode();
        assertEquals(200, responseCode);
    }
}

You can try the whole process by running mvn verify.

At this point I like to emphasize how great it is to use a database container for tests. This way, we don’t have to set up and maintain a database for the tests manually. The plugin takes care for us and automatically starts a fresh database. That’s nice for testing. Moreover, we don’t have to clean an existing database because we always start with an empty one. You only have to clean the database before each test. Since creating a fresh database is that easy and fast, we don’t need an in-memory-database.

Step 5: Pushing the Image to a Registry

Finally, we want to push the image to the image registry Docker Hub. The configuration needed is easy. Just add another execution to the docker-maven-plugin:

<execution>
    <id>push-to-docker-registry</id>
    <phase>deploy</phase>
    <goals>
        <goal>push</goal>
    </goals>
</execution>

Let’s assume we only want to produce an image and we don’t want to deploy the fat jar to an artifact repository. Therefore, we skip the normal execution of the maven-deploy-plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-deploy-plugin</artifactId>
    <version>2.7</version>
    <configuration>
        <skip>true</skip>
    </configuration>
</plugin>

Don’t forget to pass your username and password of your Docker Hub account.

mvn -Ddocker.username=<username> -Ddocker.password=<password> deploy

This statement builds a Docker image with our microservice, tests it and finally deploys it to Docker Hub.

If you want to push your image to your own registry instead of Docker Hub just set the docker.registry.name property. Let’s assume your docker registry runs on your local machine on port 5000:

<docker.registry.name>localhost:5000/</docker.registry.name>

Voila! We successfully integrated Docker into our Maven build lifecycle.

Further Reading