August 24, 2018

Building Java applications using Maven and Docker (multi-stage builds)

How to produce minimal Docker image which runs Java application built using Maven?

Building Docker image

Docker provides feature of multi-stage builds which allows dramatically reduce the size of resulting image and keep Dockerfile simple and maintainable.

This feature will be used to build example Java application.

Java application

The example application uses Functional Java as example of external library to slightly complicate building process:

package com.scalabledeveloper.multistagebuild;

import static fj.data.List.list;
import fj.data.List;
import static fj.Show.intShow;
import static fj.Show.listShow;

public class App {
    public static void main(String[] args) {
        System.out.println("Functional Java!");

        final List<Integer> a = list(1, 2, 3, 4, 5).map(i -> i * 2);
        listShow(intShow).println(a);
        // List(2,4,6,8,10)
    }
}

Maven

Goal is to build executable .jar file which could used to run application:

<?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>

    <groupId>com.scalabledeveloper</groupId>
    <artifactId>multistagebuild</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <maven.compiler.target>1.8</maven.compiler.target>
        <maven.compiler.source>1.8</maven.compiler.source>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.functionaljava</groupId>
            <artifactId>functionaljava</artifactId>
            <version>4.8</version>
        </dependency>
        <dependency>
            <groupId>org.functionaljava</groupId>
            <artifactId>functionaljava-java8</artifactId>
            <version>4.8</version>
        </dependency>
        <dependency>
            <groupId>org.functionaljava</groupId>
            <artifactId>functionaljava-quickcheck</artifactId>
            <version>4.8</version>
        </dependency>
        <dependency>
            <groupId>org.functionaljava</groupId>
            <artifactId>functionaljava-java-core</artifactId>
            <version>4.8</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <version>3.1.0</version>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
                <executions>
                    <execution>
                        <id>make-assembly</id> <!-- this is used for inheritance merges -->
                        <phase>package</phase> <!-- bind to the packaging phase -->
                        <goals>
                            <goal>single</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

Apache Maven Assembly Plugin helps to build executable .jar file.

Docker

Dockerfile uses power of multi-stage builds:

FROM maven:3.5-jdk-8-alpine as builder

COPY src /usr/src/app/src
COPY pom.xml /usr/src/app

RUN mvn -f /usr/src/app/pom.xml clean package

FROM java:8

COPY --from=builder /usr/src/app/target/multistagebuild-1.0-SNAPSHOT-jar-with-dependencies.jar /usr/app/multistagebuild-1.0-SNAPSHOT-jar-with-dependencies.jar

ENTRYPOINT ["java", "-cp", "/usr/app/multistagebuild-1.0-SNAPSHOT-jar-with-dependencies.jar", "com.scalabledeveloper.multistagebuild.App"]

Main feature is to build things and copy results of builds to target image. There could be a lot of stages. And Docker will remove intermediate results.

Building and running

Building image:

$ docker build -t multistagebuild .
Sending build context to Docker daemon  43.52kB
Step 1/7 : FROM maven:3.5-jdk-8-alpine as builder
 ---> 074bb2ef6669
Step 2/7 : COPY src /usr/src/app/src
 ---> Using cache
 ---> cec7781c9798
Step 3/7 : COPY pom.xml /usr/src/app
 ---> Using cache
 ---> 628a9ab1d6b0
Step 4/7 : RUN mvn -f /usr/src/app/pom.xml clean package
 ---> Using cache
 ---> f598cd1a66a7
Step 5/7 : FROM java:8
 ---> d23bdf5b1b1b
Step 6/7 : COPY --from=builder /usr/src/app/target/multistagebuild-1.0-SNAPSHOT-jar-with-dependencies.jar /usr/app/multistagebuild-1.0-SNAPSHOT-jar-with-dependencies.jar
 ---> 06d0e7db71db
Step 7/7 : ENTRYPOINT ["java", "-cp", "/usr/app/multistagebuild-1.0-SNAPSHOT-jar-with-dependencies.jar", "com.scalabledeveloper.multistagebuild.App"]
 ---> Running in 766a2508625c
Removing intermediate container 766a2508625c
 ---> 5783705270fc
Successfully built 5783705270fc
Successfully tagged multistagebuild:latest

Running image:

$ docker run -it multistagebuild
Functional Java!
List(2,4,6,8,10)

Summary

Using Docker multi-stage build feature size of resulting image is reduced and overall process of building image (described in Dockerfile) is easy to support and extend.