DEV Community

anand jaisy
anand jaisy

Posted on

1

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

Runner H image

Automate Your Workflow in Slack, Gmail, Notion & more

Runner H connects to your favorite tools and handles repetitive tasks for you. Save hours daily. Try it free while it’s in beta.

Try for Free

Top comments (0)

Feature flag article image

Create a feature flag in your IDE in 5 minutes with LaunchDarkly’s MCP server 🏁

How to create, evaluate, and modify flags from within your IDE or AI client using natural language with LaunchDarkly's new MCP server. Follow along with this tutorial for step by step instructions.

Read full post

πŸ‘‹ Kindness is contagious

Take a moment to explore this thoughtful article, beloved by the supportive DEV Community. Coders of every background are invited to share and elevate our collective know-how.

A heartfelt "thank you" can brighten someone's dayβ€”leave your appreciation below!

On DEV, sharing knowledge smooths our journey and tightens our community bonds. Enjoyed this? A quick thank you to the author is hugely appreciated.

Okay