Quarkus - Building a Native Executable

This guide covers:

  • Compiling the application to a native executable

  • The packaging of the application in a container

This guide takes as input the application developed in the Getting Started Guide.

Prerequisites

To complete this guide, you need:

  • less than 15 minutes

  • an IDE

  • JDK 1.8+ installed with JAVA_HOME configured appropriately

  • GraalVM installed from the GraalVM web site. Using the community edition is enough. Version 19.1.1 is required.

  • The GRAALVM_HOME environment variable configured appropriately

  • The native-image tool must be installed; this can be done by running gu install native-image from your GraalVM directory

  • Apache Maven 3.5.3+

  • A working C developer environment (see the note below for details)

  • A running Docker

  • The code of the application developed in the Getting Started Guide.

Once you have downloaded GraalVM, expand the archive and set the GRAALVM_HOME variable to this location:

export GRAALVM_HOME=$HOME/Development/graalvm/

On MacOS, point the variable to the Home sub-directory:

export GRAALVM_HOME=$HOME/Development/graalvm/Contents/Home/

What does having a working C developer environment mean?

  • On Linux, you will need GCC, the glibc and zlib headers (on common distributions: sudo dnf install gcc glibc-devel zlib-devel or sudo apt-get install build-essential libz-dev).

  • On macOS, execute xcode-select --install.

Some previous releases of GraalVM included the native-image tool by default. This is no longer the case; it now has to be installed as a second step after GraalVM itself is installed. Use gu install native-image to do so.

If you cannot install GraalVM, you can use a multi-stage Docker build to run Maven inside a Docker container that embeds GraalVM. There is an explanation of how to do this at the end of this guide.

Solution

We recommend that you follow the instructions in the next sections and package the application step by step. However, you can go right to the completed example.

Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git, or download an archive.

The solution is located in the getting-started directory.

Producing a native executable

Let’s now produce a native executable for our application. It improves the startup time of the application, and produces a minimal disk footprint. The executable would have everything to run the application including the "JVM" (shrunk to be just enough to run the application), and the application.

Creating a native executable

Before going further, be sure that the GRAALVM_HOME environment variable is configured appropriately.

If you have generated the application from the previous tutorial, you can find in the pom.xml the following profile:

<profiles>
    <profile>
        <id>native</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>io.quarkus</groupId>
                    <artifactId>quarkus-maven-plugin</artifactId>
                    <version>${quarkus.version}</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>native-image</goal>
                            </goals>
                            <configuration>
                                <enableHttpUrlHandler>true</enableHttpUrlHandler>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

You can provide additional options for native-image command using <additionalBuildArgs>foo,bar</additionalBuildArgs> configuration element. Provided options have to be , separated.

We use a profile because, you will see very soon, packaging the native executable takes a few seconds.

Create a native executable using: ./mvnw package -Pnative.

In addition to the regular files, the build also produces target/getting-started-1.0-SNAPSHOT-runner. You can run it using: ./target/getting-started-1.0-SNAPSHOT-runner.

Testing the native executable

Producing a native executable can lead to a few issues, and so it’s also a good idea to run some tests against the application running in the native file.

In the pom.xml file, the native profile contains:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>${surefire-plugin.version}</version>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
            <configuration>
                <systemProperties>
                    <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
                </systemProperties>
            </configuration>
        </execution>
    </executions>
</plugin>

This instructs the failsafe-maven-plugin to run integration-test and indicates the location of the produced native executable.

Then, open the src/test/java/org/acme/quickstart/NativeGreetingResourceIT.java. It contains:

package org.acme.quickstart;


import io.quarkus.test.junit.SubstrateTest;

@SubstrateTest (1)
public class NativeGreetingResourceIT extends GreetingResourceTest { (2)

    // Run the same tests

}
1 Use another test runner that starts the application from the native file before the tests. The executable is retrieved using the native.image.path system property configured in the Failsafe Maven Plugin.
2 We extend our previous tests, but you can also implement your tests

To see the NativeGreetingResourceIT run against the native executable, use ./mvnw verify -Pnative:

