posts
How to use buildpacks to build Java containers

How to use buildpacks to build Java containers

Mar 2, 2023
Dmitry Chuyko
13.4

Do you still write lengthy Dockerfiles describing every step necessary to build a container image? Buildpacks come to your rescue! Developers simply feed them an application, buildpacks do their magic, and turn it into a fully functional container ready to be deployed on any cloud.

But how exactly does the magic happen? And what should you do if the resulting container performance doesn’t meet the business requirements? 

This article will look under the hood of buildpacks to see how they operate and give tips on optimizing the default setting to reach better performance outcomes.

What are buildpacks?

A buildpack turns the application source code into a runnable production-ready container image. Buildpacks save time and effort for developers because there’s no need to configure the image and manually manage dependencies through a Dockerfile.

Heroku was the first company to develop buildpacks in 2011. Since then, many other companies (Cloud Foundry, Google, etc.) have adopted the concept. In 2018, Heroku partnered with Pivotal to create the Cloud Native Buildpacks project, encompassing modern standards and specifications for container images, such as the OCI format. The project is part of the Cloud Native Computing Foundation (CNCF).

Paketo buildpacks, which we will use in this article, is an open-source project backed by Cloud Foundry and sponsored by VMware. It implements Cloud Native Buildpacks specifications and supports the most popular languages, including Java. Containers produced with Paketo buildpacks can run on any cloud.

How buildpacks work

Buildpacks operate in two phases: detect and build.

The detect phase

During the detect phase, the buildpack analyzes the source code looking for indicators of whether or not it should be applied to the application. In other words, a group of buildpacks is tested against the source code, and the first group deemed fit for the code is selected for building the app. After the buildpack detects the necessary indicators, it returns a contract of what is required for creating an image and proceeds to the build phase.

The build phase

During the build phase, the buildpack transforms the codebase, fulfilling the contract requirements composed earlier. It provides the build-time and runtime environment, downloads necessary dependencies, compiles the code if needed, and sets the entry points and startup scripts.

Builders

A builder is a combination of components required for building a container image:

  • Buildpacks, sets of executables that analyze the code and provide a plan for building and running the app;
  • Stack consists of two images: the build image and the run image. The build image provides the build environment (a containerized environment where buildpacks are executed), the run image offers the environment for the application image during runtime;
  • Lifecycle manages the buildpack execution and assembles the resulting artifact into a final image.

Therefore, one builder can automatically detect and build different applications.

Buildpacks offer a variety of JVMs — how to choose?

Paketo buildpacks use Liberica JVM by default. Liberica is a HotSpot-based Java runtime supported by a major OpenJDK contributor and recommended by Spring. It provides JDK and JRE for all LTS versions (8, 11, 17) the current version, and Liberica Native Image Kit (NIK), a GraalVM-based utility for converting JVM-based apps into native images with an accelerated startup. Native images are beneficial when you need to avoid cold starts in AWS.

But the buildpacks support several Java distributions, which can be used instead of the default JVM:

  • Adoptium
  • Alibaba Dragonwell
  • Amazon Corretto
  • Azul Zulu
  • BellSoft Liberica (default)
  • Eclipse OpenJ9
  • GraalVM
  • Oracle JDK
  • Microsoft OpenJDK
  • SapMachine

If you want to switch JVMs, you have to keep in mind several nuances:

  • Alibaba Dragonwell, Amazon Corretto, GraalVM, Oracle JDK, and Microsoft OpenJDK offer only JDK. The resulting container will be twice as big as the JRE-based one;
  • Adoptium provides JDK and JRE for Java 8 and 11 and only JDK for Java 16+;
  • Oracle JDK provides only Java 17.

Another important consideration: buildpacks facilitate and accelerate deployment, but if you are dissatisfied with container performance or seek to improve essential KPIs (throughput, latency, or memory consumption), you have to tune the JVM yourself. For more details, see the section Configuring the JVM below.

For instance, Eclipse OpenJ9 based on the OpenJ9 JVM may demonstrate better performance than HotSpot in some cases because HotSpot comes with default settings, and OpenJ9 is already tuned. Adding a few simple parameters will give you equal or superior performance with HotSpot. Please find more information on JVM tuning in our comparative study of Hotspot vs OpenJ9.

How to use Paketo buildpacks

Let’s build a Java container utilizing a Paketo buildpack.

First, make sure Docker is up and running. If you don’t have it, follow these instructions to install Docker Desktop for your system.

The next step is to install pack CLI, a Command Line Interface maintained by Cloud Native Buildpack that can be used to work with buildpacks. Follow the guide to complete the installation for your platform (macOS, Linux, and Windows are supported). pack is one of the several available tools. Spring Boot developers, for instance, can look into Spring Boot Maven Plugin or Spring Boot Gradle Plugin.

We will use Paketo sample applications, so run the following command:

git clone https://github.com/paketo-buildpacks/samples && cd samples

Alternatively, utilize your own demo app.

Make Paketo Base builder the default builder:

pack config default-builder paketobuildpacks/builder:base

To build an image from source with Maven, run

pack build samples/java \
 --path java/maven --env BP_JVM_VERSION=17

Java example images should return {"status":"UP"} from the actuator health endpoint:

