How to set up NGINX Docker Reverse Proxy?
Sukhbir Sekhon
Posted on April 25, 2020
Introduction
Imagine a time when you set up the local server and created a front-end application based on it. To access the API of an app, you would type example.com:3000 and for the application, you would type something like this example.com:4040. There could be multiple ports for one application to access different pages: home, dashboard, contacts, table, e.t.c. Now imagine remembering multiple ports for multiple applications.
Wouldn't be nice to just do something like this to access API: example.com/api and for an app: example.com/app. Instead of typing stupid ports, you could establish routes for multiple containers, and those routes URLs could be anything. Reverse proxy using docker makes it easier to accomplish.
What is a Proxy?
Let me try to explain it using an analogy. Let's say you walk into your favorite restaurant. You are hungry. You want to have your favorite food. You give an order food to the waiter. The waiter takes your order and goes to the kitchen. The waiter asks the chef to cook all the things that you asked from the waiter. Chef cooked the food. The waiter brings your food. Simple?
Key things to note: You requested the waiter. The waiter fulfilled your request without making you go to the kitchen. The chef doesn't know for whom was that food.
In this example, the waiter is a proxy, and the chef is the internet. A proxy acts as a firewall between you and the internet. A proxy provides security, privacy, and other levels of functionality. You can set up a proxy server in Nigeria. Then you can make an HTTP request on the internet and that request would go to Nigeria first and then on the internet in easy language.
Another example of proxy would be multiple computers in one company or building. All computers inside a company have IP address under the supreme IP address of a company. All the internet requests are made under one IP address which makes it harder to trace back to an individual computer.
Proxy is all about making secure external requests.
What is Reverse Proxy
It would not be fair if I don't throw in an analogy here. This analogy is old. Disclaimer: Don't judge me, I am a tech-savvy person. Let's say you woke up in the morning and you see postman on your door. Postman gives you multiple letters, invoices, postcards, and other forms of letters. All those letters are sent from different places, but they all end up to you at the right place.
Note: Senders didn't have to make direct contact with you. Postman retrieved letters on behalf of you from multiple sources.
In this example, the postman is a reverse proxy, the source is client, and you are applying. The client makes an HTTP request to talk to your application. The client wants to see the app so the client creates this request: example.com. But behind the scene reverse proxy converts this request to example.com:8080. If the client or user wants to see the API, then the client creates this request: example.com/api, but behind the scene, reverse proxy converts it to example.com:3000. A reverse proxy sits in front of the web servers and forwards client requests to the servers. Reverse proxies are usually implemented to increase security, performance, and reliability.
Reverse proxy lets you make secure internal requests.
What is Docker?
Docker is a popular enterprise PAAS (Platform as a Service) to create, run, and deploy applications by using containers. Containers allow a developer to package up an application with necessary modules, libraries, dependencies and deploy it as one of the packages. Docker sits on an existing host operating system and allows a developer to make light-weight containers on virtual machines to run their applications. So a developer can create many containers and run several applications on one host operating system. Enterprises don't have to spend money on purchasing multiple OS to deploy individual applications.
How to setup NGINX Docker Reverse Proxy
Let's establish a use case for setting up NGINX reverse proxy using docker. Inside the docker container, it is not possible to access ports and IP addresses that are private unless they are bound to host. We can use a reverse proxy to access multiple web applications running on multiple containers through single port 80. We will set up Nginx container that will be bind to port 80 to the docker host's port 80 and it will forward the request to web application running on multiple containers.
We need to set up two containers for web services or two applications that could be written in any language. But for the sake of the tutorial, let's create two web services with a simple index.html page.
Structure for our web service 1:
webservice1
├── docker-compose.yml
└── index.html
cd ~
mkdir webservice1
cd webservice1
vi docker-compose.yml
Then paste this YAML code in the file:
version: '2'
services:
app:
image: nginx:1.9
volumes:
- .:/usr/share/nginx/html/
expose:
- "80"
Ouick tip: Make sure that the indentations for all the YAML files correctly formatted. You can also use the VS Code to format them correctly.
Now, let's create a simple HTML page for webservice1:
vi index.html
<!DOCTYPE html>
<html>
<head>
<title>Web service 1</title>
</head>
<body>
<h1>Welcome to website 1</h1>
</body>
</html>
Now, build the webservice1 using the docker-compose command
docker-compose build
Start the container
docker-compose up -d
List all the containers
docker ps -a
You should see your container with a name:
site2_app_1
Similarly, create a second container for webservice2
cd ~
mkdir webservice2
cd webservice2
vi docker-compose.yml
Code for YAML file:
version: '2'
services:
app:
image: nginx:1.9
volumes:
- .:/usr/share/nginx/html/
expose:
- "80"
Create an index file
vi index.html
Create a simple HTML response
<!DOCTYPE html>
<html>
<head>
<title>Web service 2</title>
</head>
<body>
<h1>Welcome to website 2</h1>
</body>
</html>
Build the webservice2 container using docker-compose command
docker-compose build
Start the container
docker-compose up -d
Great! Now we have the two running docker containers that we want to bind to the proxy. Note: Both these containers are supposed to be running on port 80 so we have to set up a proxy that will direct requests accordingly. So let's set up the proxy.
This will be file structure for the proxy:
proxy/
├── backend-not-found.html
├── default.conf
├── docker-compose.yml
├── Dockerfile
├── includes
│ ├── proxy.conf
│ └── ssl.conf
└── ssl
├── site1.crt
├── site1.key
├── site2.crt
└── site2.key
Let's begin by creating all the necessary files.
cd ~
mkdir proxy
cd proxy
touch Dockerfile
touch backend-not-found.html
touch default.conf
touch docker-compose.yml
mkdir includes
mkdir ssl
cd includes
touch proxy.conf
touch ssl.conf
Create Dockerfile
vi Dockerfile
FROM nginx:1.9
COPY ./default.conf /etc/nginx/conf.d/default.conf
COPY ./backend-not-found.html /var/www/html/backend-not-found.html
COPY ./includes/ /etc/nginx/includes/
COPY ./ssl/ /etc/ssl/certs/nginx/
Dockerfiles are used to create docker images. Then docker images are used to create docker containers. In this Dockerfile, the image will refer to the existing Nginx docker image to create a custom docker image. Then, the image will be built by copying existing files on a local machine to the docker image. We are basically adding a configuration file, basic error HTML, some more configuration files for proxy, and certifications that we will generate later.
Create default.conf
vi default.conf
# web service1 config.
server {
listen 80;
listen 443 ssl http2;
server_name site1.test;
# Path for SSL config/key/certificate
ssl_certificate /etc/ssl/certs/nginx/site1.crt;
ssl_certificate_key /etc/ssl/certs/nginx/site1.key;
include /etc/nginx/includes/ssl.conf;
location / {
include /etc/nginx/includes/proxy.conf;
proxy_pass http://site1_app_1;
}
access_log off;
error_log /var/log/nginx/error.log error;
}
# web service2 config.
server {
listen 80;
listen 443 ssl http2;
server_name site2.test;
# Path for SSL config/key/certificate
ssl_certificate /etc/ssl/certs/nginx/site2.crt;
ssl_certificate_key /etc/ssl/certs/nginx/site2.key;
include /etc/nginx/includes/ssl.conf;
location / {
include /etc/nginx/includes/proxy.conf;
proxy_pass http://site2_app_1;
}
access_log off;
error_log /var/log/nginx/error.log error;
}
# Default
server {
listen 80 default_server;
server_name _;
root /var/www/html;
charset UTF-8;
error_page 404 /backend-not-found.html;
location = /backend-not-found.html {
allow all;
}
location / {
return 404;
}
access_log off;
log_not_found off;
error_log /var/log/nginx/error.log error;
}
In this Nginx configuration file, there are two main server components for both Webservice. Both of the containers will listen to Nginx port 80. To access both containers, we can curl them on their defined server_name: HTTP://site1.test or HTTP://site2.test. Both the server components will help Nginx to instruct the request to the appropriate web services container. This config also allow Nginx to throw custom error HTML instead of the default Nginx error page. Both server components direct Nginx to pick SSL certificates from the appropriate place.
Create docker-compose.yml
vi docker-compose.yml
version: '2'
services:
proxy:
build: ./
networks:
- site1
- site2
ports:
- 80:80
- 443:443
networks:
site1:
external:
name: site1_default
site2:
external:
name: site2_default
Note: Make sure indentation is right when copying my file to the terminal.
Here is the main part where we are actually implementing proxy. (Technically we did it in default.conf) This docker-compose.yml file will connect two external networks named site1 and site2 to the proxy. The binding of port 80/443 of this proxy is implemented on the Docker host's port 80/443. We already created our two docker containers for Webservice and we created two external docker networks with the name site1_default and site2_default.
Create certificates and keys
For webservice 1
cd ssl
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout site1.key -out site1.crt
For webservice 2
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout site2.key -out site2.crt
Now let's create a standard configuration for the proxy.
cd includes
vi proxy.conf
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_request_buffering off;
proxy_http_version 1.1;
proxy_intercept_errors on;
Create standard SSL configuration to decode the SSL certifications and keys
vi ssl.conf
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-
ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-
SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-
GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-
AES128-SHAECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-
SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:
DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-
DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:
AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-
CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;
This configuration helps to decode the SSL certs and keys using the ciphertext.
Let's add hostnames and IP addresses
cd ~
vi /etc/hosts
172.31.30.78 site1.test
172.31.30.78 site2.test
These are the private IP address of the docker host. The request will come to port 80 of the docker host but it will be redirected to port 80 of Nginx container.
Finally, let's build a proxy container
docker-compose build
We have a container and now run the container
docker-compose up -d
Now, you should see three running docker containers. Two for web services and one for the proxy container.
docker ps -a
Let's verify if our reverse proxy is working
curl site1.test
You should see this response:
<!DOCTYPE html>
<html>
<head>
<title>Web service 1</title>
</head>
<body>
<h1>Welcome to website 1</h1>
</body>
</html>
Great job! ✨
One of the benefits of a containerized application is that you can add and deploy applications easily. You can add as many web services containers as you want by adding configuration to default.conf and adding the external network to a docker-compose YAML file. This example illustrates how easy it is to use docker and Nginx to set up a reverse proxy. Imagine having multiple applications running on docker containers and all of their requests are getting redirected by Nginx container. You don't need to worry about adding configuration manually. You can simply automate the process using Docker APIs, which is a good concept to talk about for another blog.
Thanks for reading the blog and I hope it helped you to understand the pretty unique concept.
Leave me a comment if you have any questions or concerns.
Posted on April 25, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.