02 June 2021

Summary

Containerization of Java applications is as easy as copying the application's Jar file into a JRE equipped base image. But choosing the right base image can be hard and have a big impact on performance, effectiveness of resource utilization, security and costs. This Blog Post discusses the trend to Distroless base images.

What is Distroless?

If you’re looking around for an appropriate base image to package your application, you may stumble into the term distroless image. This term was coined by Google, which describes it as

Distroless images contain only your 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.
— Google

The described approach is best practice and basically means to restrict what’s going into your runtime container to precisely what’s necessary for the application.

Although, distroless images are rather a general principal, Google is still the major supplier of such images. Google provides images for different languages like

  • Java

  • Python

  • JavaScript/NodeJS

  • Golang

Technically, the distroless base images contain mainly the language platform components, i.e. in case of Java the JDK. Because the JDK is dynamically linked to system libraries like GlibC, these libraries are also contained in the image.

The list of supported languages even includes Go. This may be surprising a little bit at the first glance, because Go applications are usually linked statically and can be packaged into images FROM SCRATCH, i.e. into an entirely empty image. Actually, the distroless base image just contains some small pieces like a truststore of CA root certificates and does not increase the size of the image notably.

Sample Application

Before discussing the benefits of applications packaged with distroless images, let’s have a look at the containerization of a sample Java application. I’ll use the Jakarta MVC demo application of one of the last Blog posts, which is packaged as Wildfly Bootable JAR, i.e. as a Single Fat Jar.

The following Dockerfile just copies the application’s Jar file into the image, which is based on Google’s distroless image for Java 11:

FROM gcr.io/distroless/java:11

COPY target/mvc-demo-bootable.jar /app/main.jar
WORKDIR /app
CMD ["main.jar"]

Because the java command is defined as entrypoint of the image, the application’s Jar can be provided as command. The regular docker build command creates the image:

$ docker build -t mvc-app .
Sending build context to Docker daemon  204.7MB
Step 1/4 : FROM gcr.io/distroless/java:11
 ---> 6395a77cb03c
Step 2/4 : COPY target/mvc-demo-bootable.jar /app/main.jar
 ---> 2209115d4185
Step 3/4 : WORKDIR /app
 ---> Running in 5faa7537ec9b
Removing intermediate container 5faa7537ec9b
 ---> a81720f1f285
Step 4/4 : CMD ["main.jar"]
 ---> Running in 944c9a473721
Removing intermediate container 944c9a473721
 ---> d21a6c599d58
Successfully built d21a6c599d58
Successfully tagged mvc-app:latest

This containerized application can be executed on Linux by:

$ docker run --sysctl net.ipv4.ip_forward=1 --network=host -p 8080:8080 --rm -it mvc-app:latest
WARNING: Published ports are discarded when using host network mode
05:26:38,437 INFO  [org.wildfly.jar] (main) WFLYJAR0007: Installed server and application in /tmp/wildfly-bootable-server5397933122926229342, took 932ms
05:26:38,684 INFO  [org.wildfly.jar] (main) WFLYJAR0008: Server options: [--read-only-server-config=standalone.xml]
05:26:38,806 INFO  [org.jboss.msc] (main) JBoss MSC version 1.4.12.Final
05:26:38,814 INFO  [org.jboss.threads] (main) JBoss Threads version 2.4.0.Final
05:26:38,979 INFO  [org.jboss.as] (MSC service thread 1-3) WFLYSRV0049: WildFly Full 22.0.1.Final (WildFly Core 14.0.1.Final) starting
05:26:39,967 INFO  [org.jboss.as.jaxrs] (ServerService Thread Pool -- 15) WFLYRS0016: RESTEasy version 3.14.0.Final
05:26:39,988 INFO  [org.wildfly.extension.undertow] (MSC service thread 1-6) WFLYUT0003: Undertow 2.2.4.Final starting
...
05:26:43,418 INFO  [org.jboss.as.server] (Controller Boot Thread) WFLYSRV0010: Deployed "mvc-demo.war" (runtime-name : "ROOT.war")
05:26:43,449 INFO  [org.jboss.as.server] (Controller Boot Thread) WFLYSRV0212: Resuming server
05:26:43,451 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: WildFly Full 22.0.1.Final (WildFly Core 14.0.1.Final) started in 4761ms - Started 144 of 149 services (23 services are lazy, passive or on-demand)

Beside the simple and straightforward image build, the generated images are surprisingly small compared to Java application images built on top of distribution based images:

$ docker images
REPOSITORY           TAG              IMAGE ID            CREATED             SIZE
mvc-app              latest           d21a6c599d58        4 minutes ago       265MB
...

From my experience, the size of application images based on a Linux distribution is about 200 MB larger - the only exception of this are Alpine Linux based images, but these have other drawbacks…​

Why Distroless?

Distroless best resembles the origins of application containers, i.e. packaging applications with its required runtime components in order to

  • isolate applications in the best possible way

  • allow resource management like CPU and memory quotas on a per application basis

At the same time it refutes the widespread misconception that containers are a replacement for VMs.

The main attribute of distroless image applications is their small size. And size actually matters because of:

Performance

Images are copied, transmitted and launched by fleet managers like Kubernetes. In addition, fitting more containers into one machine means less machine spawns.

Security

Improved security by minimizing the attack surface, because everything in your container, e.g. shells, not used by your application can still be used by attackers.

Money

Fitting more containers into one machine (AKA worker node in Kubernetes) reduces the bill from your cloud provider.

There are situations, in which a shell access to an application’s container may be convenient. For such situations Google offers a debug version of their distroless containers, which includes a busybox in addition. But usually containerized applications should go without a shell. This may be not possible for legacy applications brought to the cloud applying a Lift-and-Shift approach. But cloud-native applications should be designed and implemented in a way, that shells and other components of a Linux distribution are not required.

Tags: cloud-native docker wildfly java container