In this article, we continue the trend from the previous article here and we will explore a few ways of creating a Docker image for a Quarkus app.

Minimal install docker file

Now with Quarkus, there are two ways of compiling a application: the normal java way and the native way.

Quarkus native images and Java application packages have some significant differences. Java application packages contain a compiled bytecode that requires the JVM to run, while Quarkus native images are pre-compiled and optimized for a specific platform (in this example Linux), resulting in a smaller size and faster startup time.

However, in the case of Quarkus native images certain dynamic features may not be available (like reflection, dynamic proxies, and bytecode generation).

Minimal Dockerfile for Quarkus java app

Example Dockerfile:

FROM eclipse-temurin:17-jre-alpine
WORKDIR /work/
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
COPY --chown=javauser:javauser target/*-runner.jar app.jar
# if this is NOT a uber jar build, add the next line
# COPY --chown=javauser:javauser target/lib/ lib/
USER javauser
ENTRYPOINT ["java", "-jar", "app.jar"]

This Dockerfile is based on the Alpine image with Temurin JDK 17 installed. It sets the working directory to /work/, creates a system user called javauser, copies the generated Quarkus runner JAR file to /work/, sets the ownership to the javauser user, and sets the user to javauser. Finally, the entry point is defined as java -jar app.jar to start the application.

Now, let’s check the size of the docker image:

REPOSITORY         TAG                 IMAGE ID       CREATED         SIZE

quarkus-java       latest              8784be59c952   4 seconds ago    186MB

Minimal Dockerfile for native Quarkus app

Example Dockerfile:

#FROM registry.access.redhat.com/ubi8/ubi-minimal:8.7
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

In this case, we are using the Red Hat Universal Base Image (UBI) minimal image version 8.7.

In this case we are using the quarkus-micro-image which is a pre-built image containing the Quarkus runtime and necessary dependencies to run Quarkus applications in a containerized environment.

This Dockerfile is similar to the previous one, but it copies the native image to /work/application.

Before build the docker image, please run the ./mvnw package -Pnative command to build the native image

Now, to check the size of the docker image:

REPOSITORY         TAG                 IMAGE ID       CREATED         SIZE

quarkus-native     latest              4c9e63aaf38a   5 seconds ago   74.2MB

As part of an exercise, I tested the resulting image size by using the registry.access.redhat.com/ubi8/ubi-minimal:8.7 base image. The resuting image is larger then the one recommended by quarkus (quarkus-micro-image).

REPOSITORY         TAG                 IMAGE ID       CREATED              SIZE

quarkus-native     latest              f0f20f65310e   About a minute ago   139MB

Multistage build

Using a multi-stage build Dockerfile for Quarkus provides several benefits, such as eliminating the need for build tools to be installed. It also allows for greater control over the build environment and enables building the application for various platforms, including native images.

The first stage of the Dockerfile uses a Maven builder image with native capabilities to build the Quarkus application.

The second stage of the Dockerfile creates the final Docker image that runs the Quarkus application.

Multistage build for Quarkus java app

Example of a multistage Docker file:

# Build stage
FROM maven:3.9.1-eclipse-temurin-17-alpine AS build
WORKDIR /code
COPY pom.xml /code/
COPY src /code/src
RUN mvn clean package

# Run stage
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
RUN addgroup --system javauser && adduser -S -s /usr/sbin/nologin -G javauser javauser
COPY --from=build --chown=javauser:javauser /code/target/*-runner.jar /app/app.jar
# if this is NOT a uber jar build, add the next line
# COPY --from=build --chown=javauser:javauser target/lib/ lib/
USER javauser
CMD ["java", "-jar", "app.jar"]

In the build stage, it starts with a Maven-based image, copies the source code and pom.xml file to /code directory, and runs mvn clean package to build the Java application.

In the run stage, it uses a minimal JRE image, creates a system user and group called javauser, copies the compiled Java artifact from the build stage to /app directory, sets the ownership of the /app directory to javauser, switches to the javauser user, and runs the Java application with the command java -jar app.jar.

This Dockerfile uses multi-stage builds to keep the final image small by discarding the build environment and intermediate artifacts from the final image.

Now, to check the size of the docker image:

REPOSITORY         TAG                 IMAGE ID       CREATED         SIZE

quarkus-java-multi latest              a3a357e26539   8 seconds ago    186MB

Multistage build for native Quarkus app

Example Dockerfile:

## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/ubi-quarkus-graalvmce-builder-image:22.3-java17 AS build
COPY --chown=quarkus:quarkus mvnw /code/mvnw
COPY --chown=quarkus:quarkus .mvn /code/.mvn
COPY --chown=quarkus:quarkus pom.xml /code/
USER quarkus
WORKDIR /code
COPY src /code/src
RUN ./mvnw package -Pnative

## Stage 2 : create the docker final image
FROM quay.io/quarkus/quarkus-micro-image:2.0
WORKDIR /app/

# set up permissions for user `1001`
COPY --from=build --chown=1001:root --chmod="g+rwX" /code/target/*-runner /app/application

EXPOSE 8080
USER 1001

CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

This Dockerfile is similar with the previous one.

The interesting part is the line COPY command in a Dockerfile is used to copy the application built in a previous build stage to the current build stage. The --from=build option specifies the build stage to copy from, and the source files are specified as /code/target/*-runner. The destination folder for the copied files is /app/application, with the options --chown=1001:root setting ownership and --chmod="g+rwX" setting file permissions. This command ensures that the application files are copied with the correct ownership and permissions into the container image.`

Now, to check the size of the docker image:

REPOSITORY         TAG                 IMAGE ID       CREATED              SIZE

quarkus-native     latest              d7f1287a0630   6 seconds ago       74.2MB

Docker images sizes

We compared the sizes of Docker images for a Quarkus app using 4 different build methods. The results are shown below:

REPOSITORY         TAG                 IMAGE ID       CREATED         SIZE

quarkus-java       latest              8784be59c952   4 seconds ago    186MB
quarkus-native     latest              4c9e63aaf38a   5 seconds ago   74.2MB
quarkus-java-multi latest              a3a357e26539   8 seconds ago    186MB
quarkus-native     latest              d7f1287a0630   6 seconds ago   74.2MB

It’s clear that the native Quarkus Docker image is significantly smaller than its Java counterpart - about 112MB smaller.