Build Java Containers with Jib
Google’s Jib is a container image build tool designed specifically for Java applications. Unlike other approaches, Jib does not depend on Docker or require users to write Dockerfiles. Instead, Jib integrates directly with the Maven and Gradle build systems to create container images for Java applications. When paired with Chainguard Java Containers, these tools provide:
- improved security through minimal base images,
- faster builds through layer optimization,
- and simplified CI/CD integration without Docker daemon requirements
This tutorial will walk you through building a demo application with Maven, Jib, and Chainguard Containers.
Prerequisites
Before proceeding, you’ll need to meet the following requirements:
- Java Development Kit (JDK) 21 or later installed
- Maven 3.6+ installed
- Docker to test the containerized application
Understanding Chainguard Java Images
Chainguard provides several Java-related images optimized for different use cases. Understanding which image to use depends on your application’s requirements:
- cgr.dev/chainguard/jre: Java Runtime Environment only; for running pre-compiled Java applications
- cgr.dev/chainguard/jdk: Full Java Development Kit; use if your build process requires compilation within the container
- cgr.dev/chainguard/maven: Pre-configured with Apache Maven for build environments
- cgr.dev/chainguard/gradle: Pre-configured with Gradle for build environments
For most production applications built with Jib, the JRE image is appropriate since Jib handles the compilation outside the container.
You can verify the version of Java in a container as follows:
docker run --rm cgr.dev/chainguard/jre:latest -version
This will display the Java version information from the image:
openjdk version "25" 2025-09-16
OpenJDK Runtime Environment (build 25+-wolfi-r1)
OpenJDK 64-Bit Server VM (build 25+-wolfi-r1, mixed mode, sharing)
Note that the latest version of the JRE (currently 25) is freely available from Chainguard but access to other versions requires a subscription.
Stage 1 — Creating a Demo Java Application
Start by creating a demo Java application to demonstrate Jib’s containerization capabilities. This example uses a basic Spring Boot application, but the principles apply to any Java application.
Create a new directory for your project and navigate into it:
mkdir jib-demo
cd jib-demo
Next, create the following source code directory for your application:
mkdir -p src/main/java/com/example/demo
You can now create the main application class. The following command creates a
new DemoApplication.java
file defining a Spring Boot application with two
endpoints that you can use to verify that the containerized application works
correctly:
cat > src/main/java/com/example/demo/DemoApplication.java <<EOF
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
@RestController
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@GetMapping("/")
public String hello() {
return "Hello from Jib and Chainguard!";
}
@GetMapping("/health")
public String health() {
return "Application is healthy";
}
}
EOF
The application is bootstrapped, but still needs build details for Maven. The following command will create the pom.xml file with basic build settings for Java 21:
cat > pom.xml <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>jib-demo</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>
<properties>
<maven.compiler.release>21</maven.compiler.release>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.build.timestamp.format>yyyyMMddHHmmss</maven.build.timestamp.format>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>3.5.6</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>3.5.6</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
EOF
To run the build, run the following command:
mvn clean install
Observe the output indicating that the build was successful:
...
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 0.733 s
[INFO] Finished at: 2025-09-25T11:40:03+01:00
[INFO] ---
Now you can run the application with the following command:
java -jar target/jib-demo-1.0.0.jar
The application exposes a web server on port 8080
. You can test the application
with a curl
request from another terminal window:
curl localhost:8080
You should get output like the following:
Hello from Jib and Chainguard!
After validating that the application builds successfully and works as
expected, you can stop the server by pressing Ctrl+C
in the terminal where it’s
running.
Stage 2 — Configuring Jib with Chainguard Containers
The next step is to include and configure Jib as a plugin in the pom.xml
file.
Locate the <plugins>
section of your pom.xml
file. Add the Jib plugin
definition like this:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.6</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>dockerBuild</goal>
</goals>
</execution>
</executions>
<configuration>
<from>
<image>cgr.dev/chainguard/jre:latest</image>
<platforms>
<platform>
<os>linux</os>
<architecture>amd64</architecture>
</platform>
<platform>
<os>linux</os>
<architecture>arm64</architecture>
</platform>
</platforms>
</from>
<to>
<image>linky</image>
</to>
<container>
<ports>
<port>8080</port>
</ports>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
</container>
</configuration>
</plugin>
The configuration specifies several settings:
- Build goal: The execution goal is defined as
dockerBuild
. This will build a container image for the application and then load the built image into the local Docker instance, allowing you to immediately run and test the container. - Base (or “from”) image: Uses Chainguard’s JRE image (
cgr.dev/chainguard/jre:latest
). This example explicitly specifies botharm64
andamd64
architectures. If you don’t do this, Jib will build anamd64
image only, regardless of host architecture. - Target image: Name of the image to build (
linky
). The tag will default tolatest
if not specified. - Port: Documents that port
8080
is used for the Spring Boot application - Creation time: Uses the current timestamp
Stage 3 — Building Container Images with Jib
With Jib configured, you can now build your containerized Java application.
Because we have specified dockerBuild
as the execution goal, this step will
require a local Docker install. If you want to avoid using Docker and push the
image straight to a registry, jump to Stage 5.
Run:
mvn clean install
You’ll get output like the following:
...
[WARNING] Detected multi-platform configuration, only building image that matches the local Docker Engine's os and architecture (linux/arm64) or the first platform specified
[INFO]
[INFO]
[INFO] Container entrypoint set to [java, -cp, @/app/jib-classpath-file, com.example.demo.DemoApplication]
[INFO] Container entrypoint set to [java, -cp, @/app/jib-classpath-file, com.example.demo.DemoApplication]
[INFO]
[INFO] Built image to Docker daemon as linky
...
This command has compiled the application, built the container, and loaded it into the local Docker instance.
To verify that the image was built:
docker images linky
Which will provide details on the new image:
REPOSITORY TAG IMAGE ID CREATED SIZE
linky latest 43171844f68e 3 minutes ago 466MB
You should also scan the image for CVEs. This example uses grype, but you can use your preferred scanner:
grype linky
This should give you output similar to the following:
✔ Vulnerability DB [updated]
✔ Loaded image linky:latest
✔ Parsed image sha256:bf2e8781db85416a96815753aa5dce19f9d8
✔ Cataloged contents a25dc3225e1f655da4f3f160acf6004973061ae0a25
├── ✔ Packages [72 packages]
├── ✔ Executables [121 executables]
├── ✔ File metadata [1,335 locations]
└── ✔ File digests [1,335 files]
✔ Scanned for vulnerabilities [0 vulnerability matches]
├── by severity: 0 critical, 0 high, 0 medium, 0 low, 0 negligible
No vulnerabilities found
In this case, there were no CVEs found. Try comparing those results to images built with different base images.
Stage 4 — Testing the Containerized Application
After building your container image, test it to ensure it works correctly. Start by running the container locally:
docker run -p 8080:8080 linky
The application will start, and you’ll see Spring Boot’s startup logs. In a separate terminal, test the endpoints:
curl http://localhost:8080/
This should return:
Hello from Jib and Chainguard!
Test the health endpoint:
curl http://localhost:8080/health
Expected output:
Application is healthy
Stop the container by pressing Ctrl+C
in the terminal where it’s running.
Stage 5 — Building and pushing to a remote registry
One of the strongest points of jib is that you don’t need Docker installed to
build and distribute container images. In this step we will change pom.xml
to
push the image directly to a container registry.
This example uses ttl.sh
, which is a free-to-use Docker registry for short-lived hosting of images. You can also change the code to use your own registry such as a local one or the Docker Hub.
Replace the previous plugin definition with this one (or add this one if you skipped Stage 3):
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.6</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>build</goal>
</goals>
</execution>
</executions>
<configuration>
<from>
<image>cgr.dev/chainguard/jre:latest</image>
<platforms>
<platform>
<os>linux</os>
<architecture>amd64</architecture>
</platform>
<platform>
<os>linux</os>
<architecture>arm64</architecture>
</platform>
</platforms>
</from>
<to>
<image>ttl.sh/jib-demo-${maven.build.timestamp}:20m</image>
</to>
<container>
<ports>
<port>8080</port>
</ports>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
</container>
</configuration>
</plugin>
This contains a couple of changes:
- The execution goal is now
build
. This will push to the registry rather than the local Docker instance. - The image name now refers to the
ttl.sh
registry and includes a timestamp for uniqueness.
Run the build again:
mvn clean install
You’ll get output like this:
...
[INFO] Using base image with digest: sha256:60a6d264537fbad2f5d37a01cf6222f2f172415f686bf59afe3b1043f79323f0
[INFO]
[INFO]
[INFO] Container entrypoint set to [java, -cp, @/app/jib-classpath-file, com.example.demo.DemoApplication]
[INFO] Container entrypoint set to [java, -cp, @/app/jib-classpath-file, com.example.demo.DemoApplication]
[INFO]
[INFO] Built and pushed image as ttl.sh/jib-demo-20250925104449:20m
...
The image name is parameterised with a timestamp, so yours will look different. The image won’t be in your local Docker instance, but you can pull it as normal (remember to copy your unique image name from the output):
docker pull ttl.sh/jib-demo-20250925104449:20m
And run it as before:
docker run -p 8080:8080 ttl.sh/jib-demo-20250925104449:20m
The ttl.sh
registry is only for temporary testing of images and your image
will be deleted in 20 minutes.
This time the built image is a multi-platform image – if you pull the image
from an amd64
host you will get the amd64
version and if you pull from an
arm64
host you will get the arm64
version. You can also force the platform
when pulling:
docker pull --platform amd64 ttl.sh/jib-demo-20250925104449:20m
Other Features
Building a tar archive
If you want a local copy of the image as a file that can be later loaded by a container runtime or saved to a file server, you can instead build the image as a tar archive.
This doesn’t work for multiplatform images unfortunately, so to test you will need to remove one of the platforms in the from definition. (Or alternatively, pull the architecture setting into a parameter.)
The following plugin definition will build a tarball for the amd64
platform:
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.6</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>buildTar</goal>
</goals>
</execution>
</executions>
<configuration>
<from>
<image>cgr.dev/chainguard/jre:latest</image>
<platforms>
<platform>
<os>linux</os>
<architecture>amd64</architecture>
</platform>
</platforms>
</from>
<to>
<image>linky</image>
</to>
<container>
<ports>
<port>8080</port>
</ports>
<creationTime>USE_CURRENT_TIMESTAMP</creationTime>
</container>
</configuration>
</plugin>
After replacing the plugin definition, run:
mvn clean install
And the output should give you the location of the built tarball:
...
[INFO]
[INFO] Built image tarball at /Users/amouat/proj/jib-demo/target/jib-image.tar
...
You can directly load this tarball into Docker:
docker load < target/jib-image.tar
This should respond with the name of the built image:
Loaded image: linky:latest
If required, the image can then be retagged and pushed to a registry.
Calling goals directly
In these examples, you’ve been editing the pom.xml to configure the execution goal. You can also directly call these from Maven, for example:
mvn install jib:dockerBuild
For full details of Jib plugin configuration and features, see the guide on GitHub.
Next Steps
You’ve now successfully containerized a Java application using Jib and Chainguard’s minimal container images. You have seen how Jib and Chainguard combine to provide a streamlined path to building secure, minimal container images for Java applications.
To continue learning about container security and optimization for the Java ecosystem, consider exploring:
-
Chainguard Libraries for Java: provides enhanced security for the Java ecosystem by rebuilding popular Maven dependencies with the latest patches and comprehensive supply chain protection.
-
How to Migrate a Java Application to Chainguard Images: in this video, learn how to migrate Java applications to Chainguard Containers for reduced vulnerabilities, smaller images, and comprehensive JDK/JRE support with daily security updates.
By combining Jib’s build optimization with Chainguard’s security-focused images, you’ve established a foundation for building secure, efficient container images as part of your Java development workflow.
Quick Nav
- Prerequisites
- Understanding Chainguard Java Images
- Stage 1 — Creating a Demo Java Application
- Stage 2 — Configuring Jib with Chainguard Containers
- Stage 3 — Building Container Images with Jib
- Stage 4 — Testing the Containerized Application
- Stage 5 — Building and pushing to a remote registry
- Other Features
- Next Steps