Maven/Docker notes

Russell Bateman
July 2021
last update:



The goal is to figure out how much the Maven build of my web application, hl7v3-generator, should take on itself rather than leave it to some hairy, back-end build process to handle.

One problem untried here is whether this will run without installed Docker. I'm worried that it won't. To handle this downstream (rather negating the utility of what I have just spent several hours investigating), this can be added to the invocation of the Maven build:

$ mvn clean package -Ddockerfile.skip

(That is, in case of using Spotify's dockerfile-maven-plugin, which is the case.)

Why is this a problem?

Preliminaries

The Spotify plug-ins

This is how to build a Docker image using Maven and Spotify's plug-in.

<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>docker-maven-plugin</artifactId>
  <version>1.2.2</version>
  <configuration>
    <imageName>hl7v3-generator-docker</imageName>
    <baseImage>tomcat:jdk8-openjdk-slim</baseImage>
    <entryPoint>["java", "-jar",
	 "/${project.build.finalName}.jar"]</entryPoint>
    <resources>
      <resource>
        <targetPath>/</targetPath>
        <directory>${project.build.directory}</directory>
        <include>${project.build.finalName}.jar</include>
      </resource>
    </resources>
  </configuration>
</plugin>
$ mvn clean -DskipTests=true package docker:build     # (Spotify plug-in)

But, don't use Spotify's original Maven plug-in (above). Their docker-maven-plugin disclaimer says:

The future of docker-maven-plugin:

This plugin was the initial Maven plugin used at Spotify for building Docker images out of Java services. It was initially created in 2014 when we first began experimenting with Docker. This plugin is capable of generating a Dockerfile for you based on configuration in the pom.xml file for things like the FROM image, resources to add with ADD/COPY, etc.

Over time at Spotify we have realized that the simplest way to build a Docker image from a Java project is to have the developer write the Dockerfile. The behavior of this plugin around generating Dockerfiles, copying your project directory to a "staging" directory to use as the Docker build context, etc., ultimately led to a lot of unnecessary confusion with our users that stemmed from introducing extra abstractions and a need for configuration on top of what Docker is providing.

This led to the creation of a second Maven plugin for building docker images, dockerfile-maven, which we think offers a simpler mental model of working with Docker from Maven, for all of the reasons outlined in dockerfile-maven's README.

Spotify suggests using its latest plug-in, dockerfile-maven-plugin although it's discontinued development thereof.

<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.4.13</version>
  <executions>
    <execution>
      <id>default</id>
      <goals>
        <goal>build</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <repository>hl7v3-generator-docker</repository>
    <tag>${project.version}</tag>
    <buildArgs>
      <WAR_FILE>target/${project.build.finalName}.war</WAR_FILE>
    </buildArgs>
  </configuration>
</plugin>
$ mvn clean -DskipTests=true package dockerfile:build # (Spotify new plug-in)

The Maven maven-exec-plugin from Code Haus

It's just a plug-in that supports console/command line stuff. This is how to build a Docker image using maven-exec-plugin. It really doesn't work (I tried it).

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>3.0.0</version>
  <executions>
   <execution>
    <id>docker-clean</id>
    <phase>install</phase>
    <goals>
      <goal>exec</goal>
    </goals>
    <configuration>
      <executable>docker</executable>
      <workingDirectory>${project.basedir}</workingDirectory>
      <arguments>
        <argument>rmi</argument>
        <argument>${project.groupId}/${project.artifactId}:${project.version}</argument>
      </arguments>
    </configuration>
    </execution>
    <execution>
      <id>docker-build</id>
      <phase>install</phase>
      <goals>
        <goal>exec</goal>
      </goals>
      <configuration>
        <executable>docker</executable>
        <workingDirectory>${project.basedir}</workingDirectory>
        <arguments>
          <argument>build</argument>
          <argument>-t</argument>
          <argument>${project.groupId}/${project.artifactId}:${project.version}</argument>
          <argument>.</argument>
        </arguments>
      </configuration>
    </execution>
  <executions>
</plugin>

Some command lines I'm working with...

$ docker image rm -f image-name(s) or image-id(s)

Steps to work through this build enhancement...

  1. I had to put a copy of Dockerfile at the top of my project. It cannot sit down in src/main/docker/Dockerfile. Think of it as being like pom.xml.

  2. Tried Maven's maven-exec-plugin. This is very bad news; it didn't work whatsoever, but broke Maven.

  3. Tried Spotify's docker-maven-plugin. No problems, but it didn't put the WAR into the container. While seeking a solution, I saw that this plug-in is deprecated.

  4. I checked out dockerfile-maven-plugin also from Spotify. It does work well just as its antecedent. However, it's ill documented and abandoned too.

  5. I am building the container using (Spotify) this command. Then, I see my Docker container with the correct size (as compared to the identically sized one I first created a couple of months: ago). Here's how that went toward the end when Docker became Maven's focus:
    .
    $ mvn clean -DskipTests=true package dockerfile:maven
    .
    .
    .
    [INFO] --- maven-war-plugin:3.2.0:war (default-war) @ hl7v3-generator ---
    [INFO] Packaging webapp
    [INFO] Assembling webapp [hl7v3-generator] in [/home/russ/dev/hl7v3-generator/target/hl7v3-generator]
    [INFO] Processing war project
    [INFO] Copying webapp webResources [/home/russ/dev/hl7v3-generator/src/main/webapp] to...
    [INFO] Copying webapp resources [/home/russ/dev/hl7v3-generator/src/main/webapp]
    [INFO] Webapp assembled in [110 msecs]
    [INFO] Building war: /home/russ/dev/hl7v3-generator/target/hl7v3-generator##3.2.1.war
    [INFO]
    [INFO] --- dockerfile-maven-plugin:1.4.13:build (default) @ hl7v3-generator ---
    [INFO] dockerfile: null
    [INFO] contextDirectory: /home/russ/dev/hl7v3-generator
    [INFO] Building Docker context /home/russ/dev/hl7v3-generator
    [INFO] Path(dockerfile): null
    [INFO] Path(contextDirectory): /home/russ/dev/hl7v3-generator
    [INFO]
    [INFO] Image will be built as hl7v3-generator-docker:3.2.1
    [INFO]
    [INFO] Step 1/5 : FROM   tomcat:jdk8-openjdk-slim
    [INFO]
    [INFO] Pulling from library/tomcat
    [INFO] Image 33847f680f63: Pulling fs layer
    [INFO] Image 47d5ef013a61: Pulling fs layer
    [INFO] Image 9f9b3d6a536d: Pulling fs layer
    [INFO] Image b0fef2b01060: Pulling fs layer
    [INFO] Image e64cf0645cca: Pulling fs layer
    [INFO] Image 4c1e173cea35: Pulling fs layer
    [INFO] Image b261692ea796: Pulling fs layer
    [INFO] Image e64cf0645cca: Waiting
    [INFO] Image b0fef2b01060: Waiting
    [INFO] Image b261692ea796: Waiting
    [INFO] Image 4c1e173cea35: Waiting
    [INFO] Image 47d5ef013a61: Downloading
    [INFO] Image 9f9b3d6a536d: Downloading
    [INFO] Image 9f9b3d6a536d: Verifying Checksum
    [INFO] Image 9f9b3d6a536d: Download complete
    [INFO] Image 33847f680f63: Downloading
    [INFO] Image 47d5ef013a61: Verifying Checksum
    [INFO] Image 47d5ef013a61: Download complete
    [INFO] Image b0fef2b01060: Downloading
    [INFO] Image e64cf0645cca: Downloading
    [INFO] Image e64cf0645cca: Verifying Checksum
    [INFO] Image e64cf0645cca: Download complete
    [INFO] Image 33847f680f63: Verifying Checksum
    [INFO] Image 33847f680f63: Download complete
    [INFO] Image 33847f680f63: Extracting
    [INFO] Image 4c1e173cea35: Downloading
    [INFO] Image 33847f680f63: Pull complete
    [INFO] Image 47d5ef013a61: Extracting
    [INFO] Image 47d5ef013a61: Pull complete
    [INFO] Image 9f9b3d6a536d: Extracting
    [INFO] Image 9f9b3d6a536d: Pull complete
    [INFO] Image 4c1e173cea35: Verifying Checksum
    [INFO] Image 4c1e173cea35: Download complete
    [INFO] Image b261692ea796: Downloading
    [INFO] Image b261692ea796: Verifying Checksum
    [INFO] Image b0fef2b01060: Verifying Checksum
    [INFO] Image b0fef2b01060: Download complete
    [INFO] Image b0fef2b01060: Extracting
    [INFO] Image b0fef2b01060: Pull complete
    [INFO] Image e64cf0645cca: Extracting
    [INFO] Image e64cf0645cca: Pull complete
    [INFO] Image 4c1e173cea35: Extracting
    [INFO] Image 4c1e173cea35: Pull complete
    [INFO] Image b261692ea796: Extracting
    [INFO] Image b261692ea796: Pull complete
    [INFO] Digest: sha256:3d8247aa423017820b420452433ba7c6980c1808cadd13c0bb695dc4b092b69d
    [INFO] Status: Downloaded newer image for tomcat:jdk8-openjdk-slim
    [INFO]  ---> 50a6132601ce
    [INFO] Step 2/5 : LABEL  maintainer="[email protected]"
    [INFO]
    [INFO]  ---> Running in 1cad9a6ccdc8
    [INFO] Removing intermediate container 1cad9a6ccdc8
    [INFO]  ---> 41d8153857f1
    [INFO] Step 3/5 : ADD    ./target/hl7v3-generator##3.2.1.war /usr/local/tomcat/webapps
    [INFO]
    [INFO]  ---> 9247554b3c9f
    [INFO] Step 4/5 : EXPOSE 8080
    [INFO]
    [INFO]  ---> Running in 308e10a3532d
    [INFO] Removing intermediate container 308e10a3532d
    [INFO]  ---> 50509ae54cb1
    [INFO] Step 5/5 : CMD    [ "catalina.sh", "run" ]
    [INFO]
    [INFO]  ---> Running in 78e51a26a2a8
    [INFO] Removing intermediate container 78e51a26a2a8
    [INFO]  ---> e9be3dc5592f
    [INFO] Successfully built e9be3dc5592f
    [INFO] Successfully tagged hl7v3-generator-docker:3.2.1
    [INFO]
    [INFO] Detected build of image with id e9be3dc5592f
    [INFO] Building jar: /home/russ/dev/hl7v3-generator/target/hl7v3-generator-docker-info.jar
    [INFO] Successfully built hl7v3-generator-docker:3.2.1
    [INFO]
    [INFO] --- dockerfile-maven-plugin:1.4.13:build (default-cli) @ hl7v3-generator ---
    [INFO] dockerfile: null
    [INFO] contextDirectory: /home/russ/dev/hl7v3-generator
    [INFO] Building Docker context /home/russ/dev/hl7v3-generator
    [INFO] Path(dockerfile): null
    [INFO] Path(contextDirectory): /home/russ/dev/hl7v3-generator
    [INFO]
    [INFO] Image will be built as hl7v3-generator-docker:3.2.1
    [INFO]
    [INFO] Step 1/5 : FROM   tomcat:jdk8-openjdk-slim
    [INFO]
    [INFO] Pulling from library/tomcat
    [INFO] Digest: sha256:3d8247aa423017820b420452433ba7c6980c1808cadd13c0bb695dc4b092b69d
    [INFO] Status: Image is up to date for tomcat:jdk8-openjdk-slim
    [INFO]  ---> 50a6132601ce
    [INFO] Step 2/5 : LABEL  maintainer="[email protected]"
    [INFO]
    [INFO]  ---> Using cache
    [INFO]  ---> 41d8153857f1
    [INFO] Step 3/5 : ADD    ./target/hl7v3-generator##3.2.1.war /usr/local/tomcat/webapps
    [INFO]
    [INFO]  ---> Using cache
    [INFO]  ---> 9247554b3c9f
    [INFO] Step 4/5 : EXPOSE 8080
    [INFO]
    [INFO]  ---> Using cache
    [INFO]  ---> 50509ae54cb1
    [INFO] Step 5/5 : CMD    [ "catalina.sh", "run" ]
    [INFO]
    [INFO]  ---> Using cache
    [INFO]  ---> e9be3dc5592f
    [INFO] Successfully built e9be3dc5592f
    [INFO] Successfully tagged hl7v3-generator-docker:3.2.1
    [INFO]
    [INFO] Detected build of image with id e9be3dc5592f
    [INFO] Successfully built hl7v3-generator-docker:3.2.1
    [INFO] ------------------------------------------------------------------------
    [INFO] BUILD SUCCESS
    [INFO] ------------------------------------------------------------------------
    [INFO] Total time:  46.205 s
    [INFO] Finished at: 2021-07-27T13:33:18-06:00
    [INFO] ------------------------------------------------------------------------
    
    $ docker images
    REPOSITORY             TAG                 IMAGE ID       CREATED          SIZE
    hl7v3-generator-docker 3.2.1               e9be3dc5592f   17 minutes ago   389MB
    tomcat                 jdk8-openjdk-slim   50a6132601ce   4 days ago       309MB
    hl7v3-generator        latest              9a9cdcd50c62   2 months ago     389MB
    tomcat                 <none>              f1acc0fc1a93   2 months ago     309MB
    hello-world            latest              d1165f221234   4 months ago     13.3kB
    java                   latest              d23bdf5b1b1b   4 years ago      643MB
    
  6. I can run the container and I can talk to it as this demonstrates (so it is the web application I think it is and it works):
    $ docker run -d -p 6666:8080 hl7v3-generator-docker:3.2.1
    b356b7d199a5a7d5bdaa7da56065ea125d188dfe4367e7e75a56efa2b255076f
    $ curl localhost:6666/hl7v3-generator -# | head
    #=#=-  #     #
    <?xml version="1.0" encoding="UTF-8"?>
    <hl7v3-generator>
      <status>
        HL7v3 Generator is up.
      </status>
      <manifest>
                Manifest-Version: 1.0
        Implementation-Title: hl7v3-generator
      Implementation-Version: 3.2.1
         Specification-Title: hl7v3-generator
    $ docker container ls
    CONTAINER ID   IMAGE                        COMMAND             CREATED         STATUS         PORTS                    NAMES
    b356b7d199a5   hl7v3-generator-docker:3.2.1 "catalina.sh run"   3 minutes ago   Up 3 minutes   0.0.0.0:6666->8080/tcp   goofy_mendeleev
    
  7. Let's stop the container:
    $ docker stop b356b7d199a5
    b356b7d199a5
    
  8. I took time to begin writing this experience up.

Creating an artifact to deliver...

Now I need to investigate:

  1. I seem only to be able to export this container by using its container id and not its name (unlinke the formal example in the Docker documentation).
    $ ls *.tar
    $ docker export --output="hl7v3-generator-docker-3.2.1.tar" b356b7d199a5
    -rw-------  1 russ russ 477239296 Jul 27 14:29 hl7v3-generator-docker-3.2.1.tar
    
  2. However, I can save the container using its name though it results in a slightly different tar-file size.
    $ ls *.tar
    $ docker save hl7v3-generator-docker:3.2.1 > hl7v3-generator-docker-3.1.2.tar
    $ ls *.tar
    -rw-rw-r--  1 russ russ 393878016 Jul 27 14:34 hl7v3-generator-docker-3.1.2.tar
    
  3. The above appears to be the only artifact Docker is capable of creating outside an artifactory. This is really because a Docker image is a layering of filesystems one atop the other.

Tracking changing versions (as Docker image tags)

I changed Dockerfile:

ADD  ./target/hl7v3-generator##3.2.1.war /usr/local/tomcat/webapps
to
ADD  ./target/hl7v3-generator##*.war /usr/local/tomcat/webapps
in order not to need to intervene by hand when the WAR's version changes. This leaves Dockerfile very simply this:
FROM   tomcat:jdk8-openjdk-slim
LABEL  maintainer="[email protected]"
ADD    target/hl7v3-generator##*.war /usr/local/tomcat/webapps
EXPOSE 8080
CMD    [ "catalina.sh", "run" ]

A working implementation

To create the Docker image (in your local Docker repository):

The way I configured dockerfile-maven-plugi, leaving off dockerfile:build doesn't also leave off Maven building the image and adding it to the local repository. Therefore, that part of the command featured above as optional, makes no difference in the outcome.

Final pom.xml sections...

The Spotify dockerfile-maven-plugin:

<plugin>
  <groupId>com.spotify</groupId>
  <artifactId>dockerfile-maven-plugin</artifactId>
  <version>1.4.13</version>
  <executions>
    <execution>
      <id>default</id>
      <goals>
        <goal>build</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <repository>mdht-restlet-docker</repository>
    <tag>${project.version}</tag>
    <verbose>true</verbose>
  </configuration>
</plugin>

The Code Haus exec-maven-plugin:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>3.0.0</version>
  <executions>
    <execution>
      <id>Generate Docker artifact</id>
      <phase>build</phase>
      <goals>
        <goal>exec</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <executable>docker</executable>
    <workingDirectory>${project.basedir}</workingDirectory>
    <arguments>
      <argument>save</argument>
      <argument>--output</argument>
      <argument>mdht-restlet-docker-${project.version}.tar</argument>
      <argument>mdht-restlet-docker:${project.version}</argument>
    </arguments>
  </configuration>
</plugin>

Appendices: what's left?

I can't get exec-maven-plugin to cooperate by invoking docker save (which it does above) as well as invoking gzip -c to create the tarball I want. This is because the plug-in does not (no longer?) accept multiple executions, each with its own configuration. I hoped to add this:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>3.0.0</version>
  <executions>
    <execution>
      <id>Generate Docker artifact</id>
      <phase>build</phase>
      <goals>
        <goal>exec</goal>
      </goals>
      <configuration>
        <executable>docker</executable>
        <workingDirectory>${project.basedir}</workingDirectory>
        <arguments>
          <argument>save</argument>
          <argument>--output</argument>
          <argument>mdht-restlet-docker-${project.version}.tar</argument>
          <argument>mdht-restlet-docker:${project.version}</argument>
        </arguments>
      </configuration>
    </execution>
    <execution>
      <id>Make Docker artifact a tarball</id>
      <phase>build</phase>
      <goals>
        <goal>exec</goal>
      </goals>
      <configuration>
        <executable>gzip</executable>
        <workingDirectory>${project.basedir}</workingDirectory>
        <arguments>
          <argument>-c</argument>
          <argument>mdht-restlet-docker-${project.version}.tar</argument>
          <argument> > </argument>
          <argument>mdht-restlet-docker:${project.version}.tgz</argument>
        </arguments>
      </configuration>
    </execution>
  </executions>
</plugin>

I will have to look into this.

Appendices: other issues, scratch pad, etc...

dockerfile-maven-plugin

.
.
.
[INFO] --- maven-help-plugin:3.2.0:describe (default-cli) @ hl7v3-generator ---
[INFO] com.spotify:dockerfile-maven-plugin:1.4.13

Name: Dockerfile Maven Plugin
Description: Adds support for building Dockerfiles in Maven
Group Id: com.spotify
Artifact Id: dockerfile-maven-plugin
Version: 1.4.13
Goal Prefix: dockerfile

This plugin has 4 goals:

dockerfile:build
  Description: (no description available)

dockerfile:help
  Description: Display help information on dockerfile-maven-plugin.
    Call mvn dockerfile:help -Ddetail=true -Dgoal= to display
    parameter details.

dockerfile:push
  Description: (no description available)

dockerfile:tag
  Description: (no description available)

Got exec-maven-plugin to work...

...performing a "console" command. It echoes "Hello World!" See this phrase below:

[INFO]
[INFO] --- maven-clean-plugin:2.5:clean (default-clean) @ hl7v3-generator ---
[INFO]
[INFO] --- exec-maven-plugin:3.0.0:exec (default-cli) @ hl7v3-generator ---
Hello World!
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  0.603 s
[INFO] Finished at: 2021-07-27T17:30:09-06:00
[INFO] ------------------------------------------------------------------------

Here is how this plug-in is configured in pom.xml:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>${exec-maven-plugin.version}</version>
  <executions>
    <execution>
      <id>Echo stuff</id>
      <phase>build</phase>
      <goals>
        <goal>exec</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <executable>echo</executable>
    <workingDirectory>${project.basedir}</workingDirectory>
    <arguments>
      <argument>Hello</argument>
      <argument>World!</argument>
    </arguments>
  </configuration>
</plugin>

...with this Maven command line:

$ mvn exec:exec

...and also in the right place (at the end) with this command line:

$ mvn clean -DskipTests=true package dockerfile:build exec:exe

Explanation:

  1. exec:exec
  2. The first exec is this plug-ins magic prefix.
  3. The second exec is the goal (exec) in the execution.

Conclusion?

It appears impossible to use exec-maven-plugin to issue a Docker command such as (because of the redirection):

$ docker save hl7v3-generator-docker:3.2.1 > hl7v3-generator-docker-3.1.2.tar

...however, the docker save command has a better option (--output).

Indeed, Maven (or exec-maven-plugin) has trouble with the docker save command line when redirection is used. So, this form of docker save works instead:

<plugin>
  <groupId>org.codehaus.mojo</groupId>
  <artifactId>exec-maven-plugin</artifactId>
  <version>${exec-maven-plugin.version}</version>
  <executions>
    <execution>
      <id>Generate Docker artifact</id>
      <phase>build</phase>
      <goals>
        <goal>exec</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <executable>docker</executable>
    <workingDirectory>${project.basedir}</workingDirectory>
    <arguments>
      <argument>save</argument>
      <argument>--output</argument>
      <argument>hl7v3-generator-docker-${project.version}.tar</argument>
      <argument>hl7v3-generator-docker:${project.version}</argument>
    </arguments>
  </configuration>
</plugin>

Useful links

Google search string: "maven building docker image"

Something curious I have run into before...

As I searched for help, I noted again that when you learn Docker, or learn to do something in Docker, you have to filter out misleading information from the dubious yet enthusiastic excitement of a small crowd of enthusiasts eager to set up Docker container-based development.

These communities are so entertained by Docker's possibilities that they want to live inside Docker to do everything, development in particular. It's sort of like have a big, beautiful house while insisting on sleeping in a tent pitched on the living room floor—just because they can.

I prefer a development environment where I can extend my wings without fear of touching the walls or being otherwise cramped (having to live in a limited container).