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.

Installing GraalVM JDK

If you want to build a GraalVM native image on your host, you need to install a GraalVM JDK.

There are two types of GraalVM JDK: Oracle GraalVM and GraalVM Community Edition. You can choose a GraalVM JDK to install.

Install GraalVM JDK by SDKMAN:

sdk install java 21.0.8-graal
sdk use java 21.0.8-graal

Or download GraalVM in IntelliJ IDEA and add it to PATH.

After you have installed a GraalVM JDK, you can verify it by running the following command line:

$ java -version

Output:

java version "21.0.8" 2025-07-15 LTS
Java(TM) SE Runtime Environment Oracle GraalVM 21.0.8+12.1 (build 21.0.8+12-LTS-jvmci-23.1-b72)
Java HotSpot(TM) 64-Bit Server VM Oracle GraalVM 21.0.8+12.1 (build 21.0.8+12-LTS-jvmci-23.1-b72, mixed mode, sharing)

GraalVM Native Image Plugins

Before using GraalVM Native Image, you need to add the GraalVM Native Image plugin to your project.

Using Gradle

Add the GraalVM Native Image Plugin to build.gradle.kts.

build.gradle.kts

...
plugins {
...
// Gradle plugin for GraalVM Native Image
id("org.graalvm.buildtools.native") version "0.11.0"
...
}
...

Note: Don’t add the following configuration in build.gradle.kts if you want to build a Spring Boot Application to a GraalVM Native Image:

tasks.jar { enabled = false }

Build a GraalVM Native Image

# Build a GraalVM Native Image
$ ./gradlew nativeCompile -Dorg.gradle.configuration-cache=false
# Execute
$ ./build/native/nativeCompile/myapp
# Add system properteis in running the native image executable file
$ ./build/native/nativeCompile/myapp -Dserver.port=8081

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
# Install findutils to fix "xargs is not available" error when running ./gradlew commands
RUN microdnf install -y --setopt=install_weak_deps=0 --nodocs findutils \
&& microdnf clean all
ARG WORKDIR=/app
WORKDIR /app
COPY gradle ./gradle
COPY settings.gradle.kts build.gradle.kts gradle.properties gradlew ./
RUN --mount=type=cache,target=${WORKDIR}/.gradle \
--mount=type=cache,target=/root/.gradle \
./gradlew help
COPY . .
RUN --mount=type=cache,target=${WORKDIR}/.gradle \
--mount=type=cache,target=/root/.gradle \
./gradlew -x test nativeCompile -Dorg.gradle.configuration-cache=false

FROM alpine:3 AS runtime
# Install the `gcompat` package in your Alpine Dockerfile to provide a compatibility layer for `glibc` binaries.
RUN apk add --no-cache gcompat
# Copy the native executable from the builder stage
COPY --from=builder /app/build/native/nativeCompile/a-simple-web-application /app/a-simple-web-application
# Set the timezone to Shanghai
ENV TZ=Asia/Shanghai
# Expose the port the app runs on
EXPOSE 8080
# Set the entrypoint to the native executable
ENTRYPOINT ["/app/a-simple-web-application"]
  • 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 using glibc, which is incompatible with musl libc without specific adjustments. 1. Adding gcompat: Install the gcompat package in your Alpine Dockerfile to provide a compatibility layer for glibc binaries. 2. Or Use a glibc compatible base image such as Ubuntu or Debian.

3. Build Docker Image

# Time cost: About 120s.
docker build -t myapp .

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 an image (gradle bootBuildImage): 125MB.
  • Non GraalVM native-image Docker image: 230MB.

Build resources on my local PC:

  • 24.18GB of memory (75.6% of 32.00GB system memory, determined at start)
  • 8 thread(s) (100.0% of 8 available processor(s), determined at start)

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
docker login

docker buildx create \
--name my-multi-platform-builder \
--driver docker-container \
--bootstrap --use

# Time cost: About 300~400 seconds (A simple Spring Boot web application)
docker build \
--builder my-multi-platform-builder \
--platform linux/amd64,linux/arm64 \
--tag IMAGE_NAME \
--push .
  • 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 an 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
$ gradle --configuration-cache \
init --use-defaults \
--type java-application \
--package com.taogen.demo

2. Gradle configuration

app/build.gradle.kts

plugins {
java
application
id("org.graalvm.buildtools.native") version "0.11.0"
}

repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}

dependencies {
// Use JUnit Jupiter for testing.
testImplementation(libs.junit.jupiter)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}

// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

application {
// Define the main class for the application.
mainClass = "com.taogen.demo.App"
}

tasks.named<Test>("test") {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}

tasks.withType<Jar> {
// Set a constant name for the JAR
archiveFileName.set("myapp.jar")
// Configure the JAR task to include the main class in the manifest.
// Adds metadata to the JAR manifest
manifest {
attributes["Main-Class"] = "com.taogen.demo.App" // Replace with your actual main class
}
}

3. The application

com/taogen/demo/App.java

package com.taogen.demo;

import java.util.Scanner;

public class App {
public static void main(String[] args) {
System.out.println("Echo application started. Type 'exit' to quit.");
String message = "";
while (!"exit".equals(message)) {
Scanner scanner = new Scanner(System.in);
System.out.print("Enter 'exit' to quit or enter your message: ");
message = scanner.nextLine();
System.out.println("You entered: " + message);
}
}
}

4. Build and run the application on your host

