6

How to Create a GraalVM Docker Image

 11 months ago
source link: https://mydeveloperplanet.com/2023/03/29/how-to-create-a-graalvm-docker-image/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

In 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 more tricky than what you are used to when creating Docker images. Enjoy!

1. 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 at GitHub.

A good starting point for learning, is the information provided in the GraalVM documentation. It is good reference material when reading this blog.

As an example application, you will use the Spring Boot application from a 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!

2. 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.

3. 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 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, you 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 an 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

4. 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) and 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 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)

5. 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 it 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 to copy 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.

6. Create Docker Image Based On Oracle Linux

Another approach for creating Docker images is the one that can be found in 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:
ARG USER_HOME_DIR="/root"
ARG SHA=1ea149f4e48bc7b34d554aef86f948eca7df4e7874e30caf449f3708e4f8487c71a5e5c072a05f17c60406176ebeeaf56b5f895090c7346f8238e2da06cf6ecd
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 for this application, GraalVM and Maven;
  • Use a multistage Docker build in order to create the target image.

7. 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 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.

Share this:

Loading...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK