Building a Docker Image for a Maven Project
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.
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.
Comments