# Time cost: about 35 seconds
$ ./gradlew nativeCompile -Dorg.gradle.configuration-cache=false
$ ./app/build/native/nativeCompile/app

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
# Install findutils to fix "xargs is not available" error when running ./gradlew commands
RUN microdnf install -y --setopt=install_weak_deps=0 --nodocs findutils \
&& microdnf clean all
ARG WORKDIR=/app
WORKDIR /app
COPY gradle ./gradle
COPY settings.gradle.kts gradle.properties gradlew ./
COPY app/build.gradle.kts ./app/
RUN --mount=type=cache,target=${WORKDIR}/.gradle \
--mount=type=cache,target=/root/.gradle \
./gradlew help
COPY . .
RUN --mount=type=cache,target=${WORKDIR}/.gradle \
--mount=type=cache,target=/root/.gradle \
./gradlew -x test nativeCompile -Dorg.gradle.configuration-cache=false

FROM alpine:3 AS runtime
# Install the `gcompat` package in your Alpine Dockerfile to provide a compatibility layer for `glibc` binaries.
RUN apk add --no-cache gcompat
# Copy the native executable from the builder stage
COPY --from=builder /app/app/build/native/nativeCompile/app /app/app
# Set the timezone to Shanghai
ENV TZ=Asia/Shanghai
# Set the entrypoint to the native executable
ENTRYPOINT ["/app/app"]

Build Docker Image

# Time cost: About 40 seconds.
docker build -t simple-echo-cli .
  • 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

6. Fully Static GraalVM Native Image

Dockerfile

FROM --platform="linux/amd64" ghcr.io/graalvm/native-image-community:21-muslib-ol9 AS builder
# Install findutils to fix "xargs is not available" error when running ./gradlew commands
RUN microdnf install findutils
ARG WORKDIR=/app
WORKDIR /app
COPY gradle ./gradle
COPY settings.gradle.kts gradle.properties gradlew ./
COPY app/build.gradle.kts ./app/
RUN --mount=type=cache,target=${WORKDIR}/.gradle \
--mount=type=cache,target=/root/.gradle \
./gradlew help
COPY . .
# Build the native image with static linking
# buildArgs.addAll(listOf("--static", "--libc=musl"))
RUN --mount=type=cache,target=${WORKDIR}/.gradle \
--mount=type=cache,target=/root/.gradle \
./gradlew -x test nativeCompile --build-args="--static" --build-args="--libc=musl" -Dorg.gradle.configuration-cache=false

FROM --platform="linux/amd64" scratch AS runtime
# Copy the native executable from the builder stage
COPY --from=builder /app/app/build/native/nativeCompile/myapp /app/app
# Set the timezone to Shanghai
ENV TZ=Asia/Shanghai
# Set the entrypoint to the native executable
ENTRYPOINT ["/app/app"]
  • Using ghcr.io/graalvm/native-image-community:21-muslib-ol9 instead of ghcr.io/graalvm/native-image-community:21-ol9.
  • Add GraalVM native image buildArgs --static --libc=musl by ./gradlew nativeCompile --build-args="--static" --build-args="--libc=musl"

Add GraalVM native image buildArgs

You can add GraalVM native image buildArgs in command line ./gradlew nativeCompile --build-args="--static" --build-args="--libc=musl"

Or you can add GraalVM native image buildArgs in src/main/resources/META-INF/native-image/native-image.properties

native-image.properties

Args=\
--static \
--libc=musl

Or you can add GraalVM native image buildArgs in build.gradle.kts

build.gradle.kts

graalvmNative {
binaries {
named("main") {
buildArgs.addAll(listOf("--static", "--libc=musl"))
}
}
}

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
# Install the `gcompat` package in your Alpine Dockerfile to provide a compatibility layer for `glibc` binaries.
RUN apk add --no-cache gcompat
# ...

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.

Appendixes

Running Simple Echo CLI GraalVM Native Image on Debian Linux

1. Install SDKMAN, GraalVM JDK 21, and Gradle 9.

sudo apt update && sudo apt upgrade -y
# sudo apt install git -y
# git clone git@github.com:tg-playground/code-example-devops.git
# Install zip before install SDKMAN
sudo apt install zip -y
# Install SDKMAN
curl -s "https://get.sdkman.io" | bash
source "/root/.sdkman/bin/sdkman-init.sh"
# Install GraalVM JDK 21 with SDKMAN
sdk install java 21.0.8-graal
java -version
# Install Gradle with SDKMAN
sdk install gradle 9.0.0
gradle -v

2. Build and run Java JAR

# Build with Gradle
./gradlew build
java -jar app/build/libs/myapp.jar

3. Build and run GraalVM Native Image

# Add JAVA_HOME environment variable before build a GraalVM Native Image
echo $JAVA_HOME
echo "export JAVA_HOME=/root/.sdkman/candidates/java/current" >> /etc/profile
source /etc/profile
echo $JAVA_HOME
# Install gcc
sudo apt install gcc -y
# Build
./gradlew -x test nativeCompile -Dorg.gradle.configuration-cache=false --stacktrace
# Run GraalVM Native Image executable file
./app/build/native/nativeCompile/myapp

4. Run with Docker (GraalVM Native Image)

# Install Docker
# ...
docker -v
# Time cost: 95s
docker build -t simple-echo-cli:native -f Dockerfile-native .
docker run -it simple-echo-cli:native

Resources