If you're looking to build lightning-fast Java applications with minimal container footprint, Micronaut + GraalVM is the perfect combination. In this post, we'll walk through creating a Micronaut app, containerizing it, and optimizing it with native images and distroless containers.
π οΈ Getting Started with Micronaut
You can quickly generate a Micronaut project using the Micronaut Launch tool:
Or via the CLI:
mn create-app --build=gradle_kotlin --jdk=21 --lang=java --test=junit --features=openapi,swagger-ui,management,gcp-logging fete.bird.container-demo
Run the application:
./gradlew run
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
08:22:52.715 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 312ms. Server Running: http://localhost:8080
<==========---> 80% EXECUTING [13s]
π Startup: ~312ms
π³ Containerizing the App with Docker
Micronaut provides convenient Gradle tasks to build Docker artifacts. Let's generate the standard Dockerfile:
./gradlew dockerfile
Youβll find the Dockerfile at:
Build ππ»β docker ππ»β main ππ»β Dockerfile
Hereβs what it looks like:
FROM eclipse-temurin:21-jre
WORKDIR /home/app
COPY --link layers/libs /home/app/libs
COPY --link layers/app /home/app/
COPY --link layers/resources /home/app/resources
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/home/app/application.jar"]
If we have notice in the Dockerfile, we can see a COPY command with --link layers. This mean we need to create layers directory. OHHH how can I created, don't worry micronaut gradle has all plugin. Lets run another Gradle task to build those directory
Now, generate the necessary layer files:
./gradlew buildLayers
Build the Docker image:
Navigate to the Dockerfile
~/project/Sample/container-demo/build/docker/main ξ°
docker buildx build -f Dockerfile -t micronuat-temurin.21 .
Lets check the docker image in docker
docker image ls
π§± Image: eclipse-temurin:21-jre
π Startup: ~340ms
π¦ Container size: 337MB
docker run --rm -p 8080:8080 micronuat-temurin.21
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
22:44:36.206 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 340ms. Server Running: http://5a2c1732c6ad:8080
Can we make it better, can Java be better for container ?? Hell yeah, let explore native with GRAALVM
β‘ Going Native with GraalVM
Lets run the gradle task ./gradlew dockerfileNative
. This will create a DockerfileNative file under
Build ππ»β docker ππ»β native-main ππ»β DockerfileNative
FROM ghcr.io/graalvm/native-image-community:21-ol9 AS graalvm
WORKDIR /home/app
COPY --link layers/libs /home/app/libs
COPY --link layers/app /home/app/
COPY --link layers/resources /home/app/resources
RUN mkdir /home/app/config-dirs
RUN mkdir -p /home/app/config-dirs/generateResourcesConfigFile
RUN mkdir -p /home/app/config-dirs/ch.qos.logback.contrib/logback-json-classic/0.1.5
RUN mkdir -p /home/app/config-dirs/ch.qos.logback/logback-classic/1.4.9
RUN mkdir -p /home/app/config-dirs/org.apache.httpcomponents/httpclient/4.5.14
RUN mkdir -p /home/app/config-dirs/com.google.protobuf/protobuf-java-util/3.21.12
RUN mkdir -p /home/app/config-dirs/io.netty/netty-common/4.1.115.Final
RUN mkdir -p /home/app/config-dirs/io.netty/netty-transport/4.1.115.Final
COPY --link config-dirs/generateResourcesConfigFile /home/app/config-dirs/generateResourcesConfigFile
COPY --link config-dirs/ch.qos.logback.contrib/logback-json-classic/0.1.5 /home/app/config-dirs/ch.qos.logback.contrib/logback-json-classic/0.1.5
COPY --link config-dirs/ch.qos.logback/logback-classic/1.4.9 /home/app/config-dirs/ch.qos.logback/logback-classic/1.4.9
COPY --link config-dirs/org.apache.httpcomponents/httpclient/4.5.14 /home/app/config-dirs/org.apache.httpcomponents/httpclient/4.5.14
COPY --link config-dirs/com.google.protobuf/protobuf-java-util/3.21.12 /home/app/config-dirs/com.google.protobuf/protobuf-java-util/3.21.12
COPY --link config-dirs/io.netty/netty-common/4.1.115.Final /home/app/config-dirs/io.netty/netty-common/4.1.115.Final
COPY --link config-dirs/io.netty/netty-transport/4.1.115.Final /home/app/config-dirs/io.netty/netty-transport/4.1.115.Final
RUN native-image --exclude-config .*/libs/netty-buffer-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-common-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-transport-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http2-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-handler-4.1.119.Final.jar ^/META-INF/native-image/.* -cp /home/app/libs/*.jar:/home/app/resources:/home/app/application.jar --no-fallback -o application -H:ConfigurationFileDirectories=/home/app/config-dirs/generateResourcesConfigFile,/home/app/config-dirs/ch.qos.logback.contrib/logback-json-classic/0.1.5,/home/app/config-dirs/ch.qos.logback/logback-classic/1.4.9,/home/app/config-dirs/org.apache.httpcomponents/httpclient/4.5.14,/home/app/config-dirs/com.google.protobuf/protobuf-java-util/3.21.12,/home/app/config-dirs/io.netty/netty-codec-http/4.1.80.Final,/home/app/config-dirs/io.netty/netty-common/4.1.115.Final,/home/app/config-dirs/io.netty/netty-buffer/4.1.80.Final,/home/app/config-dirs/io.netty/netty-transport/4.1.115.Final,/home/app/config-dirs/io.netty/netty-handler/4.1.80.Final,/home/app/config-dirs/io.netty/netty-codec-http2/4.1.80.Final fete.bird.Application
FROM cgr.dev/chainguard/wolfi-base:latest
EXPOSE 8080
COPY --link --from=graalvm /home/app/application /app/application
ENTRYPOINT ["/app/application"]
Lets run few task to generate the directory with files
./gradlew buildNativeLayers
./gradlew dockerPrepareContext
Lets build the docker image
docker buildx build -f DockerfileNative -t micronuat-native-graal .
Lets check the image
docker image ls
docker run --rm -p 8080:8080 micronuat-native-graal
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
22:57:32.389 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 12ms. Server Running: http://388dc34c3d3d:8080
π§± Image: ghcr.io/graalvm/native-image-community:21-ol9
π Startup: ~12ms
π¦ Container size: 82.5MB
π¦ Optimizing Further with Distroless Containers
tasks.named<io.micronaut.gradle.docker.NativeImageDockerfile>("dockerfileNative") {
jdkVersion = "21"
baseImage("gcr.io/distroless/static-debian12")
}
Create again NativeImageDockerfile
./gradlew dockerfileNative
./gradlew buildNativeLayers
./gradlew dockerPrepareContext
FROM ghcr.io/graalvm/native-image-community:21-ol9 AS graalvm
WORKDIR /home/app
COPY --link layers/libs /home/app/libs
COPY --link layers/app /home/app/
COPY --link layers/resources /home/app/resources
RUN mkdir /home/app/config-dirs
RUN mkdir -p /home/app/config-dirs/generateResourcesConfigFile
RUN mkdir -p /home/app/config-dirs/ch.qos.logback.contrib/logback-json-classic/0.1.5
RUN mkdir -p /home/app/config-dirs/ch.qos.logback/logback-classic/1.4.9
RUN mkdir -p /home/app/config-dirs/org.apache.httpcomponents/httpclient/4.5.14
RUN mkdir -p /home/app/config-dirs/com.google.protobuf/protobuf-java-util/3.21.12
RUN mkdir -p /home/app/config-dirs/io.netty/netty-common/4.1.115.Final
RUN mkdir -p /home/app/config-dirs/io.netty/netty-transport/4.1.115.Final
COPY --link config-dirs/generateResourcesConfigFile /home/app/config-dirs/generateResourcesConfigFile
COPY --link config-dirs/ch.qos.logback.contrib/logback-json-classic/0.1.5 /home/app/config-dirs/ch.qos.logback.contrib/logback-json-classic/0.1.5
COPY --link config-dirs/ch.qos.logback/logback-classic/1.4.9 /home/app/config-dirs/ch.qos.logback/logback-classic/1.4.9
COPY --link config-dirs/org.apache.httpcomponents/httpclient/4.5.14 /home/app/config-dirs/org.apache.httpcomponents/httpclient/4.5.14
COPY --link config-dirs/com.google.protobuf/protobuf-java-util/3.21.12 /home/app/config-dirs/com.google.protobuf/protobuf-java-util/3.21.12
COPY --link config-dirs/io.netty/netty-common/4.1.115.Final /home/app/config-dirs/io.netty/netty-common/4.1.115.Final
COPY --link config-dirs/io.netty/netty-transport/4.1.115.Final /home/app/config-dirs/io.netty/netty-transport/4.1.115.Final
RUN native-image --exclude-config .*/libs/netty-buffer-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-common-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-transport-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-codec-http2-4.1.119.Final.jar ^/META-INF/native-image/.* --exclude-config .*/libs/netty-handler-4.1.119.Final.jar ^/META-INF/native-image/.* -cp /home/app/libs/*.jar:/home/app/resources:/home/app/application.jar --no-fallback -o application -H:ConfigurationFileDirectories=/home/app/config-dirs/generateResourcesConfigFile,/home/app/config-dirs/ch.qos.logback.contrib/logback-json-classic/0.1.5,/home/app/config-dirs/ch.qos.logback/logback-classic/1.4.9,/home/app/config-dirs/org.apache.httpcomponents/httpclient/4.5.14,/home/app/config-dirs/com.google.protobuf/protobuf-java-util/3.21.12,/home/app/config-dirs/io.netty/netty-codec-http/4.1.80.Final,/home/app/config-dirs/io.netty/netty-common/4.1.115.Final,/home/app/config-dirs/io.netty/netty-buffer/4.1.80.Final,/home/app/config-dirs/io.netty/netty-transport/4.1.115.Final,/home/app/config-dirs/io.netty/netty-handler/4.1.80.Final,/home/app/config-dirs/io.netty/netty-codec-http2/4.1.80.Final fete.bird.Application -H:+StaticExecutableWithDynamicLibC
FROM gcr.io/distroless/static-debian12
EXPOSE 8080
COPY --link --from=graalvm /home/app/application /app/application
ENTRYPOINT ["/app/application"]
Lets build the docker image
docker buildx build -f DockerfileNative -t micronuat-native-graal .
Lets check the image
docker image ls
docker run --rm -p 8080:8080 micronuat-native-graal
__ __ _ _
| \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| | | | | (__| | | (_) | | | | (_| | |_| | |_
|_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
22:57:32.389 [main] INFO io.micronaut.runtime.Micronaut - Startup completed in 10ms. Server Running: http://388dc34c3d3d:8080
π§± Image: ghcr.io/graalvm/native-image-community:21-ol9
π Startup: ~10ms
π¦ Container size: 70.6MB
Can you go further - YES
- Running a Fully Static Application β In an Empty Container
π§± Image: scratch
π Startup: ~8ms
π¦ Container size: 69.2MB
- Going Extreme β UPX Compression
π§± Image: scratch
π Startup: ~6ms
π¦ Container size: 22.3MB
π§΅ Conclusion
Micronaut combined with GraalVM unlocks blazing-fast Java apps with minimal startup time and memory usageβideal for cloud-native and serverless deployments. With the help of native compilation and container optimizations, Java is now a top-tier choice for microservices at scale.
More details here - https://medium.com/graalvm/from-jit-to-native-path-to-efficient-java-containers-d81221418c39
Top comments (0)