Comparing Modern-Day Container Image Builders: Jib, Buildpacks, and Docker

Comparing Modern-Day Container Image Builders: Jib, Buildpacks, and Docker

This article will cover and compare various methods through which you can containerize your Java applications.

Originally published on the SUSE & Rancher Community

Introduction

Docker has gained immense popularity since its debut in 2013. Many enterprises rely on Docker to containerize their applications. Many things work in favor of Docker .i.e. cost-effectiveness, provides isolation, consistent environment, and perfect for microservices use cases, which makes it the go-to for creating containers. However, in the tech world, nothing is permanent and there's always an alternative. Choices often arise to fix the shortcomings of a tool. So what are Docker's shortcomings? I'd include needing root access and dependency on the daemon process, to name a two.

No hatred against Docker. But every technology has its use case. Suppose your use case is relatively simple and you're not interested in learning new technology to containerize your Java application. In that case, there are better alternatives than Docker. These tools have emerged over the past few years because of developers' hardships while containerizing their applications. Containerizing Java applications is not a trivial task. There is a learning curve involved as you have to get yourself familiar with tool-specific commands etc. Don't expect Java developers to be container experts as well.

In this blog post, we'll discuss Docker and alternatives worthy of consideration for containerizing your Java applications from scratch. These tools will allow you to get a containerized application with minimal configurations and setup. You might not even need to install Docker or deal with Dockerfile. By the end of this article, you'll be able to decide which container image builder is the fastest, consumes the least amount of resources, easiest to set up, and best for your use case.

Background

In this article, we will do a quick walkthrough and cover the core concepts of the following container image builder tools.

  1. Jib
  2. Buildpacks
  3. Docker

Comparison Framework

We'll use the following criteria and characteristics to analyze container image builder tools.

  1. Ease of use
  2. Resource consumption (memory, disk space, etc.)
  3. Time taken for image building (first time build vs. subsequent build)
  4. Image layers optimization

Prerequisites

If you would like to follow along, then make sure you have the following:

  1. Rancher for dev if you plan to deploy and run the images created
  2. DockerHub Account
  3. Docker for Desktop
  4. OpenJDK11
  5. IDE of your choice
  6. Spring Boot project with version 2.3 or higher

Anatomy of the Spring Boot Application

The Spring Boot application that we're using for this comparison is a simple Hello World application. We have exposed a REST endpoint /hello when accessed locally through curl or REST client like Postman will return string message "Hello World!!!". The source code of the Spring Boot application is available here. In this article, we have purposefully trimmed down the command output to reduce the verbosity of the logs.

Alternative One: Jib

Jib is a Java containerizer from Google that lets Java developers build containers using the build tools like Maven and Gradle.

The big deal with Jib is that you don't need to know anything about installing Docker or maintaining a Dockerfile. Jib is deamonless. Moreover, as a developer, you only care about the artifact (jar, war, etc.) you will produce, and you don't have to deal with any of the Docker nonsense (build/push, etc.). A Java developer just needs to add a plugin to the build tool of their choice (Maven/Gradle), and that's it. You don't have to go through lots of tutorials to learn technology like Docker to containerize your Java application.

Building an Image with Jib

Before building an image, you need to add Jib support for your Spring Boot application. We can enable this support just by adding Maven or Gradle plugin to your pom.xml or build.gradle file. Getting started with Jib is straightforward.

For Maven Spring Boot Project, add the following to your pom.xml file:

<project>
  ...
  <build>
    <plugins>
      ...
      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>2.7.1</version>
        <configuration>
          <to>
            <image>docker.io/my-docker-id/my-app</image>
          </to>
        </configuration>
      </plugin>
      ...
    </plugins>
  </build>
  ...
</project>

For Gradle Spring Boot Project add the following to your build.gradle file:

plugins {
  id 'com.google.cloud.tools.jib' version '2.7.1'
}
jib.to.image = 'my-docker-id/my-app'

Furthermore, the layers created by Jib are built on top of a distroless base image. By default, Jib uses distroless Java 8 image, but you can choose an image of your choice. For this demo, we will focus on containerizing the application with the Maven Jib plugin. To push the image to the container registry of your choice, you would need to add registry credentials to maven settings.xml. You should also check this post on how to do the configuration for different container registry. For example, we can use the following for pushing the image to the DockerHub container registry.

<server>
    <id>registry.hub.docker.com</id>
    <username>username</username>
    <password>password</password>
