My natural laziness is the perfect driver for automation efforts. This little tweak streamlines the development lifecycle. If you build something with Docker and Apache Maven, you could find this article useful.

When I started a new proof of concept, I decide that do something with Docker could be educational and fun at the same time. I started with one container that runs a couple JAX-RS services in the container. For one project, it worked just fine. Later I've decided to add another one with the JSP application. Now I had to build two Maven projects and two containers to run simple tests. At this time, I have discovered Docker Composer.  It creates multi-container applications and provides better ways to configure, build, and run container images as a single application. At this moment, I converted two separate projects into a multi-module Maven application, so I can compile and test my code with a single maven command, yet I have to run additional commands to build and test containerized app. It wasn't good enough for me because I want to:

  • Use a single tool for all operations
  • Build containers only when all modules are packaged successfully.
  • Run JUnit tests, rebuild container images and test application with the same tool

After series of trials and errors, I reached all my goals when following the sage advice: "If you want to do something with Maven - create a new module." the diagram below shows the current project structure.

Image depicts Apache Maven projects structure with the separate module for container operations.
Multi-module Maven project

The main POM is pretty standard and contains all must-have artifacts and three modules:

  • application services container,
  • web application container
  • container build module.  (should be the last one)

The first modules are war applications, and they know how to build the appropriate image, but only the last one knows how to compose the application and work with the image registry.

Essentially, the last module has only two artifacts:

  • The Docker Compose build descriptor with relative references to application containers.
  • Maven module descriptor with prescriptions how to test, package,  and run container application.

The Apache Maven document below is self-explanatory. It defines executable actions for different goals. For example, on mvn build command, Maven will execute the shell command docker-compose build from the module folder after building all the other modules. To actually run the application on my local Docker engine, I implemented an integration-test phase.

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>com.example</groupId>
    <artifactId>oauth.jsp</artifactId>
    <version>1.0-SNAPSHOT</version>
    <relativePath>../pom.xml</relativePath>
  </parent>
  <groupId>com.example</groupId>
  <artifactId>build.composer</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>pom</packaging>

  <name>Package Docker Containers</name>
  <url>https://example.com/ojsp.app</url>
  
  <build>
    <plugins>
      <plugin>
          <artifactId>exec-maven-plugin</artifactId>
          <version>3.0.0</version>
          <groupId>org.codehaus.mojo</groupId>
          <executions>
            <execution>
              <id>Build Containers</id>
              <phase>package</phase>
              <configuration>
                     <executable>docker-compose</executable>
                     <commandlineArgs>build</commandlineArgs>
                 </configuration>
              <goals>
                <goal>exec</goal>
              </goals>
            </execution>
              <id>Test Configuration</id>
              <phase>test</phase>
              <configuration>
                     <executable>docker-compose</executable>
                     <commandlineArgs>config</commandlineArgs>
                 </configuration>
              <goals>
                <goal>exec</goal>
              </goals>
            </execution>              
            <execution>
              <id>Run Containers</id>
              <phase>integration-test</phase>
              <configuration>
                     <executable>docker-compose</executable>
                     <commandlineArgs>up --detach</commandlineArgs>
                 </configuration>
              <goals>
                <goal>exec</goal>
              </goals>
            </execution>
            <execution>
              <id>Registry Login</id>
              <phase>install</phase>
              <configuration>
                     <executable>docker</executable>
                     <commandlineArgs>login registry.gitlab.com</commandlineArgs>
                 </configuration>
              <goals>
                <goal>exec</goal>
              </goals>
            </execution>
            <execution>
              <id>Install Containers</id>
              <phase>install</phase>
              <configuration>
                     <executable>docker-compose</executable>
                     <commandlineArgs>push</commandlineArgs>
                 </configuration>
              <goals>
                <goal>exec</goal>
              </goals>
            </execution>
          </executions>
        </plugin>
    </plugins>
</build>
</project>

Now I can use a single command to build, test, and run my composite container application.

[composite-app]$ mvn integration-test