Getty Images
How to build lightning-fast Quarkus native executables
Want to build cloud-native Java apps in Java versus Go and Rust? This step-by-step tutorial shows how to create native executables in Quarkus that start fast and use little memory.
In a previous article, I walked through how to build a product catalogue microservice with Quarkus. Now, let's take it to the next level by compiling our application into a native executable using GraalVM. This transformation will provide us with an application that starts in milliseconds and uses a fraction of the memory, making it ideal for serverless, containerized and edge deployments.
This article assumes you've completed the product catalog application from the previous tutorial about Quarkus microservices. If you haven't, grab the source code on GitHub.
Why native images matter
In the cloud-native world, every millisecond and megabyte count. Traditional JVM applications take seconds to start and consume significant memory, which makes them unsuitable for serverless functions and cost-prohibitive for containers.
Native compilation transforms this landscape. Pre-compiling everything at build time eliminates JVM overhead entirely, so applications start in milliseconds instead of seconds, use significantly less memory, and ship as small optimized executables. These characteristics make Java competitive with Go and Rust for cloud-native deployments.
Step by step: Build a native image in Quarkus
One of Quarkus's most compelling features is its ability to compile Java applications to native executables using GraalVM. The tool's native image technology performs ahead-of-time compilation to transform Java bytecode into platform-specific machine code.
This results in applications that start in milliseconds and use minimal memory.
Prerequisites for native compilation
First, ensure you have the necessary C development tools. Issue the following instructions depending on your platform OS. For more detailed platform-specific instructions and troubleshooting, consult the official Quarkus guide.
Linux (Fedora/RHEL/CentOS):
sudo dnf install gcc glibc-devel zlib-devel libstdc++-static
Linux (Ubuntu/Debian):
sudo apt-get install build-essential libz-dev zlib1g-dev
macOS:
xcode-select --install
Windows: Install Visual Studio 2017 Visual C++ Build Tools
The simplest way to install GraalVM is by using SDKMAN:
# List available GraalVM versions
sdk list java | grep graal
# Install GraalVM for Java 21
sdk install java 21.0.5-graal
# Set it as the current JVM
sdk use java 21.0.5-graal
# Verify installation
java --version
You should see the following output to indicate GraalVM is installed and working properly:
java 21.0.5 2024-10-15 LTS
Java(TM) SE Runtime Environment Oracle GraalVM 21.0.5+9.1 (build 21.0.5+9-LTS-jvmci-23.1-b48)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 21.0.5+9.1 (build 21.0.5+9-LTS-jvmci-23.1-b48, mixed mode, sharing)
Build the native executable
With our environment ready, let's build the native image. This process will take several minutes as GraalVM performs extensive analysis and optimizations, including aggressive ahead-of-time compilation.
./mvnw clean package -Pnative
During the build, you'll see output such as the following:
[INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building native image source jar:
/Users/bazlur/projects/quarkus/product-catalog/product-catalog/target/product-catalog-1.0.0-SNAPSHOT-native-image-source-jar/product-catalog-1.0.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Building native image from
/Users/bazlur/projects/quarkus/product-catalog/product-catalog/target/product-catalog-1.0.0-SNAPSHOT-native-image-source-jar/product-catalog-1.0.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on GRAALVM 23.1 JDK 21.0.5+9-LTS-jvmci-23.1-b48
...
...
GraalVM Native Image: Generating 'product-catalog-1.0.0-SNAPSHOT-runner' (executable)...
===============================================================================================================
[1/8] Initializing... (6.3s @ 0.22GB)
Java version: 21.0.5+9-LTS, vendor version: Oracle GraalVM 21.0.5+9.1
Graal compiler: optimization level: 2, target machine: armv8-a, PGO: ML-inferred
C compiler: cc (apple, arm64, 17.0.0)
Garbage collector: Serial GC (max heap size: 80% of RAM)
10 user-specific feature(s):
- com.oracle.svm.thirdparty.gson.GsonFeature
- io.quarkus.caffeine.runtime.graal.CacheConstructorsFeature
- io.quarkus.hibernate.orm.runtime.graal.DisableLoggingFeature: Disables INFO logging during the analysis phase
The result is as follows, a standalone executable in the target directory:
ls -lh target/*-runner
Which outputs:
-rwxr-xr-x@ 1 bazlur staff 93M Jun 15 08:44 target/product-catalog-1.0.0-SNAPSHOT-runner
For quick testing, you can run the native executable directly:
target/product-catalog-1.0.0-SNAPSHOT-runner
However, this will fail if it can't connect to a database.
Next we'll set up a proper production environment.
Run in production mode
For production deployment, you'll need a real PostgreSQL database. You can either install PostgreSQL locally or run it in a container, as so:
# Run PostgreSQL in a container
docker run -d \
--name postgres-prod \
-e POSTGRES_DB=productdb \
-e POSTGRES_USER=produser \
-e POSTGRES_PASSWORD=prodpass \
-p 5432:5432 \
postgres:15-alpine
# Wait for PostgreSQL to be ready
sleep 5
# Create the schema (optional - Hibernate can do this)
docker exec -i postgres-prod psql -U produser -d productdb << EOF
CREATE TABLE IF NOT EXISTS products (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description VARCHAR(1000),
price DECIMAL(10,2) NOT NULL,
category VARCHAR(255) NOT NULL,
created_at TIMESTAMP
);
EOF
Next, configure the application to connect to your database:
export QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://localhost:5432/productdb
export QUARKUS_DATASOURCE_USERNAME=produser
export QUARKUS_DATASOURCE_PASSWORD=prodpass
Run the native executable:
./target/product-catalog-1.0.0-SNAPSHOT-runner
After that you'll see the following output:
2024-10-26 07:33:03,735 INFO [io.quarkus] (main) product-catalog 1.0.0-SNAPSHOT native
(powered by Quarkus 3.20.0) started in 0.047s. Listening on: http://0.0.0.0:8080
2024-10-26 07:33:03,735 INFO [io.quarkus] (main) Profile prod activated.
2024-10-26 07:33:03,735 INFO [io.quarkus] (main) Installed features:
[agroal, cdi, hibernate-orm, hibernate-orm-panache, jdbc-postgresql,
narayana-jta, resteasy, resteasy-jackson, smallrye-context-propagation, vertx]
Notice the startup time: 0.047 seconds! This is roughly 25-50x faster than a traditional JVM application.
Test the native application:
# Create a product
curl -X POST localhost:8080/products \
-H "Content-Type: application/json" \
-d '{
"name": "Native Product",
"description": "Created by native image",
"price": 99.99,
"category": "Test"
}'
# Retrieve all products
curl localhost:8080/products | jq
Conclusion
And that's it!
We've built a production-ready native image that runs blazing fast.
A N M Bazlur Rahman is a Java Champion and staff software developer at DNAstack. He is also founder and moderator of the Java User Group in Bangladesh.