Multi-Cloud Apps with GraalVM - Up and Running
Introduction
This workshop is for developers looking to understand better how to build size-optimized cloud native Java applications using GraalVM Native Image . You are going to discover ways to minimize application footprint by taking advantage of different Native Image linking options and packaging into various base containers.
For the demo part, you will run a Spring Boot web server application, hosting the GraalVM website. Spring Boot 3 has integrated support for GraalVM Native Image, making it easier to set up and configure a project. Compiling a Spring Boot application ahead of time can significantly improve the startup time, and reduce its footprint.
Workshop Objectives
In this workshop you will:
- Learn how to compile a Spring Boot application ahead-of-time into a native executable and optimize it for file size.
- See how to use the GraalVM Native Image Maven Plugin .
- Create several native executables and run them inside different containers.
- Shrink a container image size by taking advantage of different Native Image containerisation and linking options.
- Compare the deployed container images sizes.
- See how to use GitHub Actions to automate the build of native executables as part of a CI/CD pipeline.
Estimated workshop time is 120 minutes.
Workshop Environment
You are not required to install any software for the workshop. The session will run on an interactive workshop platform in the Oracle Cloud Infrastructure (OCI): https://luna.oracle.com/ . Some key facts:
- You are provided with an ephemeral account and necessary OCI resources for the time.
- The development environment comes pre-configured.
- All resources will be cleaned up after completing this workshop.
- Remote control over users' sessions is possible in case of trouble.
NOTE: If you see the laptop icon in the lab, this means you need to do something such as enter a command. Keep an eye out for it.
# This is where you will need to do something
STEP 1: Connect to a Virtual Host and Check the Development Environment
Your development environment is provided by a remote host: an OCI Compute Instance with Oracle Linux 8, 4 cores, and 48GB of memory.
Start by connecting to your remote host.
Double-click the Luna Lab icon on the desktop to open the browser:
Check if the host is ready under the Resources tab. The cog shown next to the Resources title spins while the compute host is being provisioned in the cloud. When the host is provisioned (this may take up to 2 minutes), you will see a checkmark:
A Visual Studio (VS) Code window will open and automatically connect to your remote host that has been provisioned for you. Click Continue to accept the machine fingerprint.
The VS Code window opens, connected to your remote host, with the source code for this lab.
If you don't hit
Continue
VS Code will show you a dialogue, shown below. HitRetry
and VS Code will ask you to accept the machine fingerprint. Hit Continue, as in the above step.You are now successfully connected to your remote host in Oracle Cloud!
Next, open a terminal within VS Code.
This terminal will allow you to interact with the remote host. A terminal can be opened in VS Code through the menu: Terminal > New Terminal. Use this terminal in the rest of the lab.
VS Code may prompt you to install plugins and extensions. Ignore/close the notification prompts as this lab doesn't need any plugins and extensions.
Issues With Connecting to the Remote Development Environment
If you encounter any issues with connecting to the remote development environment in VS Code, that are not covered above, we suggest that you try the following:
- Close VS Code
- Copy the Configure Script from the Resources tab and paste it into the Luna Desktop Terminal again
- Repeat the above instructions to connect to the Remote Development environment
Note on the Development Environment
Your Java runtime environment comes preconfigured with Oracle GraalVM for JDK 21 . Docker is also installed and running.
You can easily check that by running these commands in the terminal:
java -version
native-image --version
STEP 2: Compile and Run the Application from a JAR File Inside a Container
Start by compiling and running the application from a JAR file inside a Docker container. It requires a container image with a full JDK and runtime libraries.
Explanation
The Dockerfile, provided for this step, Dockerfile.distroless-base.uber-jar, uses Oracle GraalVM for JDK 21 container image for the builder, and Debian Slim Linux image for the runtime.
The entrypoint for this image is equivalent to java -jar
, so only a path to a JAR file is specified in CMD
.
Action
Run the build-jar.sh script from the application directory:
./build-jar.sh
Once the script finishes, a container image debian-slim.jar should be available. Start the application using
docker run
:docker run --rm -p8080:8080 webserver:debian-slim.jar
The container started in hundreds of milliseconds, 1.135 seconds.
Go to the Luna window that you opened in the browser, and get a complete URL for the workshop user under Resources. Copy and paste in a browser. You see the GraalVM documentation pages served.
If you are running the example locally, not on a remote host, just open http://localhost:8080 .
Return to VS Code and stop the running container by clicking CTRL+C. (The Docker runs in an attached mode.)
Check the container size:
docker images
The expected size is ~240MB. Note that the website pages added 44MB to the container size.
REPOSITORY TAG IMAGE ID CREATED SIZE webserver debian-slim.jar 5c69f06a3972 About an hour ago 238MB
STEP 3: Build and Run a Jlink Custom Runtime Image Inside a Container
In this step, you will create a custom runtime of this Spring Boot web server with Jlink and run it inside a container image. See how much of reduction in size you can gain.
Explanation
Jlink, or jlink
, is a tool that generates a custom Java runtime image that contains only the platform modules that are required for your application. This is one of the approaches to create cloud native applications introduced in Java 11.
The script build-jlink.sh that runs docker build
using the Dockerfile.distroless-java-base.jlink.
The Dockerfile contains a multistage build: first it generates a Jlink custom runtime on a full JDK; then copies the runtime image folder along with static website pages into a Java base container image, and sets the entrypoint.
The application does not have to be modular, but you need to figure out which modules the application depends on to be able to jlink
it.
In the builder stage, running on a full JDK, after compiling the project, Docker generates a file cp.txt containing the classpath with all the dependencies:
RUN ./mvnw dependency:build-classpath -Dmdep.outputFile=cp.txt
Then, Docker runs the jdeps
command with the classpath to check required modules for this Spring Boot application:
RUN jdeps --ignore-missing-deps -q --recursive --multi-release 21 --print-module-deps --class-path $(cat cp.txt) target/webserver-0.0.1-SNAPSHOT.jar
Finally, Docker runs jlink
to create a custom runtime in the specified output directory jlink-jre.
The ENTRYPOINT
for the application would be java
from that custom runtime.
Action
Run the script:
./build-jlink.sh
Once the script finishes, a container image distroless-java-base.jlink should be available. Run it, mapping the ports:
docker run --rm -p8080:8080 webserver:distroless-java-base.jlink
The container started in 1.224 seconds.
Go to the browser window and check if the website is running at http://<SERVER_IP>:8080/.
Return to VS Code and stop the running container by clicking CTRL+C.
Compare the file size of container images:
docker images
The expected output is:
REPOSITORY TAG IMAGE ID CREATED SIZE webserver distroless-java-base.jlink 687f7683ad58 16 minutes ago 192MB webserver debian-slim.jar 5c69f06a3972 2 hours ago 238MB
Jlink shrinked the container by 46MB.
STEP 4: Build and Run a Native Image Inside a Container Using Paketo Buildpacks
In this step, you will compile this Spring Boot application ahead of time with GraalVM Native Image and run it using Paketo Buildpacks container images.
Explanation
Spring Boot supports building a native image in a container using the Paketo Buildpack for Oracle which provides GraalVM Native Image.
The mechanism is that the Paketo builder pulls the Jammy Tiny Stack image (Ubuntu distroless-like image) which contains no buildpacks. Then you point the builder image to the creator image. For this workshop, you point to the Paketo Buildpack for Oracle explicitly requesting the Native Image tool.
If you open the pom.xml file, you see the buildpacks configuration within the spring-boot-maven-plugin
declaration added for you:
<configuration>
<image>
<builder>paketobuildpacks/builder-jammy-buildpackless-tiny</builder>
<buildpacks>
<buildpack>paketobuildpacks/oracle</buildpack>
<buildpack>paketobuildpacks/java-native-image</buildpack>
</buildpacks>
</image>
</configuration>
When java-native-image
is requested, the buildpack downloads Oracle GraalVM, which includes Native Image.
The Paketo documentation provides several examples that show you how to build applications with Native Image using buildpacks.
Note that if you do not specify Oracle's buildpack, it will pull the default buildpack, which can result in reduced performance.
Action
Build a native executable for this Spring application using the Paketo buildpack:
./mvnw -Pnative spring-boot:build-image
Once the build completes, a container image 0.0.1-SNAPSHOT should be available. Run it, mapping the ports:
docker run --rm -p8080:8080 docker.io/library/webserver:0.0.1-SNAPSHOT
The application is running from the native image inside a container. The container started in just several milliseconds 0.031 seconds!
Go to the browser window and check if the website is running at http://<SERVER_IP>:8080/.
Return to VS Code and stop the running container by clicking CTRL+C.
Check the size of this container image:
docker images
The expected output is:
REPOSITORY TAG IMAGE ID CREATED SIZE webserver distroless-java-base.jlink 687f7683ad58 31 minutes ago 192MB webserver debian-slim.jar 5c69f06a3972 2 hours ago 238MB webserver 0.0.1-SNAPSHOT 0660806da4a2 44 years ago 163MB
The new container, tagged as 0.0.1-SNAPSHOT, is much smaller now 163MB.
STEP 5: Build a Native Image Locally and Run Inside a Container (Default Configuration)
In this step, you will create a native image with the default configuration on a host machine, and only run it inside a container.
Explanation
Spring Boot 3 has integrated support for GraalVM Native Image, making it easier to set up and configure your project. The support is based on Native Build Tools providing Maven and Gradle plugins for building native images and maintained by the GraalVM team. The project configuration already contains necessary plugins, including Native Image Maven plugin :
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
</plugin>
You can build this web server ahead of time into a native executable, on your host machine, just like this: ./mvnw -Pnative native:compile
. The command will compile the application and create a fully dynamically linked native image, webserver
, in the target/ directory.
However, there is a script build-dynamic-image.sh, for your convenience, that does that and packages this native image in a distroless base container image with just enough to run the application. No Java Runtime Environment (JRE) is required!
Distroless container images contain only your application and its runtime dependencies. They do not contain package managers, shells or any other programs you would expect to find in a standard Linux distribution. Learn more in "Distroless" Container Images .
Action
Run the script:
./build-dynamic-image.sh
Once the build completes, a container image distroless-java-base.dynamic should be available. Run it, mapping the ports:
docker run --rm -p8080:8080 webserver:distroless-java-base.dynamic
The application is running from the native image inside a container. The container started in 0.033 seconds.
Go to the browser window and check if the website is running at http://<SERVER_IP>:8080/.
Return to VS Code and stop the running container by clicking CTRL+C.
Check the size of this container image:
docker images
The expected output is:
REPOSITORY TAG IMAGE ID CREATED SIZE webserver distroless-java-base.dynamic 5a5de47579ef 2 minutes ago 164MB webserver distroless-java-base.jlink 687f7683ad58 37 minutes ago 192MB webserver debian-slim.jar 5c69f06a3972 2 hours ago 238MB webserver 0.0.1-SNAPSHOT 0660806da4a2 44 years ago 163MB
The new container image size, 164MB, almost matches the size of the container built with Paketo Buildpacks. It is expected because it is the same native executable packaged into a different base container.
Check what is the size of the native executable itself:
ls -lh target/webserver
The expected size is 125M. Note that the static resources are "baked" into this native executable and added 44M to its size.
STEP 6: Build a Size-Optimized Native Image Locally and Run Inside a Container
This is where the fun begins.
In this step, you will build a fully dynamically linked native image with the file size optimization on, on a host machine, and run it inside a container.
Explanation
GraalVM Native Image provides the option -Os
which optimizes the resulting native image for file size.
-Os
enables -O2
optimizations except those that can increase code or executable size significantly. Learn more in the Native Image documentation .
For that, a separate Maven profile in pom.xml is provided to differentiate this run from the default build, and to give a different name for the output file:
<profile>
<id>dynamic-size-optimized</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<imageName>webserver.dynamic</imageName>
<buildArgs>
<buildArg>-Os</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
The script build-dynamic-image.sh, available in this repository for your convenience, creates a native image with fully dynamically linked shared libraries, optimized for size, and then packages it in a distroless base container image with just enough to run the application. No Java Runtime Environment (JRE) is required.
The
-Os
optimization will be on for all the subsequent builds.
Action
This step requires to install GraalVM for JDK 23 Early Access Build . Run the installation script:
source ./install_graalvm_23.sh
It will take 1-2 minutes.
Check the Java version to ensure the installation was successful:
java -version
Run the script to build a size-optimized native executable and package it into a container:
./build-dynamic-image-optimized.sh
Once the build completes, a container image distroless-java-base.dynamic-optimized should be available. Run it, mapping the ports:
docker run --rm -p8080:8080 webserver:distroless-java-base.dynamic-optimized
The application is running from the native image inside a container. The container started in 0.035 seconds.
Go to the browser window and check if the website is running at http://<SERVER_IP>:8080/.
Return to VS Code and stop the running container by clicking CTRL+C.
Check the size of this container image:
docker images
The expected output is:
REPOSITORY TAG IMAGE ID CREATED SIZE webserver distroless-java-base.dynamic-optimized 0bed48dadd11 About a minute ago 130MB webserver distroless-java-base.dynamic 5a5de47579ef 16 minutes ago 164MB webserver distroless-java-base.jlink 687f7683ad58 51 minutes ago 192MB webserver debian-slim.jar 5c69f06a3972 2 hours ago 238MB webserver 0.0.1-SNAPSHOT 0660806da4a2 44 years ago 163MB
The size of
distroless-java-base.dynamic-optimized
container is cut down from 164MB to 130MB. This is because the native executable reduced in size.Check what is the size of the native executable now:
ls -lh target/webserver
The size decreased from 125M to 92M by applying the file size optimization.
-rwxr-xr-x. 1 opc opc 125M Aug 29 10:51 target/webserver -rw-r--r--. 1 opc opc 45M Aug 29 10:39 target/webserver-0.0.1-SNAPSHOT.jar -rw-r--r--. 1 opc opc 25M Aug 29 10:39 target/webserver-0.0.1-SNAPSHOT.jar.original -rwxr-xr-x. 1 opc opc 92M Aug 29 11:06 target/webserver.dynamic-optimized
STEP 7: Build a Size-Optimized Mostly Static Native Image Locally and Run Inside a Container
In this step, you will build a mostly static native image, with the file size optimization on, on a host machine, then package it into a container image that provides glibc
, and run.
Explanation
A mostly static native image links all the shared libraries on which it relies (zlib
, JDK-shared static libraries) except the standard C library, libc
. This type of native image is useful for deployment on a distroless base container image.
You can build a mostly statically linked image by passing the --static-nolibc
option at build time.
A separate Maven profile exists for this step:
<profile>
<id>mostly-static</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<imageName>webserver.mostly-static</imageName>
<buildArgs>
<buildArg>--static-nolibc</buildArg>
<buildArg>-Os</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
Action
Run the script:
./build-mostly-static-image.sh
Once the build completes, a container image distroless-base.mostly-static should be available. Run it, mapping the ports:
docker run --rm -p8080:8080 webserver:distroless-base.mostly-static
The application is running from the mostly static native image inside a container. The container started in 0.036 seconds.
Go to the browser window and check if the website is running at http://<SERVER_IP>:8080/.
Return to VS Code and stop the running container by clicking CTRL+C.
Check the size of this container image:
docker images
The expected output is:
REPOSITORY TAG IMAGE ID CREATED SIZE webserver distroless-base.mostly-static b49eec5bdfa6 About a minute ago 117MB webserver distroless-java-base.dynamic-optimized 0bed48dadd11 9 minutes ago 130MB webserver distroless-java-base.dynamic 5a5de47579ef 24 minutes ago 164MB webserver distroless-java-base.jlink 687f7683ad58 About an hour ago 192MB webserver debian-slim.jar 5c69f06a3972 2 hours ago 238MB webserver 0.0.1-SNAPSHOT 0660806da4a2 44 years ago 163MB
The size of the new distroless-base.mostly-static container is 117MB. The reduction in size is related to the fact that a smaller base image was pulled: gcr.io/distroless/base-debian12. Distroless images are very small, and the one used is only 48.3 MB. That's about 50% of the size of java-base-debian12(124 MB) used before, and 3 times less than java21-debian12 (192 MB) containing a full JDK.
The size of the mostly static native image has not changed, and is 92MB.
STEP 8: Build a Size-Optimized Fully Static Native Image Locally and Run Inside a Container
In this step, you will build a fully static native image, with the file size optimization on, on a host machine, then package it into a scratch container.
Explanation
A fully static native image is a statically linked binary that you can use without any additional library dependencies.
You can create a static native image by statically linking it against musl-libc
, which is a lightweight, fast, and simple libc
implementation.
To build a fully static executable, pass the --static --libc=musl
options at build time.
This static native image is easy to deploy on a slim or distroless container, even a scratch container . A scratch container is a Docker official image , useful for building super minimal images.
A separate Maven profile exists for this step:
<profile>
<id>fully-static</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<imageName>webserver.static</imageName>
<buildArgs>
<buildArg>--static --libc=musl</buildArg>
<buildArg>-Os</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
Action
This step requires the
musl
toolchain withzlib
. Run the following script to download and configure themusl
toolchain, and installzlib
into the toolchain:./setup-musl.sh
Run the script to build a fully static native executable and package it into a scratch container:
./build-static-image.sh
Once the build completes, a container image scratch.static should be available. Run it, mapping the ports:
docker run --rm -p8080:8080 webserver:scratch.static
The container started in 0.035 seconds. As a result you get a tiny container with a fully functional and deployable server application!
Go to the browser window and check if the website is running at http://<SERVER_IP>:8080/.
Return to VS Code and stop the running container by clicking CTRL+C.
Double-check if the native executable that was just created is indeed fully static with
ldd
:ldd target/webserver.static
You should see "not a dynamic executable" for the response. Which means that the image does not rely on any libraries in the operating system environment and can be packaged in the tiniest container!
Now check the size of this container image:
docker images
The expected output is:
REPOSITORY TAG IMAGE ID CREATED SIZE webserver scratch.static 6e8b25dacca6 7 minutes ago 96.4MB webserver distroless-base.mostly-static b49eec5bdfa6 25 minutes ago 117MB webserver distroless-java-base.dynamic-optimized 0bed48dadd11 32 minutes ago 130MB webserver distroless-java-base.dynamic 5a5de47579ef 48 minutes ago 164MB webserver distroless-java-base.jlink 687f7683ad58 About an hour ago 192MB webserver debian-slim.jar 5c69f06a3972 3 hours ago 238MB webserver 0.0.1-SNAPSHOT 0660806da4a2 44 years ago 163MB
The container size shrinked to 96.4MB! A scratch container weights only 14.5MB.
STEP 9: Compress a Static Native Image with UPX and Run Inside a Container
Not convincing? What can you do next to reduce the size even more?
You can compress your fully static native image with UPX, then package into the same scratch container, and run.
Explanation
UPX - an advanced executable file compressor. It can significantly reduce the executable size, but note, that UPX loads the executable into the memory, unpackages it, and then recompresses.
Download and install UPX:
./setup-upx.sh
Run the script to compress the fully static executable, created at the previous step, and package it into a scratch container.
./build-static-upx-image.sh
Once the build completes, a container image scratch.static-upx should be available. Run it, mapping the ports:
docker run --rm -p8080:8080 webserver:scratch.static-upx
The container started in 0.035 seconds.
Go to the browser window and check if the website is still running at http://<SERVER_IP>:8080/.
Return to VS Code and stop the running container by clicking CTRL+C.
Now check how much
upx
compressed the static native image:ls -lh target/webserver*
The expected output is:
-rwxr-xr-x. 1 opc opc 125M Aug 29 10:51 target/webserver -rw-r--r--. 1 opc opc 45M Aug 29 11:30 target/webserver-0.0.1-SNAPSHOT.jar -rwxr-xr-x. 1 opc opc 92M Aug 29 11:06 target/webserver.dynamic-optimized -rwxr-xr-x. 1 opc opc 92M Aug 29 11:14 target/webserver.mostly-static -rwxr-xr-x. 1 opc opc 92M Aug 29 11:32 target/webserver.static -rwxr-xr-x. 1 opc opc 35M Aug 29 11:32 target/webserver.static-upx
The webserver.static-upx is only 35MB! The
upx
compressed executable by 57MB from the "uncompressed" one.Lastly, check the size of all container images:
docker images
The expected output is:
REPOSITORY TAG IMAGE ID CREATED SIZE webserver scratch.static-upx c87bfe44c7fb 6 seconds ago 36.2MB webserver scratch.static 6e8b25dacca6 7 minutes ago 96.4MB webserver distroless-base.mostly-static b49eec5bdfa6 25 minutes ago 117MB webserver distroless-java-base.dynamic-optimized 0bed48dadd11 32 minutes ago 130MB webserver distroless-java-base.dynamic 5a5de47579ef 48 minutes ago 164MB webserver distroless-java-base.jlink 687f7683ad58 About an hour ago 192MB webserver debian-slim.jar 5c69f06a3972 3 hours ago 238MB webserver 0.0.1-SNAPSHOT 0660806da4a2 44 years ago 163MB
The container size reduced dramatically to just 36.2MB. The application and container image's size were now shrinked to the minimum.
Conclusions
A fully functional and, at the same time, minimal, Java application was compiled into a native Linux executable and packaged into base, distroless, and scratch containers thanks to GraalVM Native Image's support for various linking options. All the versions of this Spring Boot application are functionally equivalent.
Sorted by size, it is clear that the fully static native image, compressed with upx
, and then packaged in the scratch container is the smallest at just 36.2MB. Note that the website static pages added 44M to the container images size. Static resources are "baked” into native images.
Container | Size of a build artefact (JAR, Jlink runtime, native executable) | Base image | Container |
---|---|---|---|
debian-slim.jar | webserver-0.0.1-SNAPSHOT.jar 44MB | 192 MB | 249MB |
distroless-java-base.jlink | jlink-jre custom runtime 68MB | 128 MB | 202MB |
distroless-java-base.dynamic | webserver 125MB | 128 MB | 164MB |
0.0.1-SNAPSHOT | 163MB | ||
distroless-java-base.dynamic-optimized | webserver-optimized 92MB | 128 MB | 130MB |
distroless-base.mostly-static | webserver.mostly-static 92MB | 48.3 MB | 117MB |
scratch.static | webserver.scratch.static 92MB | 14.5 MB | 96.4MB |
scratch.static-upx | webserver.scratch.static-upx 35MB | 14.5 MB | 36.2MB |
Learn More
Multi-Cloud Apps with GraalVM - Up and Running
STEP 1: Connect to a Virtual Host and Check the Development Environment
STEP 2: Compile and Run the Application from a JAR File Inside a Container
STEP 3: Build and Run a Jlink Custom Runtime Image Inside a Container
STEP 4: Build and Run a Native Image Inside a Container Using Paketo Buildpacks
STEP 5: Build a Native Image Locally and Run Inside a Container (Default Configuration)
STEP 6: Build a Size-Optimized Native Image Locally and Run Inside a Container
STEP 7: Build a Size-Optimized Mostly Static Native Image Locally and Run Inside a Container
STEP 8: Build a Size-Optimized Fully Static Native Image Locally and Run Inside a Container
STEP 9: Compress a Static Native Image with UPX and Run Inside a Container