×
Community Blog Common Tricks for Minimizing Java Images

Common Tricks for Minimizing Java Images

This article describes some common tricks for minimizing Java images by taking a Spring Boot-based Java application as an example.

By Bruce Wu

Background

With the popularization of the container technology, more and more applications are container-based. Containers are used frequently, but most container users may ignore a simple but important problem - the size of container images. This article briefly describes the necessity of simplifying container images, and shows you some common tricks for minimizing Java images by taking a Spring Boot-based Java application as an example.

The Necessity of Simplifying Container Images

Simplifying container images is very necessary. This will be explained in terms of both security and agility.

Security

Removing unnecessary components from the image can reduce the attack surface and security risks. Docker allows you to limit operations within your container by using Seccomp, and configure security policies for the container by using AppArmor. However, you must have sufficient proficiency in the security field to use them.

Agility

A simplified container image improves the deployment speed of the container. Assume that the access traffic suddenly bursts, and you need to increase the number of containers to address the suddenly increased pressure. If some hosts do not contain the target image, you need to first pull the image and then start the container. In this case, smaller images can speed up the process and shorten the period for scaling up. In addition, smaller images can be built faster and can save the storage and transmission costs.

Common Tricks

You can perform the following steps to containerize a Java application:

  1. Compile the Java source code and generate a JAR package.
  2. Move the JAR package and third-party JAR dependencies to an appropriate position.

The example used in this section is spring-boot-docker, a Spring Boot-based Java application. The unoptimized dockerfile file used in this example is as follows:

FROM maven:3.5-jdk-8
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package
ENTRYPOINT ["java","-jar","/usr/src/app/target/spring-boot-docker-1.0.0.jar"]

The application was created by using Maven, and maven:3.5-jdk-8 is specified as the base image in the dockerfile. The size of this image is 635 MB. The size of the final image created through this method is 719 MB, which is quite large. The reasons are that the base image is large, and Maven downloads many JAR packages to build the final image.

Multi-Stage Builds

To run a Java application, you need only the Java Runtime Environment (JRE). You do not need Maven or any compiling, debugging, or running tools of the Java Development Kit (JDK). Therefore, a straight forward optimization method is to separate the image that compiles and creates the Java source code from the image that runs the Java application. To do this, you need to maintain two dockerfile files before the release of Docker 17.05, which increases the complexity of image building. Starting from Docker 17.05, the Multi-stage builds feature allows you to use multiple FROM statements in one dockerfile. Each FROM statement can specify different base images and start a completely new image-building process. You can choose to copy the product of a previous image-building stage to another stage, and keep only the necessary content in the final image. The optimized dockerfile file is as follows:

FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

The dockerfile uses maven:3.5-jdk-8 as the build image in the first stage, and openjdk:8-jre as the base image to run the Java application. Only the .class file that was compiled in the first stage is copied to the final image together with third-party JAR dependencies. The size of the image is reduced to 459 MB after the optimization.

Use a Distroless Image as the Base Image

Although multistage builds do have reduced the size of the final image, 459 MB is still too large. Through comprehensive analysis, we find that the size of the base image openjdk:8-jre is 443 MB, which is too large. Therefore, the next step of optimization is to reduce the size of the base image.

Distroless, an open source project of Google, was developed to solve this problem. Distroless images contain only the application and its runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution. Currently, Distroless provides base images for applications running in environments such as Java, Python, Node.js and .NET.

The dockerfile file that uses a distroless image is as follows:

FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM gcr.io/distroless/java
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

The only difference between this dockerfile and the previous one is that the base image for running the application is changed from openjdk:8-jre (443 MB) to gcr.io/distroless/java (119 MB). As a result, the size of the final image becomes 135 MB.

The only inconvenience of using a distroless image is that the image does not contain shell. You cannot use docker attach to attach the standard input, output, and error (or any combination of the three) of your application to a running container for debugging. debug image of distroless provides a busybox shell. But you have to repackage the image and deploy the container, which is not helpful for containers that have been deployed based on non-debug images. From a security point of view, this could be an advantage because attackers cannot attack through shells.

Use an Alpine Image as the Base Image

If you do need to use docker attach and hope to minimize the image size, you can use an alpine image as the base image. Alpine images are characterized by their unbelievably small size, and the base image is only about 4 MB in size.

The dockerfile file that uses an alpine image is as follows:

FROM maven:3.5-jdk-8 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
RUN mvn -f /usr/src/app/pom.xml clean package

FROM openjdk:8-jre-alpine
ARG DEPENDENCY=/usr/src/app/target/dependency
COPY --from=build ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=build ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=build ${DEPENDENCY}/BOOT-INF/classes /app
ENTRYPOINT ["java","-cp","app:app/lib/*","hello.Application"]

Instead of directly using the base alpine image, we chose openjdk:8-jre-alpine (83MB) as the base image. openjdk:8-jre-alpine was built based on alpine and contains the Java runtime. The size of the image built with this dockerfile is 99.2 MB, which is smaller than the one that is built based on a distroless image.

Run the command docker exec -ti <container_id> sh to attach to a running container.

Distroless vs Alpine

Distroless and alpine images can both provide very small base images. Which one should we use in the production environment? If security is your primary concern, distroless is recommended because your packaged application is the only binary file that it can run. If you are more concerned about the size of the image, you can go with alpine.

Other Tricks

In addition to the aforementioned tricks, you can perform the following operations to further simplify the image size:

  1. Combine multiple instructions in the dockerfile into one. This reduces the number of layers in your image, and reduces the image size.
  2. Place stable and large content to the lower layer of your image, and place frequently changing and small content to the upper layer. This method can not directly reduce the image size. But it makes full use of the image caching mechanism to speed up the image building and container deployment.

For more tips on optimizing Dockerfiles, see Best practices for writing Dockerfiles.

Summary

  1. Through a series of optimization, the image size of the Java application is reduced from 719 MB to about 100 MB. If your application runs in other environments, you can also optimize it with similar principles.
  2. For Java images jib, another tool provided by Google, can automatically handle the complex image building process and provide you with a simplified Java image. With it, you do not have to write dockerfiles, and you do not even need to install Docker.
  3. For containers such as distro-less that are inconvenient for debugging, you can centrally store their logs for easier tracing and troubleshooting of problems. For more information, see the article Technical best practices for container log processing.
0 0 0
Share on

Alibaba Cloud Storage

57 posts | 12 followers

You may also like

Comments