DEV Community

anand jaisy
anand jaisy

Posted on

How small can Java app on the container

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:

memory on base

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
Enter fullscreen mode Exit fullscreen mode

Run the application:

./gradlew run
Enter fullscreen mode Exit fullscreen mode
 __  __ _                                  _   
|  \/  (_) ___ _ __ ___  _ __   __ _ _   _| |_ 
| |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
| |  | | | (__| | | (_) | | | | (_| | |_| | |_ 
|_|  |_|_|\___|_|  \___/|_| |_|\__,_|\__,_|\__|
08:22:52.715 [main] INFO  io.micronaut.runtime.Micronaut - Startup completed in 312ms. Server Running: http://localhost:8080
<==========---> 80% EXECUTING [13s]
Enter fullscreen mode Exit fullscreen mode
πŸš€ Startup: ~312ms
Enter fullscreen mode Exit fullscreen mode

🐳 Containerizing the App with Docker

Micronaut provides convenient Gradle tasks to build Docker artifacts. Let's generate the standard Dockerfile:

./gradlew dockerfile
Enter fullscreen mode Exit fullscreen mode

You’ll find the Dockerfile at:

Build πŸ‘‰πŸ»βž” docker πŸ‘‰πŸ»βž” main πŸ‘‰πŸ»βž” Dockerfile
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

uploads for scatch

Build the Docker image:

Navigate to the Dockerfile

~/project/Sample/container-demo/build/docker/main ξ‚°

docker buildx build -f Dockerfile -t micronuat-temurin.21 .
Enter fullscreen mode Exit fullscreen mode

Lets check the docker image in docker

docker image ls
Enter fullscreen mode Exit fullscreen mode

uploads john

🧱 Image: eclipse-temurin:21-jre
πŸš€ Startup: ~340ms
πŸ“¦ Container size: 337MB
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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"]
Enter fullscreen mode Exit fullscreen mode

Lets run few task to generate the directory with files

./gradlew buildNativeLayers
./gradlew dockerPrepareContext
Enter fullscreen mode Exit fullscreen mode

Lets build the docker image

docker buildx build -f DockerfileNative -t micronuat-native-graal .
Enter fullscreen mode Exit fullscreen mode

Lets check the image

docker image ls
Enter fullscreen mode Exit fullscreen mode

john

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
Enter fullscreen mode Exit fullscreen mode
🧱 Image: ghcr.io/graalvm/native-image-community:21-ol9
πŸš€ Startup: ~12ms
πŸ“¦ Container size: 82.5MB
Enter fullscreen mode Exit fullscreen mode

πŸ“¦ Optimizing Further with Distroless Containers

tasks.named<io.micronaut.gradle.docker.NativeImageDockerfile>("dockerfileNative") {
    jdkVersion = "21"
    baseImage("gcr.io/distroless/static-debian12")
}
Enter fullscreen mode Exit fullscreen mode

Create again NativeImageDockerfile

./gradlew dockerfileNative
./gradlew buildNativeLayers
./gradlew dockerPrepareContext
Enter fullscreen mode Exit fullscreen mode
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"]
Enter fullscreen mode Exit fullscreen mode

Lets build the docker image

docker buildx build -f DockerfileNative -t micronuat-native-graal .
Enter fullscreen mode Exit fullscreen mode

Lets check the image

docker image ls
Enter fullscreen mode Exit fullscreen mode

base image

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
Enter fullscreen mode Exit fullscreen mode
🧱 Image: ghcr.io/graalvm/native-image-community:21-ol9
πŸš€ Startup: ~10ms
πŸ“¦ Container size: 70.6MB
Enter fullscreen mode Exit fullscreen mode

Can you go further - YES

  1. Running a Fully Static Application β€” In an Empty Container
🧱 Image: scratch
πŸš€ Startup: ~8ms
πŸ“¦ Container size: 69.2MB
Enter fullscreen mode Exit fullscreen mode
  1. Going Extreme β€” UPX Compression
🧱 Image: scratch
πŸš€ Startup: ~6ms
πŸ“¦ Container size: 22.3MB
Enter fullscreen mode Exit fullscreen mode

Memory consumption

startup time

🧡 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)