Combining Docker and Maven

When you’re building Java or JVM-based software, chances are these days you’ll be deploying it inside Docker. Chances also are you’re building it with Maven. Now how do you combine the two? Of course, you could plumb together some scripts for the platform of your choice, but there’s a few disadvantages to that. First of all, it makes you platform-dependant: your build may not work - or behave differently - depending on the platform where you’re building. Secondly, it’s not very elegant, since it does not easily facilitate re-use. And in these days of microservices, we don’t want to be copying the same scripts over and over again. Thirdly, it requires the Docker binaries to be present on the system where you perform your build.

Wouldn’t it be nice if we could natively integrate Docker into a Maven build process? Wouldn’t it be nice if one of the built artifacts of your Maven build is a Docker image, published in a registry? Welcome to the future, brought to us by the docker-maven-plugin from Spotify. This plugin does not require any Docker binaries to be present, making your build truly platform-independant again.

Basics

Let’s start with attaching the plugin to the package phase. Since that is the Maven phase where artifacts are packaged, it makes sense to build our Docker image as well. Frankly, a Docker image is just another artifact, isn’t it?

<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>docker-maven-plugin</artifactId>
  <version>0.4.13</version>
  <configuration>
    <dockerDirectory>src/main/docker</dockerDirectory>
    <imageName>${project.artifactId}:latest</imageName>
  </configuration>
  <executions>
      <execution>
        <id>build-container</id>
        <phase>package</phase>
        <goals>
          <goal>build</goal>
        </goals>
      </execution>
  </executions>
</plugin>

This way, the name of the Docker image is always equal to the project artifactId. It will also specify the version as latest, which is more-or-less the Docker equivalent of a -SNAPSHOT.

By default, Maven will deploy built artifacts to a central repository like Nexus. But for Docker images, that makes no sense: they can become several hundreds of megabytes, and our storage needs would grow quickly. Aside from that, the native Docker tooling is built around the concept of a registry, which can handle these big images efficiently because it also uses the layered filesystem that powers Docker images. So let’s instead push the images to our registry by adding the following execution:

<execution>
  <id>push-container</id>
  <phase>deploy</phase>
  <goals>
    <goal>push</goal>
  </goals>
</execution>

Finally, let’s add a Dockerfile to the src/main/docker-directory. By default, the Docker-plugin will create a build context consisting of all files in this directory. We can add additional artifacts, like our built JAR, to this context:

<configuration>
  <resources>
      <resource>
      <directory>${project.build.directory}</directory>
      <include>${project.build.finalName}.jar</include>
    </resource>
  </resources>
</configuration>

During build, these artifacts will be copied to target/docker, just like the contents of src/main/docker. This way, our build context stays relatively small, which speeds up building the image and also keeps the final image small.

Releases

The Maven Release Plugin allows you to version your sofware. It will create tags in the source control system of your choice and attempt to publish built JARs in a central repository like Nexus. Now that we can build Docker images with our application, we would like to publish these images as well using the same version number. This way, the version of the image will match the version in source control and the version in our Maven repo.

To achieve this, first we must define a special release profile that will be activated during release builds:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-release-plugin</artifactId>
  <version>2.5.3</version>
  <configuration>
    <releaseProfiles>release</releaseProfiles>
  </configuration>
</plugin>

Next, we can make subtle changes in this profile to the Docker-plugin:

<profile>
  <id>release</id>
  <build>
    <plugins>
      <plugin>
        <groupId>com.spotify</groupId>
        <artifactId>docker-maven-plugin</artifactId>
        <configuration>
          <!-- During a release, we want to publish the image with the same 
               version as we'll have in Nexus. -->
         <imageName>${project.artifactId}:${project.version}</imageName>
       </configuration>
     </plugin>
    </plugins>
  </build>
</profile>

This way, we have the image version coupled to the project version and coupled to the artifact version in our Maven repo.

Private Docker registries

In the examples above, we didn’t specify where our images should be pushed to. Oftentimes, you don’t want your images published in the public Docker Hub. Instead, it’s quite likely that you have a private repo inside your organisation of even for your project. Let’s assume it is located at http://registry.docker.internal/, all you need to do is one little change. The name of the image in your POM must contain the name of the registry as well.

<imageName>registry.docker.internal/${project.artifactId}:latest</imageName>