Deploying a Quarkus or any Java based microservice behind an Nginx reverse proxy with SSL using docker
It has been a while but 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
1 2 3 4 5 | 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
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | #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
1 2 3 4 5 6 7 8 | #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 if you execute sudo yum install git
)
Now let’s build it INSIDE the Docker image (yes you don’t even have to have java installed)…
1 | docker run --name=pacecalculator -d -p 9090 : 8080 diakogiannis/pacecalculator:latest |
En voila! the application is ready to run!
We actually told docker to run the container giving him the name pacecalculator, with ‘-d’ we told him to be in ‘detached’ mode so it will run in the background and with ‘-p 9090:8080’ we told him 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
that will result in {"pace":"5.67"}
Let’s examine the docker file
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | # 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 sinstallation 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
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | 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 nomisma.com.cy www.nomisma.com.cy app.nomisma.com.cy; 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.
1 | nano docker-compose.yml |
and inside paste
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | 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: reverse hostname: reverse 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
1 | 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
1 | docker-compose -f /{path to}/docker-compose.yml down |
As the French say ç’est très difficile? No ç’est très facile!
Published on Java Code Geeks with permission by Alexius Diakogiannis, partner at our JCG program. See the original article here: Deploying a Quarkus or any java based microservice behind an Nginx reverse proxy with ssl using docker Opinions expressed by Java Code Geeks contributors are their own. |
Hi Alexius,
Looking at “docker-compose.yml” -> “pacecalculator”->”container_name” should probably be something like “pacecalculator” (or something similar) instead of “reverse”, right?
Best,
Uroš