./mvnw verify -Pnative
...
[getting-started-1.0-SNAPSHOT-runner:18820]     universe:     587.26 ms
[getting-started-1.0-SNAPSHOT-runner:18820]      (parse):   2,247.59 ms
[getting-started-1.0-SNAPSHOT-runner:18820]     (inline):   1,985.70 ms
[getting-started-1.0-SNAPSHOT-runner:18820]    (compile):  14,922.77 ms
[getting-started-1.0-SNAPSHOT-runner:18820]      compile:  20,361.28 ms
[getting-started-1.0-SNAPSHOT-runner:18820]        image:   2,228.30 ms
[getting-started-1.0-SNAPSHOT-runner:18820]        write:     364.35 ms
[getting-started-1.0-SNAPSHOT-runner:18820]      [total]:  52,777.76 ms
[INFO]
[INFO] --- maven-failsafe-plugin:2.22.1:integration-test (default) @ getting-started ---
[INFO]
[INFO] -------------------------------------------------------
[INFO]  T E S T S
[INFO] -------------------------------------------------------
[INFO] Running org.acme.quickstart.NativeGreetingResourceIT
Executing [/data/home/gsmet/git/quarkus-quickstarts/getting-started/target/getting-started-1.0-SNAPSHOT-runner, -Dquarkus.http.port=8081, -Dtest.url=http://localhost:8081, -Dquarkus.log.file.path=build/quarkus.log]
2019-04-15 11:33:20,348 INFO  [io.quarkus] (main) Quarkus 999-SNAPSHOT started in 0.002s. Listening on: http://[::]:8081
2019-04-15 11:33:20,348 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy]
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.387 s - in org.acme.quickstart.NativeGreetingResourceIT
...

By default, Quarkus waits for 60 seconds for the native image to start before automatically failing the native tests. This duration can be changed using the quarkus.test.native-image-wait-time system property. For example, to increase the duration to 300 seconds, use: ./mvnw verify -Pnative -Dquarkus.test.native-image-wait-time=300.

Excluding tests when running as a native executable

When running tests this way, the only things that actually run natively are you application endpoints, which you can only test via HTTP calls. Your test code does not actually run natively, so if you are testing code that does not call your HTTP endpoints, it’s probably not a good idea to run them as part of native tests.

If you share your test class between JVM and native executions like we advise above, you can mark certain tests with the @DisabledOnSubstrate annotation in order to only run them on the JVM.

Creating a container

Before going further, be sure to have a working container runtime (Docker, podman) environment.

You can run the application in a container using the JAR produced by the Quarkus Maven Plugin. However, in this guide we focus on creating a container image using the produced native executable.

Containerization Process

By default, the native executable is tailored for your operating system (Linux, macOS, Windows etc). Because the container may not use the same executable format as the one produced by your operating system, we will instruct the Maven build to produce an executable from inside a container:

./mvnw package -Pnative -Dnative-image.docker-build=true

You can also select the container runtime to use with:

# Docker
./mvnw package -Pnative -Dnative-image.container-runtime=docker
# Podman
./mvnw package -Pnative -Dnative-image.container-runtime=podman

The produced executable will be a 64 bit Linux executable, so depending on your operating system it may no longer be runnable. However, it’s not an issue as we are going to copy it to a container. The project generation has provided a Dockerfile.native in the src/main/docker directory with the following content:

FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

Then, if you didn’t delete the generated native executable, you can build the docker image with:

docker build -f src/main/docker/Dockerfile.native -t quarkus-quickstart/getting-started .

And finally, run it with:

docker run -i --rm -p 8080:8080 quarkus-quickstart/getting-started
Interested by tiny Docker images, check the distroless version.

Creating a container with a multi-stage Docker build

The previous section showed you how to build a native executable using Maven, but implicitly required that the proper GraalVM version be installed on the building machine (be it your local machine or your CI/CD infrastructure).

In cases where the GraalVM requirement cannot be met, you can use Docker to perform the Maven build by using a multi-stage Docker build. A multi-stage Docker build is like two Dockerfile files combined in one, the first is used to build the artifact used by the second.

In this guide we will use the first stage to generate the native executable using Maven and the second stage to create our runtime image.

## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/centos-quarkus-maven:19.1.1 AS build
COPY src /usr/src/app/src
COPY pom.xml /usr/src/app
USER root
RUN chown -R quarkus /usr/src/app
USER quarkus
RUN mvn -f /usr/src/app/pom.xml -Pnative clean package

## Stage 2 : create the docker final image
FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY --from=build /usr/src/app/target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

Save this file in src/main/docker/Dockerfile.multistage as it is not included in the getting started quickstart.

Before launching our Docker build, we need to update the default .dockerignore file as it filters everything except the target directory and as we plan to build inside a container we need to be able to copy the src directory. So edit your .dockerignore and remove or comment its content.

docker build -f src/main/docker/Dockerfile.multistage -t quarkus-quickstart/getting-started .

And finally, run it with:

docker run -i --rm -p 8080:8080 quarkus-quickstart/getting-started

If you need SSL support in your native executable, you can easily include the necessary libraries in your Docker image.

Please see our Using SSL With Native Executables guide for more information.

What’s next?

This guide covered the creation of a native (binary) executable for your application. It provides an application exhibiting a swift startup time and consuming less memory. However, there is much more. We recommend continuing the journey with the deployment to Kubernetes and OpenShift.