This example shows how to run a simple Micronaut REST application, compile it with GraalVM Native Image, and package it in a container image. Along the way you will see the performance benefits that GraalVM Native Image provides to Micronaut applications.
-
Download and install GraalVM using SDKMAN!:
sdk install java 25-graal
-
Download or clone GraalVM demos repository and navigate into the example directory:
git clone https://github.com/graalvm/graalvm-demos
cd graalvm-demos/native-image/microservices/micronaut-hello-rest-maven
-
Compile the application and build a fat JAR that includes all its dependencies:
./mvnw clean package
-
Run the application on HotSpot from a JAR file:
java -jar target/MnHelloRest-0.1.jar
You can see the app starts in few hundred milliseconds.
-
To test the application, either send a
curlrequest from a new terminal window, or open it in a browser:curl http://localhost:8080/GraalVM
The response should be
Hello GraalVM. Then stop the application:CTRL-C
With no runtime reflection, Micronaut is extremely well suited to ahead-of-time (AOT) compilation with GraalVM Native Image. Micronaut provides support for GraalVM Native Image by default.
-
Compile this REST application ahead of time:
./mvnw package -Dpackaging=native-image
Compilation can take a few minutes. Compiling on a machine with more cores and more memory will reduce the build time.
The result is a standalone executable placed in the target/ directory named
hello. -
Run the application from the native executable:
./target/hello
This ahead-of-time compiled application started much faster than when running on the JVM!
It is so fast because it does not have to parse bytecode for JDK and application classes, initialize the JIT compiler, allocate JIT code caches, JIT profile data caches, and so on.
-
Test the application either with
curlor open it in a browser:curl http://localhost:8080/GraalVM
It returns the same message: "Hello GraalVM". Then stop the application:
CTRL-C
You can run the same steps in containers using Docker.
A common first question is which base container image to use.
GraalVM Native Image supports both statically and dynamically linked executables, with dynamic linking being the default.
To run a native executable that is dynamically linked against glibc, you need a base image that includes it.
A good choice is a Google Distroless image, such as java-base-debian13.
-
Create a
Dockerfile.nativewith the following contents:# Builder FROM container-registry.oracle.com/graalvm/native-image:25 AS nativebuild WORKDIR / COPY . . RUN chmod +x mvnw RUN ./mvnw --no-transfer-progress package -Dpackaging=native-image # Runner FROM gcr.io/distroless/java-base-debian13 COPY --from=nativebuild /target/hello / EXPOSE 8080 ENTRYPOINT ["/hello"]
This example demonstrates a multi-stage build. First, a dynamically linked native executable is built using the
graalvm/native-image:25container image as the builder. The executable is then copied into the runner container, where port 8080 is exposed and the entrypoint is configured. The runtime image is based on Debian and providesglibcand other libraries required to run the native executable, without including a full JDK installation. This is sufficient to run the native image. -
Build a container image:
docker build . -f Dockerfile.native -t micronaut:helloCheck the file size of the newly-created image:
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE micronaut hello 6b7ba89420ae 31 minutes ago 56.9MBThe container image that was created is about 56.9MB.
-
Now run the container image with
docker:docker run -p8080:8080 --rm micronaut:hello
-
Finally, test the application using
curlin the second terminal window or open it in a browser.curl http://localhost:8080/GraalVM
It returns the same message: "Hello GraalVM".
Additionally, you can find an example Dockerfile, Dockerfile.jvm, for packaging and running this Micronaut application from a JAR file. You can build the second container image and compare the sizes of both images:
docker build . -f Dockerfile.jvm -t micronaut:hello.jvmdocker images | grep micronaut
REPOSITORY TAG IMAGE ID CREATED SIZE
micronaut hello b10fb62f7b3e 4 seconds ago 56.9MB
micronaut hello.jvm 7471e8f472f8 36 minutes ago 245MBThe difference in image size is significant. This is because a native container does not include a full JVM; it contains only the application code and the minimal runtime required to execute it.
Micronaut makes it really easy to build modern Java applications and microservices. Its elimination of runtime reflection also makes it an ideal application framework to use with GraalVM Native Image for ahead-of-time compilation and containerization.
To take this further, you can turn your application into a completely static or mostly statically linked native executable using Native Image.
In this case, the application can run even in minimal or empty containers such as scratch or Alpine Linux.
See the guide Build a Statically Linked or Mostly-Statically Linked Native Executable to learn more.
- Tiny Java Containers demo shows how a simple Java application and a simple web server can be compiled to produce very small Docker container images using various lightweight base images.
- From JIT to Native: Efficient Java Containers with GraalVM and Spring Boot workshop demonstrates how to build efficient, size-optimized native applications, and deploy them in various containers.