GraalVM Native Image with Docker
GraalVM Native Image is a tool that takes your Java (or other JVM-based) application and compiles it ahead of time (AOT) into a standalone, platform-specific executable.
Instead of running on the JVM with JIT compilation at runtime, it produces a binary that:
- Starts instantly (milliseconds instead of seconds)
- Uses less memory
- Doesn’t require a JDK installed on the target machine
In this post, I will cover using GraalVM Native Image with Docker.
GraalVM Native Image Plugins
Before using GraalVM Native Image, you need add the GraalVM Native Image plugin to your project.
Using Gradle
Add the GraalVM Native Image Plugin to build.gradle.kts
.
build.gradle.kts
... |
Note: Don’t add the following configuration in build.gradle.kts
if you want to build Spring Boot Application to a GraalVM Native Image:
tasks.jar { enabled = false } |
Build and run native image on your host
# Build a GraalVM Native Image |
Spring Boot Application with GraalVM Native Image
1. Initialize a Spring Boot web application in Spring Initializr
2. Add Dockerfile
Dockerfile
FROM ghcr.io/graalvm/native-image-community:21-ol9 AS builder |
native-image-community:21-ol9
native-image-community
: This is the GraalVM Community Edition container image that includes the Native Image tooling—used for ahead-of-time (AOT) compilation into native binaries—without bundling the full JDK runtime.21
: Refers to Java (JDK) version 21.ol9
: Indicates that this build uses Oracle Linux 9 as its base OS. GraalVM container images are available on multiple distributions (Oracle Linux versions 7, 8, or 9), and that suffix specifies the platform.
RUN apk add --no-cache gcompat
: GraalVM native images are typically built on systems usingglibc
, which is incompatible with musl libc without specific adjustments. 1. Addinggcompat
: Install thegcompat
package in your Alpine Dockerfile to provide a compatibility layer forglibc
binaries. 2. Or Use aglibc
compatible base image such as Ubuntu or Debian.
3. Build Docker Image
# Time cost: About 120s. |
Docker Image size:
- GraalVM native-image Docker images:
- The image size using
alpine:3
as runtime: 90MB. - The image size using
oraclelinux:9-slim
as runtime: 200MB. - The image size using
Buildpacks
to create a image (gradle bootBuildImage
): 125MB.
- The image size using
- Non GraalVM native-image Docker image: 230MB.
4. Running Docker container
docker run -p 8080:8080 myapp |
5. Access the Spring Boot application
Visiting http://localhost:8080
6. Build and push to DockerHub
# Authenticate to the Registry |
- The format of IMAGE_NAME:
<dockerhub_username>/<application_name>:<tag>
Docker Image size on DockerHub
- The image compressed size on DockerHub using
alpine:3
as runtime: 30MB. - The image compressed size on DockerHub using
oraclelinux:9-slim
as runtime: 75MB - The image compressed size on DockerHub using
Buildpacks
to create a image: 40MB.
A Simple echo CLI Application
1. Create a Java application with the gradle init
command.
$ mkdir simple-echo-cli && cd simple-echo-cli |
2. Gradle configuration
app/build.gradle.kts
plugins { |
3. The application
com/taogen/demo/App.java
package com.taogen.demo; |
4. Build and run the application on your host
# Time cost: about 35 seconds |
5. Running the application with Docker
Add Dockerfile
to the root directory of the project
Dockerfile
FROM ghcr.io/graalvm/native-image-community:21-ol9 AS builder |
Build Docker Image
# Time cost: About 40 seconds. |
- The image size using
oraclelinux:9-slim
as runtime: 130MB. - The image size using
alpine:3
as runtime: 22MB.
Running Docker container
docker run -it simple-echo-cli |
Optimization for faster builds and smaller binaries
Add native-image.properties
file to the root directory of your project
1. For dev builds
native-image.properties
Args = \ |
Flag | Effect in development |
---|---|
--no-fallback |
Avoids building a JVM fallback image, which saves a lot of time. |
--initialize-at-build-time |
Broadly initializes classes at build time; reduces runtime CPU cost and usually speeds up builds slightly. |
--report-unsupported-elements-at-runtime |
Lets the build succeed even if GraalVM finds features it can’t fully handle — errors happen at runtime instead of failing the build. |
-H:+InlineBeforeAnalysis |
Improves analysis performance by inlining methods before heavy analysis — faster compilation. |
--gc=G1 |
Faster compilation than serial for some builds; good balance for dev. |
--enable-http / --enable-https |
Keeps network capabilities enabled for testing without extra setup. |
--enable-url-protocols=http,https |
Ensures URL-based resource loading works without missing protocols. |
-H:IncludeResources=.*.properties|.*.yml|.*.yaml|.*.xml |
GraalVM will include all files (from your JAR’s resources, or from dependencies) whose names match: *.properties , *.yml , *.yaml , *.xml |
--verbose |
Prints detailed build logs to help debug class/resource inclusion. |
2. For production builds
native-image.properties
Args = \ |
Flag | Effect in production |
---|---|
--no-fallback |
Produces only the native binary (no embedded JVM fallback). Makes image smaller, startup faster. |
--initialize-at-build-time |
Initializes most classes at build time, reducing startup time and runtime CPU cost. |
--enable-http / --enable-https |
Ensures networking over HTTP/HTTPS works (disabled by default for security). |
--enable-url-protocols=http,https |
Explicitly registers protocols — useful for libraries relying on URL.openStream() . |
--gc=serial |
Smaller memory footprint, predictable pauses; great for CLI tools or short-lived apps. |
-O2 or -O3 |
Maximum optimization level; increases build time but can significantly improve runtime performance. |
-H:+ReportExceptionStackTraces |
Improves debugging in production by including stack traces in error logs. |
-H:+AddAllCharsets |
Ensures all Java character sets are included — avoids UnsupportedCharsetException . |
-H:+StackTrace |
Keeps full stack traces for exceptions (slightly increases image size). |
--strict-image-heap |
Validates heap during build to avoid runtime surprises; helps catch errors early. |
--install-exit-handlers |
Ensures shutdown hooks run correctly on termination signals. |
-H:IncludeResources=.*.properties|.*.yml|.*.yaml|.*.xml |
GraalVM will include all files (from your JAR’s resources, or from dependencies) whose names match: *.properties , *.yml , *.yaml , *.xml |
Tuning tips for production:
- If you run long-lived server apps, consider
--gc=G1
instead ofserial
for better throughput. - If startup time is more critical than build size, keep
--initialize-at-build-time
broad. - If build time is becoming too long, you can reduce
-O3
to-O2
.
Common errors
Error 1: Executing the docker run
command occurs an error “Fatal glibc error: CPU does not support x86-64-v3”
Solution:
Use oraclelinux:8-slim
or oraclelinux:9-slim
as the runtime base Docker image instead of oraclelinux:10-slim
.
Error 2: Use alpine:3
as the base image to run GraalVM Docker image occurs an error: exec /app/xxx: no such file or directory
The error “no such file or directory” when running a GraalVM native executable in an Alpine-based Docker image often indicates a compatibility issue between the executable and Alpine’s musl libc library. GraalVM native images are typically built on systems using glibc, which is incompatible with musl libc without specific adjustments.
Solution 1:
Adding gcompat
: Install the gcompat
package in your Alpine Dockerfile to provide a compatibility layer for glibc
binaries.
FROM alpine:3 |
Solution 2:
Use a glibc
compatible base image: The most straightforward solution is to switch from Alpine to a base image that uses glibc
, such as Ubuntu or Debian. This eliminates the musl
/glibc
incompatibility.