Deploying a Quarkus or any java based microservice behind an Nginx reverse proxy with ssl using docker

Alexius Dionysius Diakogiannis
6 min readJul 12, 2021

--

This is my first Medium upload and as per a friend requested I am going to show you how to deploy a Quarkus microservice behind an Nginx reverse proxy using docker.

What are we going to do…

I am going to install docker and docker-compose on a centos 8 host and I am going to deploy a docker container that will expose Nginx on ports 80 and 443 and a microservice using Quarkus. The same technique can be used with ANY java microservices framework like microprofile, Springboot etc because in the end what you will do is run a simple jar file (java is magic right?).

Let’s start…

I am going to skip the installation details for docker and docker-compose. In case you haven’t heard of docker-compose have look here https://gabrieltanner.org/blog/docker-compose and you’ll love it. It automates your container deployments and it just rocks!

Prerequisites

First of all make sure you have the ports required open

sudo firewall-cmd --zone=public --add-masquerade --permanent
sudo firewall-cmd --zone=public --add-port=22/tcp
sudo firewall-cmd --zone=public --add-port=80/tcp
sudo firewall-cmd --zone=public --add-port=443/tcp
sudo firewall-cmd --reload

Now install docker as per documentation

#remove previous versions if any
sudo yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine

#install
sudo yum install -y yum-utils

sudo yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo

sudo yum install docker-ce docker-ce-cli containerd.io

sudo systemctl start docker

#Verify that Docker Engine is installed correctly by running the hello-world image.
sudo docker run hello-world

Last but not least install docker-compose

#curl is required
dnf install curl

#Download the latest version of Docker Compose. Currenlty I am using version 1.25.4
curl -L https://github.com/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose

# Test the installation.
docker-compose --version

Now to the fun stuff…

Check out a sample application that I have developed using Quarkus that calculates a runners pace by executing

git clone https://github.com/diakogiannis/pacecalculatorapi.git

In the case you have forgotten to install GIT (I won’t tell anyone) then execute

sudo yum install git

Now let’s build it INSIDE the Docker image (yes you don’t even have to have java installed)…

docker run — name=pacecalculator -d -p 9090:8080 diakogiannis/pacecalculator:latest

Et voila! the application is ready to run!

We actually told docker to run the container giving him the name pacecalculator, with ‘-d’ we told it to be in ‘detached’ mode so it will run in the background and with ‘-p 9090:8080’ we told it to expose the 8080 port internally to the 9090 port in the running system.

Let’s test if it works, and since I am a bad long-distance runner, I will try to calculate the running pace for 5km for just under 30 minutes (1.700s) try inputing

curl “http://localhost:9090/api?distance=5&seconds=1700"

that will result in

