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.
Documentation and links:
- Docker documentation: Use multi-stage builds - https://docs.docker.com/build/building/multi-stage/
- Quarkus Docker guide - https://quarkus.io/guides/building-native-image#building-a-container
- Quarkus Uber Jar creation - https://quarkus.io/guides/maven-tooling#uber-jar-maven