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
Baeldung, Spring Boot Tomcat configuration
Tom Gregory, automating docker builds with gradle and Palantir plugin
linoxide.com, installing OpenJDK on Ubuntu 20.04
linuxcapable.com, OpenJDK-17 on Ubuntu 20.04