Recently, I had to set up the process of building Docker images for a Maven-based Java project. First, I tried existing Maven plugins, namely spotify/dockerfile-maven and fabric8io/docker-maven-plugin. Both allow you to use Maven for building Docker images without using the Docker CLI directly. However, this approach was problematic as I wanted to build the images on a Gitlab CI server which itself used Docker containers to run the builds. I did not want to create an own Docker image supporting the Docker-in-Docker feature and providing me with a Maven installation. Therefore, I came up with a very simple approach of building Docker images which can easily be used in Docker-based environments like Gitlab CI. The approach uses a template and the Maven Resources Plugin to generate a Dockerfile. This also allows refering to Maven properties within the Dockerfile template which can make the build process more flexible. The generated Dockerfile is then used on the Docker CLI to build the image of the application.

Demo Application

For demo purposes I used the Eclipse Vert.x toolkit and created a simple HTTP web server. It prints out Hello World! when a client accesses the website over port 8080. We now want to package a runnable jar file and all the dependencies of this application into a Docker image.

package at.flortsch.example.docker;

import io.vertx.core.Vertx;

public class Main {
    public static void main(String[] args) {
        Vertx.vertx()
             .createHttpServer()
             .requestHandler(request -> {
                 request.response()
                        .putHeader("content-type", "text/html")
                        .end("<h1>Hello World!</h1>");
             })
             .listen(8080);
    }
}

Maven Configuration

In the Maven POM file we specify properties for the source encoding and the Java version used. We also declare specific properties used in the following Dockerfile template and the Maven plugin configurations. We define a property for the main class used by the Maven Jar Plugin when creating the jar manifest. Then we define a property for the target directory used by the Maven Dependency Plugin to copy all dependencies of the application. This property is also used by the Maven Jar Plugin to append a prefix on the classpath entries in the jar manifest for the dependencies. Finally, we define a property for the working directory used by Docker when building the image and running a container.

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <application.mainclass>at.flortsch.example.docker.Main</application.mainclass>
    <application.dependencies>lib</application.dependencies>
    <application.workdir>application</application.workdir>
</properties>

We declare the dependencies used by the demo application which are vertx-core and vertx-web.

<dependencies>
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-core</artifactId>
        <version>3.4.2</version>
    </dependency>
    <dependency>
        <groupId>io.vertx</groupId>
        <artifactId>vertx-web</artifactId>
        <version>3.4.2</version>
    </dependency>
</dependencies>

Next, we configure the Maven Dependency Plugin which shall copy all the runtime dependency jars into a specific directory before the application is packaged. As output directory, we use ${project.build.directory}/${application.dependencies} resulting in target/lib of the project directory.

<plugin>
    <artifactId>maven-dependency-plugin</artifactId>
    <version>3.0.1</version>
    <executions>
        <execution>
            <phase>prepare-package</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <includeScope>runtime</includeScope>
                <outputDirectory>${project.build.directory}/${application.dependencies}</outputDirectory>
            </configuration>
        </execution>
    </executions>
</plugin>

Now, we configure the Maven Jar Plugin which shall create a runnable jar of the application. For this, the main class is defined in the jar manifest and classpath entries are added for each dependency jar. The entries are prefixed with the directory name where the dependencies are copied into by the Maven Dependency Plugin. Although we do not use any snapshot dependency, we disable the usage of unique version strings. This has to be done in order to match with the default behaviour of the Maven Dependency Plugin which replaces the SNAPSHOT version string with a concrete date, whereas the Maven Jar Plugin actually uses a SNAPSHOT version string. If you do not do this, the classpath entries in the jar manifest will not match the file names of the dependencies and hence, the application jar could not be executed.

<plugin>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.0.2</version>
    <configuration>
        <archive>
            <manifest>
                <addClasspath>true</addClasspath>
                <classpathPrefix>${application.dependencies}</classpathPrefix>
                <mainClass>${application.mainclass}</mainClass>
                <!-- match with base version string naming of Maven Dependency Plugin -->
                <useUniqueVersions>false</useUniqueVersions>
            </manifest>
        </archive>
    </configuration>
</plugin>

At the end of the Maven POM file we configure the Maven Resources Plugin which shall generate the Dockerfile. The plugin simply applies filtering on all files in the directory src/main/docker where we store our Dockerfile template. The generated file is stored in target, located in the project directory.

<plugin>
    <artifactId>maven-resources-plugin</artifactId>
    <version>3.0.2</version>
    <executions>
        <execution>
            <phase>generate-resources</phase>
            <goals>
                <goal>copy-resources</goal>
            </goals>
            <configuration>
                <outputDirectory>${project.build.directory}</outputDirectory>
                <resources>
                    <resource>
                        <directory>src/main/docker</directory>
                        <filtering>true</filtering>
                    </resource>
                </resources>
            </configuration>
        </execution>
    </executions>
