Dockerizing Quarkus

Mohamad Yassine
8 min readFeb 24, 2021

In this medium, we will be talking about fully dockerizing Quarkus. Luckily for us, Quarkus usually generates the DockerFile for us. We only have to do some modification to establish full dockerization. Afterwards, we will move our application properties to a secure environment file. Finally, we will talk about the remote developing with Quarkus.

The DockerFile

What we have

Lucking for us, Quarkus creates not 1, not 2, but 3 DockerFiles for us to use at our leisure. For this example, we will be using the ‘DockerFile.fast-jar’ file. Unfortunately for us, the dockers provided for us requires Java and Maven (Or we can simply use ./mvnw) to be present on our local machine, and this defeats the purpose of Docker. The changes we want to add involves the docker multi-stage builds. As of writing, the current Quarkus latest final version is 1.11.3, and they provide us with the following DockerFile.

FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3 
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
&& chown 1001 /deployments \
&& chmod "g+rwX" /deployments \
&& chown 1001:root /deployments \
&& curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=1001 /target/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001 /target/quarkus-app/*.jar /deployments/
COPY --chown=1001 /target/quarkus-app/app/ /deployments/app/
COPY --chown=1001 /target/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]

Easy enough, this simply informs docker to get the image that is present in the “FROM“ keyword, execute a couple of commands to prepare the environment, and lastly copy the generated jars that were built into the docker container. For more on the docker syntax check this link here. Please note that in order for this docker build to execute correctly, we have to execute the following command prior to building the docker container:

mvn clean package -Dquarkus.package.type=mutable-jar

We may add the argument “-DskipTests=true” to skip running test for time purposes.

What we need to do

This defeats the purpose of docker and forces anyone who simply just wants to build and run our container to install maven and java. To change that, we have to add a new stage which we will call the “builder stage“ before the running stage which is shown below.

FROM maven:3-openjdk-11 AS builder
COPY ./pom.xml /usr/src/app/
RUN mvn -f /usr/src/app/pom.xml -B de.qaware.maven:go-offline-maven-plugin:resolve-dependencies
COPY ./.env /usr/src/app/
COPY src /usr/src/app/src
WORKDIR /usr/src/app
RUN mvn -DskipTests=true -Dquarkus.package.type=mutable-jar -B de.qaware.maven:go-offline-maven-plugin:resolve-dependencies clean package

As you noticed, we will be getting a docker image that has both java and maven. Afterwards, we copy our “pom.xml” and “src” into our container. Finally, we set our working directory to that where everything is copied and build the project.

Again, we may add the argument “-DskipTests=true” to skip running test for time purposes.

We added the argument “dependency:go-offline” for us to keep all downloaded maven repositories and not re-download them each time we want to build our project.

Afterwards we want to copy the generated build into the deployment directory instead of copying the build from our local machine. This can be done by replace the 4 copy commands with the following:

COPY --chown=1001 --from=builder /usr/src/app/target/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001 --from=builder /usr/src/app/target/quarkus-app/*.jar /deployments/
COPY --chown=1001 --from=builder /usr/src/app/target/quarkus-app/app/ /deployments/app/
COPY --chown=1001 --from=builder /usr/src/app/target/quarkus-app/quarkus/ /deployments/quarkus/

With these updated, we simply stated to get the files from the previous stage and we set the directory they were generated in.

Finally we will get our docker file looking like this:

FROM maven:3-openjdk-11 AS builder
COPY ./pom.xml /usr/src/app/
RUN mvn -f /usr/src/app/pom.xml -B de.qaware.maven:go-offline-maven-plugin:resolve-dependencies
COPY ./.env /usr/src/app/
COPY src /usr/src/app/src
WORKDIR /usr/src/app
RUN mvn -DskipTests=true -Dquarkus.package.type=mutable-jar -B de.qaware.maven:go-offline-maven-plugin:resolve-dependencies clean package
FROM registry.access.redhat.com/ubi8/ubi-minimal:8.3
ARG JAVA_PACKAGE=java-11-openjdk-headless
ARG RUN_JAVA_VERSION=1.3.8
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en'
# Install java and the run-java script
# Also set up permissions for user `1001`
RUN microdnf install curl ca-certificates ${JAVA_PACKAGE} \
&& microdnf update \
&& microdnf clean all \
&& mkdir /deployments \
&& chown 1001 /deployments \
&& chmod "g+rwX" /deployments \
&& chown 1001:root /deployments \
&& curl https://repo1.maven.org/maven2/io/fabric8/run-java-sh/${RUN_JAVA_VERSION}/run-java-sh-${RUN_JAVA_VERSION}-sh.sh -o /deployments/run-java.sh \
&& chown 1001 /deployments/run-java.sh \
&& chmod 540 /deployments/run-java.sh \
&& echo "securerandom.source=file:/dev/urandom" >> /etc/alternatives/jre/lib/security/java.security
# Configure the JAVA_OPTIONS, you can add -XshowSettings:vm to also display the heap size.
ENV JAVA_OPTIONS="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
# We make four distinct layers so if there are application changes the library layers can be re-used
COPY --chown=1001 --from=builder /usr/src/app/target/quarkus-app/lib/ /deployments/lib/
COPY --chown=1001 --from=builder /usr/src/app/target/quarkus-app/*.jar /deployments/
COPY --chown=1001 --from=builder /usr/src/app/target/quarkus-app/app/ /deployments/app/
COPY --chown=1001 --from=builder /usr/src/app/target/quarkus-app/quarkus/ /deployments/quarkus/
EXPOSE 8080
USER 1001
ENTRYPOINT [ "/deployments/run-java.sh" ]

In this way, we guarantee that anyone who wants to use run our project in docker, can build and run it easily without downloading any dependency to their machine.

Property Changes

The Security

In this section, we want to be able to secure our properties. We can do so using the “.env” file which we will place in our root project directory. This file will house environment variables that have this form:

LOG_LEVEL=INFO 2HTTP_ACCESS_LOG_ENABLED=true 3LOG_CATEGORY_MONGODB_LEVEL=INFO

For our properties file we will simply reference them as properties file as such:

quarkus.log.level=${LOG_LEVEL}
quarkus.http.access-log.enabled=${HTTP_ACCESS_LOG_ENABLED}

In this manner, for any change we want to do for our properties like increasing the log level, we simple edit the .env file, and restart our container.

I also want to note that we can completely move to the .env file because Quarkus support property and environment mapping which can be seen here. As such, we would not have to create an environment reference inside the properties file.

The Connections

If we are using docker-compose and we want to connect to other containers, we might have to change a few things. For connections that our outside our services, we keep them as they are. On the other hand, for connections that are to our services like database connection or internal API calls, they should be referenced as containers. For example, we have the following MySQL connection JDBC string:

jdbc:mysql://localhost:3308/my_schema

This will unable to connect to the MySQL database because with respect to the container it is not running in it, but on a different container. To fix this, we simply have to replace the hostname with the container name and the port with internal container port and not the external one. As a result, we will have something like this:

jdbc:mysql://${container-name}:${internal-container-port}/my_schema

The Docker Compose

At this point, we might want to add our container to the tribe of containers that houses all our applications from different teams. Our container might seem alone at this point, but it can be easily added and directly be one with the tribe.

The Composition

At this point, we can add our project to the docker-composition to be a part the integration tribe. This docker composition file is named ‘docker-compose.yml’, we will add this beside our Quarkus project. This file is yaml file and as such we have to take care of our spacing. Our structure should follow this format:

quarkus:
container_name: quarkus
image: quarkus
depends_on:
- ${dependency-1}
- ${dependency-2}
build:
context: ./${project-name}
dockerfile: ./src/main/docker/Dockerfile.fast-jar
env_file: ./${project-name}/.env
ports:
- xxxx:8080

The ${project-name} is the directory to our Quarkus project.

The ‘depends_on’ is a handy operation which informs that our container requires other containers for it to run like a mysql and mongodb databases for instance. This will make sure that these services run before our application does.

In the ‘build’, we have to specify the context which is the project directory, and the location of the dockerfile we wish to use.

In the ‘env_file’, we simply map it to the properties file that it will copy and use in the container.

Lastly, we will define the listening ports for our application. By default, the Quarkus project will run our application on port 8080. We may keep it like that because this is running on its own local container. We simply have to map it to an external port of our choosing.

For example, we can have the following docker compose file that contains our application, mongodb, and mysql.

version: '3.8'
services:
mongodb:
container_name: mongodb
image: 'mongo:4.4.3'
restart: always
environment:
MONGO_INITDB_ROOT_USERNAME: root
MONGO_INITDB_ROOT_PASSWORD: password
ports:
- 27017-27019:27017-27019
volumes:
- ./volumes/mongo:/data/db
quarkus:
container_name: quarkus
image: quarkus
restart: always
volumes:
- $HOME/.m2:/root/.m2
depends_on:
- mongodb
- mysql-integration
build:
context: ./quarkus
dockerfile: ./src/main/docker/Dockerfile.fast-jar
env_file: ./quarkus/.env
ports:
- 5005:5005
- 8080:8080
mysql:
container_name: mysql
image: 'mysql:8.0.23'
restart: always
volumes:
- './volumes/mysql:/var/lib/mysql'
ports:
- 3308:3306
environment:
- MYSQL_ROOT_PASSWORD=servme
command: mysqld --sql_mode=""

The Building

To build our container, we simply execute the following command:

docker-compose build ${container-name}

This will build our container and all of its container dependencies.

The Running

After we successfully build our container and its dependencies, we can run them using the following command:

docker-compose up ${container-name}

This will build and run our container in real time. In other words, we can view live logs in our terminal for debugging purpposes. We can simply use ctrl+c to exit, but keep the container. Afterwards, we can run it without viewing the logs using the following command:

docker-compose start ${container-name}

The Stopping

After we successfully run our containers, We can stop our docker using the ‘ctrl+c’ if we ran is using the ‘up’ argument. Otherwise, we have to stop it using the following command:

docker-compose stop ${container-name}

The Destruction

We can tear down our containers using the following command:

docker-compose down

This will destroy our containers and anything inside them. If we want to preserve anything, we can use docker volumes to preserve data and load them into our containers. Please also note that once we destroy and rebuild a container, we will have to redownload all dependencies.

The Remote Development

One of the great features of Quarkus is that it allows us to develop without having to run the application on IDE. In other words, we connect to a running service and modify it in real time. To do so, we have to enable remote development for docker, and in this section we will show you how. Remote development in Quarkus wants us to package our application using ‘mutable-jar’.

The Application Properties

In our application properties, we have to add the following arguments.

%dev.quarkus.package.type=mutable-jar
%dev.quarkus.live-reload.password=changeit

To enable debugging, we have to expose the port 5005 along with the default one provided in our DockerFile.

The .env

In the .env file, we have to enable it using the environment variables.

QUARKUS_LAUNCH_DEVMODE=true
JAVA_ENABLE_DEBUG=true

The Running

After completing the previous step, you can check in the logs that remote development has been enabled, and that the project is running using the development profile.

The Remote Connection

Now all we have to do is connect to the service using the following maven command:

mvn quarkus:remote-dev -Dquarkus.live-reload.url=http://${hostname}:${container-external-port} -Dquarkus.package.type=mutable-jar -Dquarkus.live-reload.password=${Reload-password}

In this command, we execute it using the installed maven project. We specify the url to connect to and the external port, and we should specify the password which we set when we build the container.

Get Coding

Now you get coding and see your changes live on the server or container as it hot reloads!

--

--