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 with Alpine Linux.
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?
Result: 79.5MB
- 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
jlink
using 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.