</plugin>

Dockerfile Template

Now, we write our Dockerfile template using the properties defined in the Maven POM file. We start with openjdk:8-jre-alpine as base image which gives us Alpine Linux and the JRE 8. Next, we set the Docker working directory which is applied to all the following commands. Since the Dockerfile will be located in the target directory after we run mvn package, we can access the dependency jars and the application jar file. With this, we copy the directory containing the dependencies and the application jar into the working directory of the Docker image. As Docker uses a layered file system when creating images, it pays off to copy files which are not going to change at the beginning of the image. This allows Docker to cache files when building images resulting in less disk usage when doing frequent builds. Therefore, we copy the dependency jars first. Finally, we set the entry point of the image such that the application jar is executed when a container is started and expose port 8080 which is used by the HTTP web server.

FROM openjdk:8-jre-alpine

WORKDIR ${application.workdir}
COPY ${application.dependencies} ${application.dependencies}
COPY ${project.build.finalName}.jar ${project.build.finalName}.jar

ENTRYPOINT ["/usr/bin/java", "-jar", "${project.build.finalName}.jar"]
EXPOSE 8080

Maven Output Generation

Now, we look at some content produced by Maven after we execute mvn package. We list the files in the resulting target directory and see that a Dockerfile, a lib directory and an application jar file have been generated, together with some other directories.

drwxr-xr-x 8 flortsch flortsch 4096  9. Jul 00:24 .
drwxr-xr-x 5 flortsch flortsch 4096  9. Jul 00:24 ..
drwxr-xr-x 3 flortsch flortsch 4096  9. Jul 00:24 classes
-rw-r--r-- 1 flortsch flortsch  237  9. Jul 00:24 Dockerfile
drwxr-xr-x 3 flortsch flortsch 4096  9. Jul 00:24 generated-sources
drwxr-xr-x 2 flortsch flortsch 4096  9. Jul 00:24 lib
drwxr-xr-x 2 flortsch flortsch 4096  9. Jul 00:24 maven-archiver
-rw-r--r-- 1 flortsch flortsch 4090  9. Jul 00:24 maven-docker-example-0.0.1-SNAPSHOT.jar
drwxr-xr-x 3 flortsch flortsch 4096  9. Jul 00:24 maven-status
drwxr-xr-x 2 flortsch flortsch 4096  9. Jul 00:24 test-classes

We look into lib and see that all the dependencies have been copied by the Maven Dependency Plugin.

-rw-r--r-- 1 flortsch flortsch   50894  9. Jul 00:24 jackson-annotations-2.7.0.jar
-rw-r--r-- 1 flortsch flortsch  253001  9. Jul 00:24 jackson-core-2.7.4.jar
-rw-r--r-- 1 flortsch flortsch 1204187  9. Jul 00:24 jackson-databind-2.7.4.jar
-rw-r--r-- 1 flortsch flortsch  259095  9. Jul 00:24 netty-buffer-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch  308818  9. Jul 00:24 netty-codec-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch   54582  9. Jul 00:24 netty-codec-dns-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch  363516  9. Jul 00:24 netty-codec-http2-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch  544881  9. Jul 00:24 netty-codec-http-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch  120136  9. Jul 00:24 netty-codec-socks-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch  695998  9. Jul 00:24 netty-common-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch  331924  9. Jul 00:24 netty-handler-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch   20449  9. Jul 00:24 netty-handler-proxy-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch   29692  9. Jul 00:24 netty-resolver-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch   71417  9. Jul 00:24 netty-resolver-dns-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch  427426  9. Jul 00:24 netty-transport-4.1.8.Final.jar
-rw-r--r-- 1 flortsch flortsch   38684  9. Jul 00:24 vertx-auth-common-3.4.2.jar
-rw-r--r-- 1 flortsch flortsch 1074907  9. Jul 00:24 vertx-core-3.4.2.jar
-rw-r--r-- 1 flortsch flortsch  486502  9. Jul 00:24 vertx-web-3.4.2.jar

We open the application jar with a zip viewer and look at the manifest file. We see that all the classpath entries are correctly prefixed with lib. To test if the application works, we could now actually execute the application jar by going into the target directory and running java -jar maven-docker-example-0.0.1-SNAPSHOT.jar. But we will skip this step.

