There are multiple Java frameworks for developing Java applications: Spring Boot, Micronaut, MicroProfile, Javalin, etc., each with their own strengths. Quarkus is another Java framework tailored to developing cloud-native Java microservices and promising to optimize memory footprint and the startup time of application.
In this article, I won’t delve into Quarkus startup study, but rather evaluate the footprint of Quarkus containers created with the default settings and via buildpacks, and see if I can optimize the size of a resulting container image even more.
Table of Contents
Overview of the Quarkus framework
Quarkus is an open-source stack of technologies with MicroProfile support aimed at optimizing Jakarta EE for building microservices. It is oriented to Kubernetes and adapted to OpenJDK HotSpot and GraalVM.
To create microservices, Quarkus implements Eclipse MicroProfile APIs along with other valuable tools, such as Apache Kafka, Camel, dependency injection, Hibernate ORM (JPA and JTA annotations), RESTEasy (JAX-RS), etc. It also provides Maven and Gradle plugins so that you can run your application in development mode on a local or remote machine.
In addition, Quarkus supports GraalVM, which lets the developers use ahead-of-time (AOT) compilation to convert the bytecode into native machine code. Another Java framework with built-in support for GraalVM Native Image is Spring Boot. With GraalVM, apps can be compiled to native executables with no need for dynamic scanning and loading all classes into a JVM. By default, all classes are initialized at build time. As a result, the application starts much faster and consumes significantly less resources.
Сompliance with Java EE standards enables developers to use existing APIs and run their applications in an optimized runtime without rewriting them. Basically, you can use enterprise APIs and extend them according to your requirements (for example, use Quarkus extensions for reactive messaging, Vert.x, or Camel).
Quarkus tutorial
Create your first Quarkus application
If you are familiar with Spring Boot, you’ve already used the Spring Initializr to quickly whip up a bare Spring Boot application with the required dependencies. Quarkus offers a similar starting point, code.quarkus.io. Let’s go there and adjust some basic settings. Choose
- Choose Java version 17 and build tool maven,
- Change the group and artifact ID to your liking or keep the defaults,
- Select the extensions: RESTEasy Classic and RESTEasy Classic's REST Client.
There are dozens of additional extensions, including those for cloud services, security, messagings, data handling and so on. But we’ll keep it simple for demo purposes.
Press Generate your application and open the project in your favorite IDE.
The application contains only one class, GreetingResource, with the following content:
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello RESTEasy";
}
}
Open the Terminal and run this command to build your application skipping the tests:
./mvnw package quarkus:dev -Dmaven.test.skip=true -Dquarkus.test.continuous-testing=disabled
The REST endpoint is exposed at localhost:8080/hello, so open the page in your browser to see your app’s “Hello RESTEasy” message.
Without stopping the app, you can now introduce changes to your project (for instance, substitute the default message with ”Hello from Quarkus”). Refresh the browser page, and you’ll see a new greeting. This is made possible by the in-build live coding functionality, thanks to which you don’t have to recompile the app and wait for several seconds for it to start every time you make changes.
How to build Quarkus container images
Containerize the Quarkus app with a Dockerfile
As Quarkus offers the “containers-first” approach, let’s see how we can containerize our application.
You can use the traditional approach and configure the container manually with a Dockerfile.
Run
./mvnw -Dmaven.test.skip package
After that, go to the docker directory of your Quarkus project. There are several ready Dockerfiles, we need the Dockerfile.jvm one. It uses Red Hat Ubi 8 and OpenJDK 17 as a base image.
Place the file to the root directory and run
docker build -f Dockerfile.jvm -t quarkus-red-hat .
Now, check the image with:
docker images
quarkus-red-hat latest 04aaf8eb792b About a minute ago 485MB
The resulting container seems to be too heavy. Can we do better?
Red Hat offers OpenJDK 17 runtime images on UBI8 without JDK tools, the compiler, or Maven. Let’s take this image and see what we get.
Substitute the FROM line in the Dockerfile with:
FROM registry.access.redhat.com/ubi8/openjdk-17-runtime:1.18
After that, run
docker build -f Dockerfile.jvm -t quarkus-red-hat-runtime .
Check the images:
docker images
quarkus-red-hat-runtime latest daf6550b5f74 14 seconds ago 444MB
That’s better. But can we optimize the footprint even more?
Return to the Dockerfile and substitute the FROM line with:
FROM bellsoft/liberica-runtime-container:jre-17-stream-musl
Here, we take advantage of Liberica Runtime Container with Liberica JRE Lite and Alpaquita Linux to create a lightweight container image with your application.
Run
docker build -f Dockerfile.jvm -t quarkus-liberica-runtime-container .
And verify the image with
docker images
quarkus-liberica-runtime-container latest 0f6b58dc07f0 4 seconds ago 135MB
As you can see, the resulting image is 3.5 times smaller compared to the first one we made!
Containerize the Quarkus app with buildpacks
There’s another approach to building container images — the buildpacks. If you are unfamiliar with the technology, refer to our previous guide. In short, buildpacks facilitate the development with automatic containerization. There’s no need to write a Dockerfile: run one command, and the buildpack will scan the project and turn it into a production-ready image.
The Quarkus project offers its own buildpack for building Java and Native Image containers. Under the hood, it uses Paketo buildpacks that utilize Liberica JDK by default, so the resulting image will be based on this OpenJDK distribution.
To containerize our app this way, we need to add the buildpack extension.
Run
./mvnw quarkus:add-extension -Dextensions='container-image-buildpack'
And then
./mvnw install -Dquarkus.container-image.build=true -Dmaven.test.skip
. . .
Adding cache layer 'paketo-buildpacks/bellsoft-liberica:jdk'
Adding cache layer 'paketo-buildpacks/syft:syft'
Adding cache layer 'paketo-buildpacks/maven:application'
Adding cache layer 'paketo-buildpacks/maven:cache'
[INFO] Buildpack build complete, with exit code 0
[INFO] [io.quarkus.container.image.buildpack.deployment.BuildpackProcessor] Buildpack build complete
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 198821ms
This will successfully build a container image of your Quarkus project.
Verify that the image was created (the name of your image will be different depending on your Docker ID):
docker images
catherineedelveis//code-with-quarkus 1.0.0-SNAPSHOT c6a436460a70 3 minutes ago 255MB
As you can see, buildpacks help to automate the containerization process, but you don’t have the control over the image insides, so it may be bigger than the one created manually.
Conclusion
Quarkus is steadily gaining popularity among developers used to working with Jakarta EE standards. For others, the learning curve may be longer.
As far as container images are concerned, there are two alternatives:
- Buildpacks that automate the containerization process but take away control over the image layers;
- Dockerfiles that can be adjusted to reduce the container image footprint. Manual configuration takes longer, but the performance gains can be significant with the lightweight base image and additional techniques.