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.

Starting image

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", "", "-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 .
docker images output with 361MB image 1024x39 - NATIVE IMAGE
micronaut startup output in 965ms 1024x182 - NATIVE IMAGE

The total comes to a 361MB and a startup time of 965ms. Not bad, but we can do better.

Native image

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 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:Name=grpc-cat-api -H:+StaticExecutableWithDynamicLibC && mv grpc-cat-api api

# Create Docker image from the Distroless base image
EXPOSE 50051
COPY --from=graalvm /home/build/api /app/api
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.

The flag +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:

└── native-image
    └── groupID
        └── artifactID

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:+StaticExecutableWithDynamicLibC \

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.

docker images output comparing images sizes 1024x53 - NATIVE IMAGE
micronaut startup in 27ms 1024x180 - NATIVE IMAGE

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!