Are you looking for a way to build super fast starting images with a low memory footprint? Use GraalVM’s native image and make the difference!
To skip the details on when building a native image is possible we will use a framework that advertises with this from the beginning: Micronaut. A nice tutorial on Micronaut and GraalVM can be found here. More in depth documentation is found on the GraalVM docs here.
We will be building a native image for an existing gRPC API from this previous post on Micronaut testing. The complete code for this post can be found on github here.
What do we have to win? How does the image look and perform when we do a normal build? Lets take a look at the Dockerfile.
FROM openjdk:14-alpine COPY target/grpc-cat-api-*.jar grpc-cat-api.jar EXPOSE 50051 CMD ["java", "-Dcom.sun.management.jmxremote", "-Xmx128m", "-jar", "grpc-cat-api.jar"]
We see that we build on top of the openjdk:14-alpine image (340MB). This is already al relatively small image for a jdk base image. Now let’s see what our mini API adds.
docker build -t grpc-cat-api:1.0.0-M1 .
The total comes to a 361MB and a startup time of 965ms. Not bad, but we can do better.
We will look at the Dockerfile for the new image. We will use a multistage dockerfile where we build the image with a GraalVM image and run with a very small and secure distroless image.
# We are using the Java 11 version of GraalVM CE 21.0.0 FROM ghcr.io/graalvm/graalvm-ce:java11-21.0.0 as graalvm # Install the native image tool RUN gu install native-image # Set the build directory WORKDIR /home/build # Copy the shaded application jar file to the build directory COPY target/grpc-cat-api-*.jar /home/build/ # Create the native image executable RUN native-image --no-server -cp grpc-cat-api-*.jar -H:Name=grpc-cat-api -H:Class=nl.sybrenbolandit.grpc.cat.api.Application -H:Name=grpc-cat-api -H:+StaticExecutableWithDynamicLibC && mv grpc-cat-api api # Create Docker image from the Distroless base image FROM gcr.io/distroless/base-debian10 EXPOSE 50051 COPY --from=graalvm /home/build/api /app/api WORKDIR /app/ ENTRYPOINT ["/app/api"]
Ok, that is a lot. But the most important line is the RUN native-image one. Here we use the native-image command from GraalVM with some arguments. In the example we just specify the jar and the name of the executable for a minimal setup.
+StaticExecutableWithDynamicLibC enables us to run the app on a very limited distroless image. Without it the app will not start up.
The preferred way to specify the configuration for the native image is to embed the config into the jar. To automatically pick up this config is to put it at the following path:
META-INF/ └── native-image └── groupID └── artifactID └── native-image.properties
In this file we give a list of the arguments for the image creation.
Args = -H:IncludeResources=logback.xml|application.yml \ -H:Name=grpc-cat-api \ -H:Class=nl.sybrenbolandit.grpc.cat.api.Application \ -H:+StaticExecutableWithDynamicLibC \ --no-server
Now we can omit this from the dockerfile and keep it together with the jar.
... # Create the native image executable RUN native-image -jar grpc-cat-api-*.jar && mv grpc-cat-api api ...
There are many more build options you can specify but this is out of scope for this post. Read more about it in the GraalVM docs here.
Just build and see.
docker build -t grpc-cat-api:1.0.0 .
Note: The build is significantly longer and CPU intensive. The benefits of faster startup and lower memory footprint will not always outweigh this longer development process. Use when appropriate.
Nice! That is just 77.2MB and a startup time of 27ms, quite a difference.
I hope you are now able to build a native image for your applications (when appropriate). Happy building!
Leave a Reply