I wanted to find out how much I can reduce the size of a Docker image with a simple Spring Boot application.
Therefore let's use a base image with Alpine Linux.
# 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
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
... # 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
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?
- 16MB JAR file
- 55MB OpenJDK 11 "jlinked"
- 8.5MB Alpine Linux + Docker image
Source Code: https://gist.github.com/gofabian/8a0f951bc1edc88b918ce1145ccfbb03
Top comments (2)
This is awesome. Thank you! I saw some articles around
jlinkusing debian, but this alpine image makes it even smaller!
Still Java is a fat animal. I am looking forward to Spring Boot 2.4 (in October/November?) and its GraalVM native image support. Using that will reduce the size a lot.