Reposted in my medium blog here
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
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 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)…
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
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 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
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: 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
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!
server_name [removed]; line68
Maybe its a mistake? 🙂
Well you got me 🙂 Thanks for pointing this out mate!
Hi Alexius,
Looking at “docker-compose.yml” -> “pacecalculator”->”container_name” should probably be something like “pacecalculator” (or something similar) instead of “reverse”, right?
Uroš
You are right Uros, thanks for pointing it out!