docker run --rm --tty --publish 8080:8080 samples/java
curl -s http://localhost:8080/actuator/health | jq .

It is also possible to build an image from a compiled artifact. The following archive formats are supported: executable JAR, WAR, or distribution ZIP. 

To compile an executable JAR and build an image using pack, run

cd java/maven
./mvnw package
pack build samples/java \
   --path ./target/demo-0.0.1-SNAPSHOT.jar

Extracting a software bill of materials

Software supply chains consist of numerous libraries, tools, and processes used to develop and run applications. It is often hard to trace the origin of all software components in a software product, increasing the risk of nested vulnerabilities. A software bill of materials (SBOM) lists all library dependencies utilized to build a software artifact. It is similar to a traditional bill of materials, which summarizes the raw materials, parts, components, and exact quantities required to manufacture a product.

SBOMs enable the developers to monitor the version of software components, integrate security patches promptly, and keep vulnerable libraries out.

Buildpacks also enable the developers to see an SBOM for their image. Run the following command to extract the SBOM for the samples/java image built previously:

pack sbom download samples/java --output-dir /tmp/samples-java-sbom

After that, you can browse the folder. SBOMs are presented in JSON format. To list all .json files in the folder, run

find /tmp/samples-java-sbom -name "*.json"
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_executable-jar/sbom.cdx.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_executable-jar/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_spring-boot/helper/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_spring-boot/spring-cloud-bindings/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/jre/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/helper/sbom.syft.json
/tmp/samples-java-sbom/layers/sbom/launch/sbom.legacy.json
/tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_ca-certificates/helper/sbom.syft.json

Now, you can open the file with any text editor. For instance, if you have Visual Studio Code installed, run

code /tmp/samples-java-sbom/layers/sbom/launch/paketo-buildpacks_bellsoft-liberica/jre/sbom.syft.json

You will get the following output:

{
   "Artifacts": [
       {
           "ID": "1f2d01eeb13b5894",
           "Name": "BellSoft Liberica JRE",
           "Version": "17.0.6",
           "Type": "UnknownPackage",
           "FoundBy": "libpak",
           "Locations": [
               {
                   "Path": "buildpack.toml"
               }
           ],
           "Licenses": [
               "GPL-2.0 WITH Classpath-exception-2.0"
           ],
           "Language": "",
           "CPEs": [
               "cpe:2.3:a:oracle:jre:17.0.6:*:*:*:*:*:*:*"
           ],
           "PURL": "pkg:generic/[email protected]?arch=amd64"
       }
   ],
   "Source": {
       "Type": "directory",
       "Target": "/layers/paketo-buildpacks_bellsoft-liberica/jre"
   },
   "Descriptor": {
       "Name": "syft",
       "Version": "0.32.0"
   },
   "Schema": {
       "Version": "1.1.0",
       "URL": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
   }
}

Configuring the JVM

The BellSoft Liberica Buildpack provides the newest patch updates of Java versions supported in the buildpack. The buildpack uses the latest LTS version by default. If you want to use another Java version, use the BP_JVM_VERSION environment variable. For instance, BP_JVM_VERSION=11 will install the newest release of Liberica JDK and JRE 11.

In addition, you can change the JDK type. The buildpack uses JDK at build-time and JRE at runtime. Specifying the BP_JVM_TYPE=JDK option will force the buildpack to use JDK at runtime.

The BP_JVM_JLINK_ENABLED option runs the jlink tool with Java 9+, which cuts out a custom JRE.

If you deploy a Java application to an application server, the buildpack uses Apache Tomcat by default. You can select another server (TomEE or Open Liberty). For instance, run the following command to switch to TomEE:

pack build samples/war -e BP_JAVA_APP_SERVER=tomee

You can configure JVM at runtime by using the JAVA_TOOL_OPTIONS environment variable. For instance, you can configure garbage collection, number of threads, memory limits, etc. to reach optimal performance for your specific needs:

docker run --rm --tty \
  --env JAVA_TOOL_OPTIONS='-XX:+UseParallelGC -XX:GCTimeRatio=4 -XX:AdaptiveSizePolicyWeight=90 -XX:MinHeapFreeRatio=20 -XX:MaxHeapFreeRatio=40' \
  --env BPL_JVM_THREAD_COUNT=100 \
  samples/java

The whole list of JVM configuration options can be found on the Liberica Buildpack page.

Automate containerization or reduce the image size? Pick two! 

As you can see, buildpacks are great automation tools saving developers time. But it would help if you used them wisely, or there’s a risk you will get a cat in the sack. Our general recommendation is to define the KPIs and adjust JVM settings accordingly.

What can you do if you are not happy with the size of the resulting image? After all, it is not possible to change the base OS image with buildpacks - or is it?

BellSoft has exciting news: we developed Alpaquita buildpacks that combine the power of Paketo buildpacks and Alpaquita Linux, a 100 % Alpine-compatible distribution with two libc variants (optimized musl and glibc) and numerous security and performance enhancements. With Alpaquita buildpacks, you will get all the benefits of the technology plus a tangible footprint reduction!

Discover Alpaquita buildpacks

Subcribe to our newsletter

figure

Read the industry news, receive solutions to your problems, and find the ways to save money.

Further reading