Dockerize Spring Boot <80MB
Tags: java, docker, alpine, jlink
Published: 2019-08-02 22:19:27

I wanted to find out how much I can reduce the size of a Docker image with a simple Spring Boot application. 

# Alpine Attempt

Therefore let's use a [base image](https://hub.docker.com/_/alpine) with [Alpine Linux](https://alpinelinux.org/).

The Dockerfile:

    # use Alpine Linux for build stage
    FROM alpine:3.10.1 as build
    
    # install build dependencies
    RUN apk --no-cache add openjdk11
    RUN apk --no-cache add maven
    ...
   
This will fetch the lightweight Alpine Linux image (~6MB) from Dockerhub and install OpenJDK 11 and Maven as build dependencies. Afterwards we can build the Spring Boot application:

    ...
    # fetch maven dependencies
    WORKDIR /build
    COPY pom.xml pom.xml
    RUN mvn dependency:go-offline
    
    # build
    COPY src src
    RUN mvn clean package
    ...

As you can see we do that in two steps. At first we download all dependencies and afterwards we compile the code. If we build the image again without changes in the pom.xml file the result will be taken from cache. This will save some time as the code changes much more frequently than the dependencies.

Finally we create a second Docker image that will only contain the necessary parts for execution. Docker calls this feature "multistage" where the first stages are preliminary build steps and the last stage results in the execution image:

    ...
    # prepare a fresh Alpine Linux with JDK
    FROM alpine:3.10.1
    RUN apk --no-cache add openjdk11
    
    # get result from build stage
    COPY --from=build /build/target/*.jar /app.jar
    VOLUME /tmp
    EXPOSE 8080
    CMD /usr/lib/jvm/default-jvm/bin/java -jar /app.jar

We use the `--from=build` argument to copy the build result from the first stage. All other parts will be dropped: Maven, Java byte code, dependencies, ...

How big is the result?

    $ docker build -t lazy . && docker image ls | grep lazy
    ...
    lazy    latest    5f63318a3a0b    11 seconds ago    288MB

# Jlink Attempt

I thought 288MB is too much. I remembered with Java 9 the standard library was split into modules. There is the jlink tool to build a JDK with only the necessary modules. We add the jlink execution just before the `mvn` calls:

    ...
    # build JDK with less modules
    RUN /usr/lib/jvm/default-jvm/bin/jlink \
        --compress=2 \
        --module-path /usr/lib/jvm/default-jvm/jmods \
        --add-modules java.base,java.logging,java.xml,jdk.unsupported, \
            java.sql,java.naming,java.desktop,java.management, \
            java.security.jgss,java.instrument \
        --output /jdk-minimal
    
    # fetch maven dependencies
    ...

Additionally we have to copy the minimal JDK into the execution stage:

    # prepare a fresh Alpine Linux with JDK
    FROM alpine:3.10.1
    
    # get result from build stage
    COPY --from=build /jdk-minimal /opt/jdk/
    COPY --from=build /build/target/*.jar /app.jar
    VOLUME /tmp
    EXPOSE 8080
    CMD /opt/jdk/bin/java -jar /app.jar

Does that reduce the size?

    $ docker build -t lazy . && docker image ls | grep lazy
    ...
    lazy    latest    fbdc64bb6826    20 seconds ago    79.5MB

# Summary

We removed ~200MB of unnecessary JDK modules. I like that. Maybe the size is even smaller if we use the [Native Image feature of GraalVM](https://www.graalvm.org/docs/reference-manual/aot-compilation/)?

Result: 79.5MB

- 16MB JAR file
- 55MB OpenJDK 11 "jlinked"
- 8.5MB Alpine Linux + Docker image


Source Code: https://gist.github.com/gofabian/8a0f951bc1edc88b918ce1145ccfbb03