</server>

We can start building the image with the following command:

mvn compile jib:build

It will compile, build and then push your application's image to the configured container registry. Following is the output.

time mvn compile jib:build
..........
[INFO] Containerizing application to registry.hub.docker.com/hiashish/spring-boot-jib...
[WARNING] Base image 'gcr.io/distroless/java:11' does not use a specific image digest - build may not be reproducible
[INFO] Using credentials from Maven settings file for registry.hub.docker.com/hiashish/spring-boot-jib
[INFO] Using base image with digest: sha256:449c1c57fac9560ee06cd50f8a3beeb9b8cc22f1ed128f068457f7607bcfcac6
[INFO] 
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, com.compare.imagebuilder.Application]
[INFO] 
[INFO] Built and pushed image as registry.hub.docker.com/hiashish/spring-boot-jib
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  31.183 s
[INFO] Finished at: 2021-02-19T16:51:35+05:30
[INFO] ------------------------------------------------------------------------
mvn compile jib:build -DskipTests  20.81s user 1.78s system 68% cpu 33.032 total

Jib can also create and store an image with a local docker daemon using the following command. However, we will have to push the image manually using the docker push command.

mvn compile jib:dockerBuild

Following is the output:

.......
[INFO] Containerizing application to Docker daemon as hiashish/spring-boot-jib...
[WARNING] Base image 'gcr.io/distroless/java:11' does not use a specific image digest - build may not be reproducible
[INFO] Using base image with digest: sha256:449c1c57fac9560ee06cd50f8a3beeb9b8cc22f1ed128f068457f7607bcfcac6
[INFO] 
[INFO] Container entrypoint set to [java, -cp, /app/resources:/app/classes:/app/libs/*, com.compare.imagebuilder.Application]
[INFO] 
[INFO] Built image to Docker daemon as hiashish/spring-boot-jib
[INFO] Executing tasks:
[INFO] [==============================] 100.0% complete
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  21.340 s
[INFO] Finished at: 2021-02-19T18:48:41+05:30
[INFO] ------------------------------------------------------------------------
mvn compile jib:dockerBuild  15.90s user 2.57s system 78% cpu 23.568 total

Image size:

REPOSITORY                 TAG       IMAGE ID       CREATED        SIZE
hiashish/spring-boot-jib   latest    eacedad2d476   51 years ago   214MB

Following is the Memory utilization using docker stats after running our Spring Boot Hello-World application as a Docker container.

CONTAINER ID   NAME                CPU %     MEM USAGE / LIMIT  
de0358ed9920   epic_varahamihira   1.04%     116.8MiB / 1.944GiB

Alternative Two: Buildpacks

Cloud Native Buildpacks transform your application source code into images that can run on any cloud.

Buildpacks was first conceived by Heroku in 2011 but is now part of the CNCF foundation. Just like Jib, Buildpacks can also work without the need for a Dockerfile, but you will need a docker daemon process to do its magic. With Buildpack, the input is your application source code, and the output is the container image. It's quite similar to Jib in this part, but Jib can do it without docker daemon. In the background, Buildpack does a lot of work like retrieving dependencies, processing assets, handling caching and compiling code for whatever language your app is built in.

Building an Image with Buildpacks

Since version 2.3, Spring Boot includes direct Buildpack support for both Maven and Gradle. A single command can give you a sensible image into your locally running Docker daemon. Buildpack requires a docker daemon to be up and running. If you don't have the docker daemon running, you will get the following error while executing the Maven command.

Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:2.4.2:build-image (default-cli) on project imagebuilder: Execution default-cli of goal org.springframework.boot:spring-boot-maven-plugin:2.4.2:build-image failed: Connection to the Docker daemon at 'localhost' failed with error "[61] Connection refused"; ensure the Docker daemon is running and accessible

For Maven Spring Boot Project, run the build with the following command:

mvn spring-boot:build-image

For Gradle Spring Boot Project, run the build with the following command:

gradle bootBuildImage

Buildpacks default behavior with Spring Boot is to store the image locally to docker daemon. However, you can push your images to the remote Container registry also. For that to work, we would need to make the following changes in Maven pom.xml file.

<project>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <image>
                        <name>docker.example.com/library/${project.artifactId}</name>
                        <publish>true</publish>
                    </image>
                    <docker>
                        <publishRegistry>
                            <username>user</username>
                            <password>secret</password>
                            <url>https://docker.example.com/v1/</url>
                            <email>user@example.com</email>
                        </publishRegistry>
                    </docker>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

First, we will try to build and publish the image to the local Docker daemon. Let's start the build with mvn spring-boot:build-image Maven Build command. Following is the output.

[INFO]     [creator]     Adding layer 'paketo-buildpacks/ca-certificates:helper'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:helper'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:jre'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:jvmkill'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/executable-jar:class-path'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/spring-boot:helper'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/spring-boot:web-application-type'
[INFO]     [creator]     Adding 5/5 app layer(s)
[INFO]     [creator]     Adding layer 'launcher'
[INFO]     [creator]     Adding layer 'config'
[INFO]     [creator]     Adding layer 'process-types'
[INFO]     [creator]     Adding label 'io.buildpacks.lifecycle.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.build.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.project.metadata'
[INFO]     [creator]     Adding label 'org.opencontainers.image.title'
[INFO]     [creator]     Adding label 'org.opencontainers.image.version'
[INFO]     [creator]     Adding label 'org.springframework.boot.spring-configuration-metadata.json'
[INFO]     [creator]     Adding label 'org.springframework.boot.version'
[INFO]     [creator]     Setting default process type 'web'
[INFO]     [creator]     *** Images (20569cdcf777):
[INFO]     [creator]           docker.io/library/buildpack:0.0.1-SNAPSHOT
[INFO] 
[INFO] Successfully built image 'docker.io/library/buildpack:0.0.1-SNAPSHOT'
[INFO] 
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  01:49 min
[INFO] Finished at: 2021-02-20T01:07:08+05:30
[INFO] ------------------------------------------------------------------------
mvn compile spring-boot:build-image -DskipTests  19.33s user 1.40s system 18% cpu 1:51.21 total

Now we will build and push the image to the remote container registry. Run the mvn spring-boot:build-image command.

[INFO]     [creator]     Adding label 'io.buildpacks.build.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.project.metadata'
[INFO]     [creator]     Adding label 'org.opencontainers.image.title'
[INFO]     [creator]     Adding label 'org.opencontainers.image.version'
[INFO]     [creator]     Adding label 'org.springframework.boot.spring-configuration-metadata.json'
[INFO]     [creator]     Adding label 'org.springframework.boot.version'
[INFO]     [creator]     Setting default process type 'web'
[INFO]     [creator]     *** Images (dc8e2a8dc2e2):
[INFO]     [creator]           registry.hub.docker.com/hiashish/buildpack:latest
[INFO] 
[INFO] Successfully built image 'registry.hub.docker.com/hiashish/buildpack:latest'
[INFO] 
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 0%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 9%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 15%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 18%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 18%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 18%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 32%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 38%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 40%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 45%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 45%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 55%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 60%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 61%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 62%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 67%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 77%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 79%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 89%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 90%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 91%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 92%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 94%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 95%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 95%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 95%
[INFO]  > Pushing image 'registry.hub.docker.com/hiashish/buildpack:latest' 100%
[INFO]  > Pushed image 'registry.hub.docker.com/hiashish/buildpack:latest'
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:58 min
[INFO] Finished at: 2021-02-20T01:19:26+05:30
[INFO] ------------------------------------------------------------------------
mvn compile spring-boot:build-image -DskipTests  19.33s user 1.38s system 11% cpu 3:00.29 total

Image size:

REPOSITORY               TAG              IMAGE ID       CREATED        SIZE
hiashish/buildpack      0.0.1-SNAPSHOT   20569cdcf777   41 years ago   258MB

Following is the memory utilization using docker stats after running our Spring Boot Hello World application as a Docker container.

CONTAINER ID   NAME              CPU %     MEM USAGE / LIMIT
a05ee6e6a07b   strange_goodall   0.54%     121.8MiB / 1.944GiB

Alternative Three: Docker

Docker is the de facto standard for creating containerized applications. Docker depends on a daemon process that must run to service all of your Docker commands. Docker CLI fires the command to docker daemon, and it does the needful (.i.e, pushing/pulling images, running containers, etc.) Docker uses a file called Dockerfile, written by you with steps and instructions that Docker understands. This Dockerfile is then used to create a container image of your application with a command like docker build. The advantage here is that it allows a different customization level while making a container image of your application as per your needs.

Naive way of building an image with Docker

To build an image with Docker, we need to add few instructions to our Dockerfile. Those instructions act as input, and then the Docker daemon process creates an image with those instructions. The downside of this approach is that we need to create an artifact using mvn clean package command so that Docker can copy the latest version of the jar. This will add time to the image creation process. In this approach, we have not considered image layering optimization with techniques such as multi-stage build. We just wanted a containerized version of our application, but this is inefficient as we have used a large base image of our application. Following is the time is taken for creating the artifact using the maven package command.

[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  8.734 s
[INFO] Finished at: 2021-02-18T22:07:40+05:30
[INFO] ------------------------------------------------------------------------

Here is a Dockerfile for our Spring Boot Hello-World application.

# Rookie way of writing Dockerfile
FROM openjdk:11 

COPY target/*.jar app.jar 

ENTRYPOINT ["java","-jar","/app.jar"]
  1. FROM instruction indicates base image for our application
  2. COPY instruction, as the name suggests, will copy the local jar built by Maven into our image
  3. Entrypoint instruction act as an executable for our container while starting

Ok, Let's get started then. Following is the output of executing this Dockerfile with docker build command.

time docker build -t hiashish/imagebuilder:latest .
[+] Building 50.8s (8/8) FINISHED                                                                                                                                                                           
 => [internal] load build definition from Dockerfile                                                                                                                                                   0.1s
 => => transferring dockerfile: 121B                                                                                                                                                                   0.1s
 => [internal] load .dockerignore                                                                                                                                                                      0.0s
 => => transferring context: 2B                                                                                                                                                                        0.0s
 => [internal] load metadata for docker.io/library/openjdk:11                                                                                                                                          3.3s
 => [auth] library/openjdk:pull token for registry-1.docker.io                                                                                                                                         0.0s
 => [internal] load build context                                                                                                                                                                      2.3s
 => => transferring context: 17.04MB                                                                                                                                                                   2.3s
 => [1/2] FROM docker.io/library/openjdk:11@sha256:3805f5303af58ebfee1d2f5cd5a897e97409e48398144afc2233f7b778337017                                                                                   46.7s
 => => resolve docker.io/library/openjdk:11@sha256:3805f5303af58ebfee1d2f5cd5a897e97409e48398144afc2233f7b778337017                                                                                    0.0s
 => => sha256:0ecb575e629cd60aa802266a3bc6847dcf4073aa2a6d7d43f717dd61e7b90e0b 50.40MB / 50.40MB                                                                                                       0.0s
 => => sha256:feab2c490a3cea21cc051ff29c33cc9857418edfa1be9966124b18abe1d5ae16 10.00MB / 10.00MB                                                                                                       0.0s
 => => sha256:f15a0f46f8c38f4ca7daecf160ba9cdb3ddeafda769e2741e179851cfaa14eec 51.83MB / 51.83MB                                                                                                       0.0s
 => => sha256:26cb1dfcbebb21160622ee663cb64f65e625a9f3d98c55b9555e21e2cb15e400 5.29MB / 5.29MB                                                                                                         0.0s
 => => sha256:242c5446d23fd9e18b2a08efc86e19bcf271b95038f7a2a58f4819fb362dee36 208B / 208B                                                                                                             0.0s
 => => sha256:3805f5303af58ebfee1d2f5cd5a897e97409e48398144afc2233f7b778337017 1.04kB / 1.04kB                                                                                                         0.0s
 => => sha256:2d17e02b6902d28c8546b2a1feff7e4a1fd74c703339bca6ae1c45584b9a0b67 1.79kB / 1.79kB                                                                                                         0.0s
 => => sha256:82e02728b3fd3958c5ca23fb86b9f06ba2e4bb834c0e456fe2d278a932923d53 6.27kB / 6.27kB                                                                                                         0.0s
 => => sha256:7467d1831b6947c294d92ee957902c3cd448b17c5ac2103ca5e79d15afb317c3 7.83MB / 7.83MB                                                                                                         0.0s
 => => sha256:f22708c7c9c1856c05e56ae8f5812a24b7304cb80ebfc9a34dd4f4cbaf3dd6d2 202.80MB / 202.80MB                                                                                                     0.0s
 => => extracting sha256:0ecb575e629cd60aa802266a3bc6847dcf4073aa2a6d7d43f717dd61e7b90e0b                                                                                                              9.3s
 => => extracting sha256:7467d1831b6947c294d92ee957902c3cd448b17c5ac2103ca5e79d15afb317c3                                                                                                              1.1s
 => => extracting sha256:feab2c490a3cea21cc051ff29c33cc9857418edfa1be9966124b18abe1d5ae16                                                                                                              1.2s
 => => extracting sha256:f15a0f46f8c38f4ca7daecf160ba9cdb3ddeafda769e2741e179851cfaa14eec                                                                                                              9.5s
 => => extracting sha256:26cb1dfcbebb21160622ee663cb64f65e625a9f3d98c55b9555e21e2cb15e400                                                                                                              0.7s
 => => extracting sha256:242c5446d23fd9e18b2a08efc86e19bcf271b95038f7a2a58f4819fb362dee36                                                                                                              0.0s
 => => extracting sha256:f22708c7c9c1856c05e56ae8f5812a24b7304cb80ebfc9a34dd4f4cbaf3dd6d2                                                                                                             20.6s
 => [2/2] COPY target/*.jar app.jar                                                                                                                                                                    0.2s
 => exporting to image                                                                                                                                                                                 0.3s
 => => exporting layers                                                                                                                                                                                0.2s
 => => writing image sha256:6704ddf7df3398be458722ea1c4d8c17393dc656a1a8ae89152f99c5462b0306                                                                                                           0.0s
 => => naming to docker.io/hiashish/imagebuilder:latest                                                                                                                                                0.0s
docker build -t hiashish/imagebuilder:latest .  0.82s user 0.64s system 2% cpu 51.572 total

Next, we have published the image using docker push command.

time docker push hiashish/imagebuilder             
Using default tag: latest
The push refers to repository [docker.io/hiashish/imagebuilder]
baebe8d2c101: Pushed 
ebab439b6c1b: Mounted from hiashish/helloworld 
c44cd007351c: Mounted from hiashish/helloworld 
02f0a7f763a3: Mounted from hiashish/helloworld 
da654bc8bc80: Mounted from hiashish/helloworld 
4ef81dc52d99: Mounted from hiashish/helloworld 
909e93c71745: Mounted from hiashish/helloworld 
7f03bfe4d6dc: Mounted from hiashish/helloworld 
latest: digest: sha256:166c00c1ba605a360e4555405f57d2f5b93ec7abc979ecb4a43de7c9366639d0 size: 2006
docker push hiashish/imagebuilder  0.26s user 0.38s system 1% cpu 32.655 total

Image size

REPOSITORY                           TAG           IMAGE ID       CREATED         SIZE
hiashish/dockersimple                latest        3cdf4936ec77   20 hours ago    664MB

To check memory utilization, we will use the docker stats command. Following is the output after running our Spring Boot Hello-Wolrd application Docker container.

CONTAINER ID   NAME              CPU %     MEM USAGE / LIMIT
f8f34e0ffaa4   magical_lamport       0.89%     138.7MiB / 1.944GiB

Pro way of building an image with Docker Multistage Build

In the previous section, our process was a bit tedious, and the size of the output image was large. We can improve this process and create slim images with Docker multistage builds. Another problem with the previous method was having to package our application first using mvn package command before creating the actual image. We will try to avoid that in this section.

Here is a Dockerfile for our Spring Boot Hello-World application which uses a multi-stage build. Let me explain the Dockerfile. I have used the maven-openjdk11 image to build and create a jar for my project in the first stage, but this will not be the final output. In the second stage, I copied the jar we made in the previous build stage and created a new final image based on a significantly smaller Java 11 JRE base image. The final Docker image does not include the JDK or maven but only the JRE. If you have noticed, the build time is higher here because all required dependencies need to be downloaded during the first stage of the build. I guess that is the only downside with this approach.

# Pro way of writing Dockerfile
FROM maven:3-openjdk-11 as build
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN mvn clean package -DskipTests

FROM openjdk:11-jre-slim
RUN mkdir /project
COPY --from=build /app/target/imagebuilder-0.0.1-SNAPSHOT.jar /project
WORKDIR /project
ENTRYPOINT ["java","-jar","imagebuilder-0.0.1-SNAPSHOT.jar"]

Following is the output of executing this Dockerfile with docker build command.

time docker build -t hiashish/multistagebuild:latest .
[+] Building 213.4s (17/17) FINISHED                                                                                                                                                                        
 => [internal] load build definition from Dockerfile                                                                                                                                                   0.1s
 => => transferring dockerfile: 503B                                                                                                                                                                   0.0s
 => [internal] load .dockerignore                                                                                                                                                                      0.0s
 => => transferring context: 2B                                                                                                                                                                        0.0s
 => [internal] load metadata for docker.io/library/openjdk:11-jre-slim                                                                                                                                 2.7s
 => [internal] load metadata for docker.io/library/maven:3-openjdk-11                                                                                                                                  3.0s
 => [auth] library/maven:pull token for registry-1.docker.io                                                                                                                                           0.0s
 => [auth] library/openjdk:pull token for registry-1.docker.io                                                                                                                                         0.0s
 => [build 1/5] FROM docker.io/library/maven:3-openjdk-11@sha256:7376ab3120aeb575cd6cd336a423c6c8f62f26d9fb1ddc2659a299c3433a2fa2                                                                     35.8s
 => => resolve docker.io/library/maven:3-openjdk-11@sha256:7376ab3120aeb575cd6cd336a423c6c8f62f26d9fb1ddc2659a299c3433a2fa2                                                                            0.0s
 => => sha256:1539a43fb9fbebf79ac9e96adfae8dada3b6caa0ab0e6586e837dae4b8eabc3c 355B / 355B                                                                                                             0.0s
 => => sha256:7376ab3120aeb575cd6cd336a423c6c8f62f26d9fb1ddc2659a299c3433a2fa2 549B / 549B                                                                                                             0.0s
 => => sha256:12b1cd95e6bf321d358f4a8484584acc1354728c4c6f82f0568a2a3fdfbb0f34 2.42kB / 2.42kB                                                                                                         0.0s
 => => sha256:feab2c490a3cea21cc051ff29c33cc9857418edfa1be9966124b18abe1d5ae16 10.00MB / 10.00MB                                                                                                       0.0s
 => => sha256:f15a0f46f8c38f4ca7daecf160ba9cdb3ddeafda769e2741e179851cfaa14eec 51.83MB / 51.83MB                                                                                                       0.0s
 => => sha256:26cb1dfcbebb21160622ee663cb64f65e625a9f3d98c55b9555e21e2cb15e400 5.29MB / 5.29MB                                                                                                         0.0s
 => => sha256:f22708c7c9c1856c05e56ae8f5812a24b7304cb80ebfc9a34dd4f4cbaf3dd6d2 202.80MB / 202.80MB                                                                                                     0.0s
 => => sha256:8749bb0dc5132a22fbd2b2b23d4179568f3a758dd0c53dc2b87048656d19b8d7 852B / 852B                                                                                                             0.0s
 => => sha256:8de5407a7bebd1d5f75cc154bf8c74535dde31e10b8baa60347d04a7b3f338c5 8.92kB / 8.92kB                                                                                                         0.0s
 => => sha256:0ecb575e629cd60aa802266a3bc6847dcf4073aa2a6d7d43f717dd61e7b90e0b 50.40MB / 50.40MB                                                                                                       0.0s
 => => sha256:7467d1831b6947c294d92ee957902c3cd448b17c5ac2103ca5e79d15afb317c3 7.83MB / 7.83MB                                                                                                         0.0s
 => => sha256:242c5446d23fd9e18b2a08efc86e19bcf271b95038f7a2a58f4819fb362dee36 208B / 208B                                                                                                             0.0s
 => => sha256:b6129cf0dd9130eddbcd914da7d0808d038216f3ea3e599da8e3d3b80d7ce427 9.58MB / 9.58MB                                                                                                         0.0s
 => => extracting sha256:0ecb575e629cd60aa802266a3bc6847dcf4073aa2a6d7d43f717dd61e7b90e0b                                                                                                             10.0s
 => => extracting sha256:7467d1831b6947c294d92ee957902c3cd448b17c5ac2103ca5e79d15afb317c3                                                                                                              1.0s
 => => extracting sha256:feab2c490a3cea21cc051ff29c33cc9857418edfa1be9966124b18abe1d5ae16                                                                                                              1.0s
 => => extracting sha256:f15a0f46f8c38f4ca7daecf160ba9cdb3ddeafda769e2741e179851cfaa14eec                                                                                                              7.7s
 => => extracting sha256:26cb1dfcbebb21160622ee663cb64f65e625a9f3d98c55b9555e21e2cb15e400                                                                                                              0.5s
 => => extracting sha256:242c5446d23fd9e18b2a08efc86e19bcf271b95038f7a2a58f4819fb362dee36                                                                                                              0.0s
 => => extracting sha256:f22708c7c9c1856c05e56ae8f5812a24b7304cb80ebfc9a34dd4f4cbaf3dd6d2                                                                                                             11.4s
 => => extracting sha256:b6129cf0dd9130eddbcd914da7d0808d038216f3ea3e599da8e3d3b80d7ce427                                                                                                              0.5s
 => => extracting sha256:8749bb0dc5132a22fbd2b2b23d4179568f3a758dd0c53dc2b87048656d19b8d7                                                                                                              0.0s
 => => extracting sha256:1539a43fb9fbebf79ac9e96adfae8dada3b6caa0ab0e6586e837dae4b8eabc3c                                                                                                              0.0s
 => [stage-1 1/4] FROM docker.io/library/openjdk:11-jre-slim@sha256:34744515bdde22d92a627555165e72b4e3e7aca2094cc025cb0f4000058a3c1c                                                                  16.0s
 => => resolve docker.io/library/openjdk:11-jre-slim@sha256:34744515bdde22d92a627555165e72b4e3e7aca2094cc025cb0f4000058a3c1c                                                                           0.0s
 => => sha256:33964aae81418d20eee9035f0a64d71bded866a765423048f456d6835b1e2a3d 7.58kB / 7.58kB                                                                                                         0.0s
 => => sha256:45b42c59be334ecda0daaa139b2f7d310e45c564c5f12263b1b8e68ec9e810ed 27.10MB / 27.10MB                                                                                                       0.0s
 => => sha256:a91c0c19c84860aaa974864243509770a5b009f3a88b4a228010a9ade71ac968 3.27MB / 3.27MB                                                                                                         0.0s
 => => sha256:dbe61a45ef1807a5db8d1f61021955a6ee0c370a88bb9c568a2ae31417afbda6 209B / 209B                                                                                                             0.0s
 => => sha256:6eeb16e47bf12a88503349a2056e84ea8b6e1329850c1ad859e2d01401f45a1f 47.04MB / 47.04MB                                                                                                       0.0s
 => => sha256:34744515bdde22d92a627555165e72b4e3e7aca2094cc025cb0f4000058a3c1c 549B / 549B                                                                                                             0.0s
 => => sha256:1317197ccd52971d38949536ba5a27ad61de3bf78ef792033622a4694eb0c373 1.16kB / 1.16kB                                                                                                         0.0s
 => => extracting sha256:45b42c59be334ecda0daaa139b2f7d310e45c564c5f12263b1b8e68ec9e810ed                                                                                                              6.8s
 => => extracting sha256:a91c0c19c84860aaa974864243509770a5b009f3a88b4a228010a9ade71ac968                                                                                                              0.8s
 => => extracting sha256:dbe61a45ef1807a5db8d1f61021955a6ee0c370a88bb9c568a2ae31417afbda6                                                                                                              0.0s
 => => extracting sha256:6eeb16e47bf12a88503349a2056e84ea8b6e1329850c1ad859e2d01401f45a1f                                                                                                              5.9s
 => [internal] load build context                                                                                                                                                                      2.4s
 => => transferring context: 17.20MB                                                                                                                                                                   2.3s
 => [stage-1 2/4] RUN mkdir /project                                                                                                                                                                   1.2s
 => [build 2/5] RUN mkdir /app                                                                                                                                                                         0.6s
 => [build 3/5] COPY . /app                                                                                                                                                                            0.2s
 => [build 4/5] WORKDIR /app                                                                                                                                                                           0.1s
 => [build 5/5] RUN mvn clean package -DskipTests                                                                                                                                                    172.8s
 => [stage-1 3/4] COPY --from=build /app/target/imagebuilder-0.0.1-SNAPSHOT.jar /project                                                                                                               0.1s 
 => [stage-1 4/4] WORKDIR /project                                                                                                                                                                     0.1s 
 => exporting to image                                                                                                                                                                                 0.2s 
 => => exporting layers                                                                                                                                                                                0.2s 
 => => writing image sha256:079ca9681f614fe441730f4baaf3494841fd672fa814cfd7baeb52097e7c83d5                                                                                                           0.0s 
 => => naming to docker.io/hiashish/multistagebuild:latest                                      
docker build -t hiashish/multistagebuild:latest .  4.26s user 3.04s system 3% cpu 3:33.89 total

Next, we have published the image using docker push command.

time docker push hiashish/multistagebuild                
Using default tag: latest
The push refers to repository [docker.io/hiashish/multistagebuild]
5f70bf18a086: Mounted from paketobuildpacks/builder 
df8f2c4d6677: Pushed 
657f5a5c28df: Pushed 
027810cd859b: Mounted from library/openjdk 
513adf10febc: Mounted from library/openjdk 
08664b16f94c: Mounted from library/openjdk 
9eb82f04c782: Mounted from library/openjdk 
latest: digest: sha256:91dca4aebfda20cd93b7fb29d946ab694bdcc2b9351a44de6d2a16822b689f18 size: 1784
docker push hiashish/multistagebuild  0.20s user 0.28s system 1% cpu 30.667 total

Image size using Docker multistage build.

REPOSITORY                 TAG       IMAGE ID       CREATED          SIZE
hiashish/multistagebuild   latest    079ca9681f61   13 seconds ago   237MB

Following is the Memory utilization after running our Spring Boot Hello-World application Docker container.

CONTAINER ID   NAME        CPU %     MEM USAGE / LIMIT 
7b64ea0aabf2   keen_bohr   0.80%     139.8MiB / 1.944GiB

Data comparison

Comparing Image layers

This section will use Dive to inspect image layers created using the containerization methods we have covered in this article. Dive is intelligent enough to figure out if you are wasting any space, and if you are, you can discover ways to shrink the size of your Docker/OCI image.

  1. Docker simple
  1. Docker multistage
  1. Jib
  1. Buildpack

We have rated the offerings based on how they fulfill each of the criteria in the following section.

Criteria One: Ease of use

Both Jib and Spring Boot Buildpack methods are straightforward. It is easy to get started with them by just adding the plugin. They are baked into the Spring Boot ecosystem. They do provide some sensible defaults to work with, and it's up to to you to what level of customization you are expecting. However, these two options may not support all features that Dockerfile does, such as using something like the RUN equivalent of Dockerfile instruction with Jib or Buildpack. Docker requires separate installation, and you have to get familiar with the commands that it supports. As we saw earlier, we also have to follow some Docker best practices to create a slimmer image for our application. However, Jib provides (by default) distroless base images, which are lightweight and more secure as they don't contain package managers that come with typical Linux distributions.

Criteria Two: Image Creation Time

As you can see in the data comparison table, Jib is the most performant option when comparing the image creation time. When we used Jib without the Docker daemon, it created the image and pushed it to a remote container registry with just ~ 33 seconds in the first run of the build, and subsequent builds were even faster. None of the other options we explored comes close.

Criteria Three: Resource Utilization

Spring Boot Jib support provides some sensible defaults, and its distroless base image is much smaller than other options, resulting in the final smaller image. Docker is not that thin compared to Jib, even with a multistage build with a slim base image. The image created by Buildpack support for Spring Boot is large compared to Jib and Docker's multi-stage build. Finally, Jib consumes the least amount of memory.

Criteria Four: Image layers

In the comparing image layers section above, you can see that Docker's simple and multistage build images have clubbed dependencies, resources, and classes into a single inefficient layer. In addition, there is wasted space, with room for improvement. With Jib, we have exploded directory structure for classes, resources, and dependencies, and their size is minimal. This approach's advantage is that only your changes are rebuilt when you do any code changes, not your entire application. This means Jib only pushes the layer, which is changed and the rest of the layers remain the same. With Buildpack, we have more or less the same layering compared to Jib, but there is some space getting wasted, too, as the image efficiency score is 99%. One more issue with Buildpack is that it clubs resources together, making the layering inefficient.

Conclusion

In this article, we covered various container image-building methods for Java developers, including Jib, Buildpacks, and Docker. We compared the image building method on multiple parameters such as image creation time (build and push), resource utilization (disk space and memory), and how easy it is to get started with each tool. With the data we've presented, you can see that Jib is an efficient method for creating container images for your application. Buildpacks are a solid option. However, they are sometimes slower than Jib and do not allow you to change the parent image. Finally, if you are looking for better control or customization, then Docker maybe your best option. That said, Jib has evolved so much since its inception that you can practically do anything you can with Docker using its extension framework.

Did you find this article valuable?

Support Ashish Choudhary's Blog by becoming a sponsor. Any amount is appreciated!