How To Create a GraalVM Docker Image
Learn how to create a Docker image for your GraalVM native image and find out it is a bit trickier than what you are used to when creating Docker images.
Join the DZone community and get the full member experience.
Join For FreeIn this post, you will learn how to create a Docker image for your GraalVM native image. By means of some hands-on experiments, you will learn that it is a bit trickier than what you are used to when creating Docker images. Enjoy!
Introduction
In a previous post, you learned how to create a GraalVM native image for a Spring Boot 3 application. Nowadays, applications are often distributed as Docker images, so it is interesting to verify how this is done for a GraalVM native image. A GraalVM native image does not need a JVM, so can you use a more minimalistic Docker base image for example? You will execute some experiments during this blog and will learn by doing.
The sources used in this blog are available on GitHub.
The information provided in the GraalVM documentation is a good starting point for learning. It is good reference material when reading this blog.
As an example application, you will use the Spring Boot application from the previous post. The application contains one basic RestController
which just returns a hello
message. The RestController
also includes some code in order to execute tests in combination with Reflection, but this part was added for the previous post.
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
// return "Hello GraalVM!"
String helloMessage = "Default message";
try {
Class<?> helloClass = Class.forName("com.mydeveloperplanet.mygraalvmplanet.Hello");
Method helloSetMessageMethod = helloClass.getMethod("setMessage", String.class);
Method helloGetMessageMethod = helloClass.getMethod("getMessage");
Object helloInstance = helloClass.getConstructor().newInstance();
helloSetMessageMethod.invoke(helloInstance, "Hello GraalVM!");
helloMessage = (String) helloGetMessageMethod.invoke(helloInstance);
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (InstantiationException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
return helloMessage;
}
}
Build the application:
$ mvn clean verify
Run the application from the root of the repository:
$ java -jar target/mygraalvmplanet-0.0.1-SNAPSHOT.jar
Test the endpoint:
$ curl http://localhost:8080/hello
Hello GraalVM!
You are now ready for Dockerizing this application!
Prerequisites
Prerequisites for this blog are:
- Basic Linux knowledge, Ubuntu 22.04 is used during this post
- Basic Java and Spring Boot knowledge
- Basic GraalVM knowledge
- Basic Docker knowledge
- Basic SDKMAN knowledge
Create Docker Image for Spring Boot Application
In this section, you will create a Dockerfile for the Spring Boot application. This is a very basic Dockerfile and is not to be used in production code. See previous posts "Docker Best Practices" and "Spring Boot Docker Best Practices" for tips and tricks for production-ready Docker images. The Dockerfile you will be using is the following:
FROM eclipse-temurin:17.0.5_8-jre-alpine
COPY target/mygraalvmplanet-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]
You use a Docker base image containing a Java JRE, copy the JAR file into the image, and, in the end, you run the JAR file.
Build the Docker image:
$ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT
Verify the size of the image. It is 188MB in size.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT be12e1deda89 33 seconds ago 188MB
Run the Docker image:
$ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT
...
2023-02-26T09:20:48.033Z INFO 1 --- [ main] c.m.m.MyGraalVmPlanetApplication : Started MyGraalVmPlanetApplication in 2.389 seconds (process running for 2.981)
As you can see, the application started in about 2 seconds.
Test the endpoint again. First, find the IP Address of your Docker container. In the output below, the IP Address is 172.17.0.2, but it will probably be something else on your machine.
$ docker inspect mygraalvmplanet | grep IPAddress
"SecondaryIPAddresses": null,
"IPAddress": "172.17.0.2",
"IPAddress": "172.17.0.2",
Invoke the endpoint with the IP Address and verify that it works.
$ curl http://172.17.0.2:8080/hello
Hello GraalVM!
In order to continue, stop the container, remove it, and also remove the image. Do this after each experiment. This way, you can be sure that you start from a clean situation each time.
$ docker rm mygraalvmplanet
$ docker rmi mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT
Create Docker Image for GraalVM Native Image
Let’s do the same for the GraalVM native image. First, switch to using GraalVM.
$ sdk use java 22.3.r17-nik
Create the native image:
$ mvn -Pnative native:compile
Create a similar Dockerfile (Dockerfile-native-image). This time, you use an Alpine Docker base image without a JVM. You do not need a JVM for running a GraalVM native image as it is an executable and not a JAR file.
FROM alpine:3.17.1
COPY target/mygraalvmplanet mygraalvmplanet
ENTRYPOINT ["/mygraalvmplanet"]
Build the Docker image, this time with an extra --file
argument because the file name deviates from the default.
$ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT --file Dockerfile-native-image
Verify the size of the Docker image. It is now only 76.5MB instead of the 177MB earlier.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT 4f7c5c6a9b29 25 seconds ago 76.5MB
Run the container and note that it does not start correctly.
$ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT
exec /mygraalvmplanet: no such file or directory
What is wrong here? Why does this not work?
It is a vague error, but the Alpine Linux Docker image uses musl as a standard C library whereas the GraalVM native image is compiled using an Ubuntu Linux distro, which uses glibc.
Let’s change the Docker base image to Ubuntu. The Dockerfile is Dockerfile-native-image-ubuntu
:
FROM ubuntu:jammy
COPY target/mygraalvmplanet mygraalvmplanet
ENTRYPOINT ["/mygraalvmplanet"]
Build the Docker image.
$ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT --file Dockerfile-native-image-ubuntu
Verify the size of the Docker image, it is now 147MB.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT 1fa90b1bfc54 3 hours ago 147MB
Run the container and it starts successfully in less than 200ms.
$ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT
...
2023-02-26T12:48:26.140Z INFO 1 --- [ main] c.m.m.MyGraalVmPlanetApplication : Started MyGraalVmPlanetApplication in 0.131 seconds (process running for 0.197)
Create Docker Image Based on Distroless Image
The size of the Docker image build with the Ubuntu base image is 147MB. But, the Ubuntu image does contain a lot of tooling which is not needed. Can we reduce the size of the image by using a distroless image which is very small in size?
Create a Dockerfile Dockerfile-native-image-distroless
and use a distroless base image.
FROM gcr.io/distroless/base
COPY target/mygraalvmplanet mygraalvmplanet
ENTRYPOINT ["/mygraalvmplanet"]
Build the Docker image.
$ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT --file Dockerfile-native-image-distroless
Verify the size of the Docker image, it is now 89.9MB.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT 6fd4d44fb622 9 seconds ago 89.9MB
Run the container and see that it is failing to start. It appears that several necessary libraries are not present in the distroless image.
$ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT
/mygraalvmplanet: error while loading shared libraries: libz.so.1: cannot open shared object file: No such file or directory
When Googling this error message, you will find threads that mention copying the required libraries from other images (e.g., the Ubuntu image), but you will encounter a next error and a next error. This is a difficult path to follow and costs some time. See, for example, this thread.
A solution for using distroless images can be found here.
Create Docker Image Based on Oracle Linux
Another approach for creating Docker images is the one that can be found on the GraalVM GitHub page. Build the native image in a Docker container and use a multistage build to build the target image.
The Dockerfile being used is copied from here and can be found in the repository as Dockerfile-oracle-linux.
Create a new file Dockerfile-native-image-oracle-linux, copy the contents of Dockerfile-oracle-linux into it, and change the following:
- Update the Maven SHA and DOWNLOAD_URL.
- Change L36 in order to compile the native image as you used to do:
mvn -Pnative native:compile
- Change L44 and L45 in order to copy and use the
mygraalvmplanet
native image.
The resulting Dockerfile is the following:
FROM ghcr.io/graalvm/native-image:ol8-java17-22 AS builder
# Install tar and gzip to extract the Maven binaries
RUN microdnf update \
&& microdnf install --nodocs \
tar \
gzip \
&& microdnf clean all \
&& rm -rf /var/cache/yum
# Install Maven
# Source:
# 1) https://github.com/carlossg/docker-maven/blob/925e49a1d0986070208e3c06a11c41f8f2cada82/openjdk-17/Dockerfile
# 2) https://maven.apache.org/download.cgi
ARG USER_HOME_DIR="/root"
ARG SHA=1ea149f4e48bc7b34d554aef86f948eca7df4e7874e30caf449f3708e4f8487c71a5e5c072a05f17c60406176ebeeaf56b5f895090c7346f8238e2da06cf6ecd
ARG MAVEN_DOWNLOAD_URL=https://dlcdn.apache.org/maven/maven-3/3.9.0/binaries/apache-maven-3.9.0-bin.tar.gz
RUN mkdir -p /usr/share/maven /usr/share/maven/ref \
&& curl -fsSL -o /tmp/apache-maven.tar.gz ${MAVEN_DOWNLOAD_URL} \
&& echo "${SHA} /tmp/apache-maven.tar.gz" | sha512sum -c - \
&& tar -xzf /tmp/apache-maven.tar.gz -C /usr/share/maven --strip-components=1 \
&& rm -f /tmp/apache-maven.tar.gz \
&& ln -s /usr/share/maven/bin/mvn /usr/bin/mvn
ENV MAVEN_HOME /usr/share/maven
ENV MAVEN_CONFIG "$USER_HOME_DIR/.m2"
# Set the working directory to /home/app
WORKDIR /build
# Copy the source code into the image for building
COPY . /build
# Build
RUN mvn -Pnative native:compile
# The deployment Image
FROM docker.io/oraclelinux:8-slim
EXPOSE 8080
# Copy the native executable into the containers
COPY --from=builder /build/target/mygraalvmplanet .
ENTRYPOINT ["/mygraalvmplanet"]
Build the Docker image. Relax, this will take quite some time.
$ docker build . --tag mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT -f Dockerfile-native-image-oracle-linux
This image size is 177MB.
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
mydeveloperplanet/mygraalvmplanet 0.0.1-SNAPSHOT 57e0fda006f0 9 seconds ago 177MB
Run the container and it starts in 55ms.
$ docker run --name mygraalvmplanet mydeveloperplanet/mygraalvmplanet:0.0.1-SNAPSHOT
...
2023-02-26T13:13:50.188Z INFO 1 --- [ main] c.m.m.MyGraalVmPlanetApplication : Started MyGraalVmPlanetApplication in 0.055 seconds (process running for 0.061)
So, this works just fine. This is the way to go when creating Docker images for your GraalVM native image:
- Prepare a Docker image based on your target base image;
- Install the necessary tooling, in the case of this application, GraalVM and Maven;
- Use a multistage Docker build in order to create the target image.
Conclusion
Creating a Docker image for your GraalVM native image is possible, but you need to be aware of what you are doing. Using a multistage build is the best option. Dependent on whether you need to shrink the size of the image by using a distroless image, you need to prepare the image to build the native image yourself.
Published at DZone with permission of Gunter Rotsaert, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments