Reducing the size of a docker image with a spring boot application
Recently, we faced the task of launching spring boot 2 applications in a kubernetes cluster using a docker image. This problem is not new, quickly enough we found examples in Google and prepared our application. We were very surprised not to find the alpine image for jdk11 and hoped that slim would be small enough, but when we sent the image to the docker registry, we noticed that its size was almost 422 megabytes. Below is a description of how we reduced the docker image with our spring boot and java 11 to 144 megabytes.
Application
As we mentioned earlier, our application is built using spring boot 2 and is a REST API wrapper over a relational database (using @RepositoryRestResource). Our dependencies include:
1 2 3 4 |
org.springframework.boot:spring-boot-starter-data-rest org.springframework.boot:spring-boot-starter-data-jpa org.flywaydb:flyway-core org.postgresql:postgresql |
The assembled jar file has a size of: 37.6 megabytes.
Dockerfile:
1 2 3 4 5 |
FROM openjdk:11-jdk-slim WORKDIR /home/demo ARG REVISION COPY target/spring-boot-app-${REVISION}.jar app.jar ENTRYPOINT ["java","-jar","app.jar"] |
As a result of the assembly, we get an image of size: 422 mb according to the output of the docker images command. Interestingly, if to use the outdated 8-jdk-slim image, the size is reduced to 306 mb.
Attempt 1: Another Basic Image
The first logical step was an attempt to find a more lightweight image, preferably based on alpine. We scanned to the most popular Java repositories:
- https://hub.docker.com/_/openjdk
- https://hub.docker.com/r/adoptopenjdk/openjdk11
- https://hub.docker.com/r/adoptopenjdk/openjdk11-openj9
- https://hub.docker.com/r/adoptopenjdk/openjdk8
(11 as the current LTS release and 8 as there are still a sufficient number of applications that could not migrate to more modern versions)
A table with some images and tags (~ 2700), their sizes at the time of writing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
openjdk 8 488MB openjdk 8-slim 269MB openjdk 8-alpine 105MB openjdk 8-jdk-slim 269MB openjdk 8-jdk-alpine 105MB openjdk 8-jre 246MB openjdk 8-jre-slim 168MB openjdk 8-jre-alpine 84.9MB openjdk 11 604MB openjdk 11-slim 384MB openjdk 11-jdk 604MB openjdk 11-jdk-slim 384MB openjdk 11-jre 479MB openjdk 11-jre-slim 273MB adoptopenjdk/openjdk8 alpine 221MB adoptopenjdk/openjdk8 alpine-slim 89.7MB adoptopenjdk/openjdk8 jre 200MB adoptopenjdk/openjdk8 alpine-jre 121MB adoptopenjdk/openjdk11 alpine 337MB adoptopenjdk/openjdk11 alpine-slim 246MB adoptopenjdk/openjdk11 jre 218MB adoptopenjdk/openjdk11 alpine-jre 140MB |
Thus, if you change the base image to adoptopenjdk / openjdk11: alpine-jre, you can reduce the image with the application to 177 mb.
Attempt 2: Custom runtime
Since the moment of jdk9 release and modularization, it became possible to build your own runtime that contains only those modules that are necessary for your application.
Let’s try to determine the necessary modules for the test spring boot application:
1 2 3 4 |
~/app ᐅ jdeps -s target/app-1.0.0.jar app-1.0.0.jar -> java.base app-1.0.0.jar -> java.logging app-1.0.0.jar -> not found |
Ok, it seems that jdeps cannot handle the fat-jar created with spring boot, but we can unzip the archive and write the classpath:
1 2 3 4 |
~/app ᐅ jdeps -s -cp target/app-1.0.0/BOOT-INF/lib/*.jar target/app-1.0.0.jar.original Error: byte-buddy-1.9.12.jar is a multi-release jar file but --multi-release option is not set ~/app ᐅ jdeps -s --multi-release 11 -cp target/app-1.0.0/BOOT-INF/lib/*.jar target/app-1.0.0.jar.original Error: aspectjweaver-1.9.2.jar is not a multi-release jar file but --multi-release option is set |
About this case, is currently open a bug: https://bugs.openjdk.java.net/browse/JDK-8207162
We tried downloading jdk12 to get this information, but ran into the following error:
1 2 3 |
Exception in thread "main" com.sun.tools.classfile.Dependencies$ClassFileError ... Caused by: com.sun.tools.classfile.ConstantPool$InvalidEntry: unexpected tag at #1: 53 |
After a lot of tries and module search by ClassNotFoundException, we determined that our application needs the following modules:
- java.base
- java.logging
- java.sql
- java.naming
- java.management
- java.instrument
- java.desktop
- java.security.jgss
Runtime for them can be collected using:
1 |
jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,java.logging,java.sql,java.naming,java.management,java.instrument,java.desktop,java.security.jgss --output /usr/lib/jvm/spring-boot-runtime |
Let’s try to build a basic docker image using this modules:
1 2 3 4 5 6 7 8 |
FROM openjdk:11-jdk-slim RUN jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,java.logging,java.sql,java.naming,java.management,java.instrument,java.desktop,java.security.jgss --output /usr/lib/jvm/spring-boot-runtime FROM debian:stretch-slim COPY --from=0 /usr/lib/jvm/spring-boot-runtime /usr/lib/jvm/spring-boot-runtime RUN ln -s /usr/lib/jvm/spring-boot-runtime/bin/java /usr/bin/java |
and collect it:
1 |
docker build . -t spring-boot-runtime:openjdk-11-slim |
As a result, the size was 106 megabytes, which is much smaller than most found basic images with openjdk. If you use it for our application, then the resulting size will be 144 megabytes.
Further, we can use spring-boot-runtime: openjdk-11-slim as the base image for all spring boot applications if they have similar dependencies. In the case of various dependencies, it is possible to use a multistage image assembly for each of the applications where java runtime will be assembled in the first stage, and the archive with the application will be added in the second.
1 2 3 4 5 6 7 8 9 |
FROM openjdk:11-jdk-slim RUN jlink --no-header-files --no-man-pages --compress=2 --strip-debug --add-modules java.base,YOUR_MODULES --output /usr/lib/jvm/spring-boot-runtime FROM debian:stretch-slim COPY --from=0 /usr/lib/jvm/spring-boot-runtime /usr/lib/jvm/spring-boot-runtime WORKDIR /home/demo ARG REVISION COPY target/app-${REVISION}.jar app.jar ENTRYPOINT ["/usr/lib/jvm/spring-boot-runtime/bin/java","-jar","app.jar"] |
Summary
Currently, most docker images for java have a large enough volume, which can negatively affect the start time of the application, especially if the necessary layers are not yet on the server. Using tags with jre or java modularization, you can build your own runtime, which will significantly reduce the size of the application image.
Related Posts
Leave a Reply Cancel reply
Service
Categories
- DEVELOPMENT (102)
- DEVOPS (53)
- FRAMEWORKS (25)
- IT (24)
- QA (14)
- SECURITY (13)
- SOFTWARE (13)
- UI/UX (6)
- Uncategorized (8)