{“pace”:”5.67"}

Let’s examine the docker file

# Stage 1 : build with maven builder image
FROM maven:3.6.0-jdk-11-slim AS BUILD
MAINTAINER Alexius Diakogiannis

COPY . /usr/app/

RUN mvn -f /usr/app/ clean package
# Stage 2 : copy from the previous container the jar file, put it in a java one and run it
FROM adoptopenjdk:11-jdk-openj9
WORKDIR /app

COPY --from=BUILD /usr/app/target/PaceCalculatorApp-runner.jar /app/

ENTRYPOINT ["java", "-jar", "/app/PaceCalculatorApp-runner.jar"]
  1. First of all, we use a maven container with JDK-11 and we use the COPY command to copy ALL the project inside.
  2. After that, we build it in the same way as we would in our normal development environment by mvn clean package pointing out the location of the pom.xml file. Afterwards, we use another container (because after all, we might need a different environment to run the application) and in this case JDK-11 but with OpenJ9 JVM (that rocks and has it’s origins to IBM’s Java SDK/IBM J9 with great memory management)
  3. Then we copy the jar file created from the previous container to the new one
  4. Last we tell docker to execute
  5. java -jar /app/PaceCalculatorApp-runner.jar
  6. when the container starts. Be very careful and notice that when using ENTRYPOINT each parameter must be on a separate section.

Now let’s stop and remove the container

docker stop pacecalculator && docker rm pacecalculator

Preparing the filesystem

For NGinX SSL to work we need to store the certificates somewhere. Also, a folder for the NGinX logs is needed. It is a very best practice not to generate IO inside a Docker image so in production would have externalize also the console log of the java application but this is just a PoC.

For my installation I am usually using the pattern /volumes/{docker image name}/{feature} and I don’t let docker decide where to store my volumes. So in this case, I created

  • /volumes/reverse/config
  • /volumes/reverse/certs
  • /volumes/reverse/logs

reverse will be the name of the docker container that NGinX will run

I have issued a certificate under a free authority and placed its two files (pacecalculator.pem and pacecalculator.key) in /volumes/reverse/certs directory

I create the file /volumes/reverse/config/nginx.conf with the contents

user nginx;
worker_processes 1;

error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
}
http {

default_type application/octet-stream;

log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

sendfile on;
tcp_nopush on;

keepalive_timeout 65;

gzip on;
gzip_http_version 1.0;
gzip_proxied any;
gzip_min_length 500;
gzip_disable "MSIE [1-6]\.";
gzip_types
text/plain
text/html
text/xml
text/css
text/comma-separated-values
text/javascript
application/x-javascript
application/javascript
application/atom+xml
application/vnd.ms-fontobject
image/svg+xml;

proxy_send_timeout 120;
proxy_read_timeout 300;
proxy_buffering off;
tcp_nodelay on;

server {
listen *:80;
server_name jee.gr;

# allow large uploads of files
client_max_body_size 80M;

# optimize downloading files larger than 1G
#proxy_max_temp_file_size 2G;

location / {
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
proxy_pass http://pacecalculator:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
server {
listen 443 ssl;
server_name xxxx.yyy;

ssl_certificate /etc/ssl/private/pacecalculator.pem;
ssl_certificate_key /etc/ssl/private/pacecalculator.key;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers HIGH:!aNULL:!MD5;

# allow large uploads of files
client_max_body_size 80M;

# optimize downloading files larger than 1G
#proxy_max_temp_file_size 2G;

location / {
# Use IPv4 upstream address instead of DNS name to avoid attempts by nginx to use IPv6 DNS lookup
proxy_pass http://pacecalculator:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}

}

}

I will not go in much detail with the configuration but in general, it will gzip the communication between the client and the reverse proxy and it will listen for the hostname jee.gr. Both 80 and 443 ports will reverse proxy on port 80 of the microservice, this means that the internal docker communication is NOT encrypted (but do we need to encrypt it?). We can, of course, encrypt it but this falls out of the scope of this tutorial. Please note that we use for the internal hostname the docker name “pacecalculator”.

Lets create the orchestrator aka docker-compose.yml file that will orchestrate the deployment of both microservices with the correct order.

nano docker-compose.yml

and inside paste

version: '3'
services:
reverse:
depends_on:
- pacecalculator
container_name: reverse
hostname: reverse
image: nginx
ports:
- 80:80
- 443:443
restart: always
volumes:
- /volumes/reverse/config/:/etc/nginx/
- /volumes/reverse/logs/:/var/log/nginx/
- /volumes/reverse/certs/:/etc/ssl/private/
pacecalculator:
container_name: pacecalculator
hostname: pacecalculator
image: diakogiannis/pacecalculator:latest
restart: always
networks:
default:
external:
name: proxy-net

So what we did here is that we started our pacecalculator service and the the reverse service telling it to expose both ports 80 and 443 BUT also to wait (depends_on) until pacecalculator is started successfully. Also we are using an internal dedicated network for the communications that we named it proxy-net

Time to fire it up!

We start the containers by issuing

docker-compose -f /{path to}/docker-compose.yml up --remove-orphans -d

this will clean up leftover containers and start again in detached mode (aka background)

If we want to stop it we issue

docker-compose -f /{path to}/docker-compose.yml down

As the French say ç’est très difficile? No ç’est très facile!

--

--

Alexius Dionysius Diakogiannis

Tech Leader and Staff Developer with more than 17 years of experience working with Java, JEE and Enterprise Architecture Design