Java: build OCI compatible image for Spring Boot web app using jib

While working on your Spring Boot web application locally, gradle provides the ‘bootRun’ for a quick development lifecycle and ‘bootJar’ for packaging all the dependencies as a single jar deliverable.

But for most applications these days, you will need this packaged into an OCI compatible (i.e. Docker) image for its ultimate deployment to an orchestrator such as Kubernetes.

In this article I will go into detail on what is required to generate the OCI compatible image using Google’s jib, which in addition to being very fast also does not require a Docker daemon (for the final image).

Overview

Being able to build an OCI-compatible Docker image from Gradle is certainly not a new idea.  I have another article that describes this exactly using gradle, the Palantir plugins, and the local Docker daemon to build and push the Spring Boot image to the public Docker hub.

But what jib promises is the ability to create an OCI image quickly, and without needing a local Docker daemon.  You can see how this would be beneficial in something like a CI/CD pipeline where eliminating this dependency means the pool of build workers could be that much faster and lighter.

However, you must be aware that one of the intentional design decisions made in jib is that RUN command equivalents are not supported.  If you have RUN commands in your Dockerfile that install OS packages, add users/groups, tweak OS settings, etc. these commands have to be wrapped up as a base Docker image.  And then jib can extend that base image.

So what you must focus on is allowing jib to do what it was designed for, packaging the Java application itself into a container.  The base image that jib uses for the build can be a completely independent project built with a Docker daemon, buildah, etc.

Prerequisites

This article assumes you are on an Ubuntu development server. You will need the following packages installed.

OpenJDK 11+

# refresh package repos
sudo apt update

# show available openjdk versions
sudo apt search openjdk-* | grep -P '^openjdk-1\d-jdk/'

# pick latest (which is 17 as of this writing)
sudo apt install openjdk-17-jdk curl git -y

# validate version reported is the one just installed
java --version

Docker CE

Here is my article on installing Docker CE on Ubuntu focal.  This is only used when we build the base image, for the final application image, we will use jib.

Fetch github project code

Start by retrieving my Spring Boot REST example project.

git clone https://github.com/fabianlee/spring-boot-with-jib.git
cd spring-boot-with-jib

# compilation should be successful
./gradlew bootJar

Build Docker base image

We want to add a minimal set of OS packages (curl,jq,ps,netstat), so we can test from inside the final container.  We also want to add a non-root user (‘spring’, uid=1001) so our image can run as non-root with least privilege.

As discussed above, jib does not support the equivalent of RUN commands, we need to create a base image that has these features.  jib will then build on top of this base image.  As shown in src/main/resources/docker/Dockerfile.base, we have the following RUN command.

