Deploying a Quarkus or any java based microservice behind an Nginx reverse proxy with ssl using docker
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"]
- First of all, we use a maven container with JDK-11 and we use the COPY command to copy ALL the project inside.
- 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)
- Then we copy the jar file created from the previous container to the new one
- Last we tell docker to execute
- java -jar /app/PaceCalculatorApp-runner.jar
- 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!