Manifest-Version: 1.0
Built-By: flortsch
Class-Path: lib/vertx-core-3.4.2.jar lib/netty-common-4.1.8.Final.jar 
 lib/netty-buffer-4.1.8.Final.jar lib/netty-transport-4.1.8.Final.jar 
 lib/netty-handler-4.1.8.Final.jar lib/netty-codec-4.1.8.Final.jar lib
 /netty-handler-proxy-4.1.8.Final.jar lib/netty-codec-socks-4.1.8.Fina
 l.jar lib/netty-codec-http-4.1.8.Final.jar lib/netty-codec-http2-4.1.
 8.Final.jar lib/netty-resolver-4.1.8.Final.jar lib/netty-resolver-dns
 -4.1.8.Final.jar lib/netty-codec-dns-4.1.8.Final.jar lib/jackson-core
 -2.7.4.jar lib/jackson-databind-2.7.4.jar lib/jackson-annotations-2.7
 .0.jar lib/vertx-web-3.4.2.jar lib/vertx-auth-common-3.4.2.jar
Created-By: Apache Maven 3.5.0
Build-Jdk: 1.8.0_131
Main-Class: at.flortsch.example.docker.Main

Finally, we look at the generated Dockerfile. We see that all the Maven properties referenced within the template have been filtered correctly by the Maven Resources Plugin.

FROM openjdk:8-jre-alpine

WORKDIR application
COPY lib lib
COPY maven-docker-example-0.0.1-SNAPSHOT.jar maven-docker-example-0.0.1-SNAPSHOT.jar

ENTRYPOINT ["/usr/bin/java", "-jar", "maven-docker-example-0.0.1-SNAPSHOT.jar"]
EXPOSE 8080

Building the Docker Image and Running a Container

We can now build a Docker image by going into the target directory and running the docker build command. When running the command, we point to the current directory where the generated Dockerfile is stored. We also label the resulting image according to the string specified by the -t parameter.

docker build -t flortsch/maven-docker-example:0.0.1-SNAPSHOT .

We can see the output of docker build indicating that the image has been built successfully.

Sending build context to Docker daemon  6.371MB
Step 1/6 : FROM openjdk:8-jre-alpine
8-jre-alpine: Pulling from library/openjdk
88286f41530e: Pull complete 
009f6e766a1b: Pull complete 
132a112fc74a: Pull complete 
Digest: sha256:74cafa4f3939e5da9970f990f273f3b89da7d889d77a40fb9673b33be86e8549
Status: Downloaded newer image for openjdk:8-jre-alpine
 ---> c4f9d77cd2a1
Step 2/6 : WORKDIR application
 ---> 906435393482
Removing intermediate container 889fc9cc6279
Step 3/6 : COPY lib lib
 ---> de1829cb0a94
Removing intermediate container 2e0601d05b9c
Step 4/6 : COPY maven-docker-example-0.0.1-SNAPSHOT.jar maven-docker-example-0.0.1-SNAPSHOT.jar
 ---> 604aca8c4899
Removing intermediate container 5cea0bf87c57
Step 5/6 : ENTRYPOINT /usr/bin/java -jar maven-docker-example-0.0.1-SNAPSHOT.jar
 ---> Running in 415069b10f30
 ---> 87ccb3007954
Removing intermediate container 415069b10f30
Step 6/6 : EXPOSE 8080
 ---> Running in b82ff58e7648
 ---> a910c9ae57e3
Removing intermediate container b82ff58e7648
Successfully built a910c9ae57e3
Successfully tagged flortsch/maven-docker-example:0.0.1-SNAPSHOT

We now run docker images to see the images stored locally on our machine after we have built our Docker image.

REPOSITORY                      TAG                 IMAGE ID            CREATED             SIZE
flortsch/maven-docker-example   0.0.1-SNAPSHOT      a910c9ae57e3        18 seconds ago      87.8MB
openjdk                         8-jre-alpine        c4f9d77cd2a1        10 days ago         81.4MB

Finally, we run our application by starting a Docker container with the image previously built and binding port 8080 on our local machine.

docker run -p 8080:8080 flortsch/maven-docker-example:0.0.1-SNAPSHOT

We can now access localhost:8080 with a web browser and see the Hello World! message from the web server.

Hello World

Conclusion

In this post I showed you how you can create Docker images for Maven-based Java projects using a demo application based on Eclipse Vert.x. The approach shown uses a template refering to Maven properties and the Maven Resources Plugin to generate a Dockerfile. The approach is simple and can be used easily in Docker-based build environments like e.g. Gitlab CI. We used the Maven Dependency Plugin to copy the dependencies of the demo application into a specific directory and we used the Maven Jar Plugin to create a runnable jar with classpath entries pointing to the copied dependencies. We verified this process by looking at the output generated by Maven. Finally, we built the Docker image and started a container using the Docker CLI. We opened the website in a browser to see that the web server was actually responding to our requests.

Click here if you want to check out the source of this project.

How do you build Docker images for your Maven-based Java Projects? Are you using specific Maven plugins which take care of this task, or do you also use your own Dockerfiles and the Docker CLI? Please let me know in the comments section below. If you have any feedback or question regarding this topic, do not hesitate to write me. I hope you liked this blog entry and stay tuned for upcoming posts.