RUN DEBIAN_FRONTEND=noninteractive && \
  apt-get update && \
  apt-get install -q -y -o Dpkg::Options::="--force-confnew" procps curl swaks netcat net-tools jq && \
  rm -rf /var/lib/apt/lists/* && \
  /usr/sbin/groupadd -g $THE_GROUP_ID spring && \
  /usr/sbin/useradd -l -u $THE_USER_ID -G spring -g $THE_GROUP_ID spring && \
  mkdir logs && chgrp spring logs && chmod ug+rwx logs

The Palantir Docker plugin and configuration is in build.gradle.

plugins {
  ...
  id 'com.palantir.docker' version '0.33.0'
}

...
docker {
    name "${project.dockerOwner}/${project.name}-base:${project.version}"
    dockerfile file("$buildDir/resources/main/docker/Dockerfile.base")
}

Which allows us to create the base Docker image (using the local Docker daemon) with the following command.

./gradlew bootJar docker

This image is now available to the local Docker daemon

$ sudo docker images | grep springbootwithjib-base
fabianlee/springbootwithjib-base 0.0.2-SNAPSHOT 5f5fba1af4ce 2 minutes ago 473MB

# go inside container running in foreground
$ docker run -it --rm fabianlee/springbootwithjib-base:0.0.2-SNAPSHOT /bin/bash
$ exit

If you have an account on Docker Hub, then you should be able to push the image for public consumption but first you must login via the CLI.

# establish login to hub.docker.com
sudo docker login -u <userid> -p <password>

# use gradle to push image to hub.docker.com for public consumption
./gradlew dockerPush

You can validate this public image at https://hub.docker.com/repository/docker/fabianlee/springbootwithjib-base

Build app image using jib

Now we can pretend that the base image was created in some independent fashion and focus solely on having this Java Spring Boot web application deployed as an OCI image, built using jib (without the local Docker daemon).

All the settings we need will be placed into build.gradle.

plugins {
  ...
  id 'com.google.cloud.tools.jib' version '3.2.1'
}

jib {
    from {
        image = "${project.owner}/${project.name}-base:${project.version}"
    }
    to {
        image = "${project.dockerOwner}/${project.name}"
        tags = [version]
    }
    container {
        mainClass = "${group}.${project.name}.SpringMainApplication"
        ports = ['8080','8081']
        creationTime = 'USE_CURRENT_TIMESTAMP'
        user = 'spring:spring' // process run as non-root
    }
    setAllowInsecureRegistries(true)
}

Then we build the OCI image and place it into both the local Docker daemon registry and public Docker Hub.

./gradlew jib jibDockerBuild

$ sudo docker images | grep fabianlee/springbootwithjib
fabianlee/springbootwithjib                       0.0.2-SNAPSHOT         d02a10e3aa7c   3 minutes ago   499MB
fabianlee/springbootwithjib                       latest                 d02a10e3aa7c   3 minutes ago   499MB
fabianlee/springbootwithjib-base                  0.0.2-SNAPSHOT         5f5fba1af4ce   2 hours ago     473MB

Validate application

You can run this application locally in the foreground.

# run in foreground
docker run -it --rm -p 8080:8080 -p 8081:8081 fabianlee/springbootwithjib:0.0.2-SNAPSHOT

# validate curl to REST API from another console
$ curl -s http://localhost:8080/api/user
[{"name":"moe"},{"name":"larry"},{"name":"curly"}]

Or in the background and validate both the API endpoint as well as internal user and tools.

# run in background
docker run -d --rm -p 8080:8080 -p 8081:8081 --name springbootwithjib fabianlee/springbootwithjib:0.0.2-SNAPSHOT

# validate curl to REST api
$ curl -s http://localhost:8080/api/user
[{"name":"moe"},{"name":"larry"},{"name":"curly"}]

# show that app process is running as non-root 'spring' user
$ docker exec -it springbootwithjib /bin/bash -c "/bin/ps -ef"
UID PID PPID C STIME TTY TIME CMD
spring 1 0 30 19:18 ? 00:00:28 java -cp @/app/jib-classpath
spring 695 0 0 19:19 pts/0 00:00:00 /bin/ps -ef

# show that application is bound to 8080 and 8081
$ docker exec -it springbootwithjib /bin/bash -c "netstat -tulnp"
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name 
tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN 1/java 
tcp 0 0 0.0.0.0:8081 0.0.0.0:* LISTEN 1/java 
tcp 0 0 0.0.0.0:35729 0.0.0.0:* LISTEN 1/java


# stop bg container
docker stop springbootwithjib

Port 8080 is mapped locally, so pulling up a browser to http://localhost:8080/swagger-ui/index.html will pull up the OpenAPI documentation and test harness page.

I’m not going to describe the full functionality of this Spring Boot REST application, the purpose of this article was just to build an OCI compliant image using jib.  If you want more detail, you can read my article on a Spring Boot REST web app enabled with Actuator and prometheus metrics.

 

REFERENCES

jib github home

google, introducing jib

Baeldung, Spring Boot Tomcat configuration

Tom Gregory, automating docker builds with gradle and Palantir plugin

Gradle, Palantir plugins

linoxide.com, installing OpenJDK on Ubuntu 20.04

linuxcapable.com, OpenJDK-17 on Ubuntu 20.04

github, Palantir source