DEV Community

Cover image for How to set up NGINX Docker Reverse Proxy?
Sukhbir Sekhon
Sukhbir Sekhon

Posted on • Updated on

How to set up NGINX Docker Reverse Proxy?

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.

Alt Text

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.

Alt Text

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.

Alt Text

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.

Alt Text

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
Enter fullscreen mode Exit fullscreen mode
cd ~
mkdir webservice1
cd webservice1
vi docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

Then paste this YAML code in the file:

version: '2'
services:
app:
image: nginx:1.9
volumes:
- .:/usr/share/nginx/html/
expose:
- "80"
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode
<!DOCTYPE html>
<html>
<head>
<title>Web service 1</title>
</head>
<body>
<h1>Welcome to website 1</h1>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Now, build the webservice1 using the docker-compose command

docker-compose build
Enter fullscreen mode Exit fullscreen mode

Start the container

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

List all the containers

docker ps -a 
Enter fullscreen mode Exit fullscreen mode

You should see your container with a name:

site2_app_1
Enter fullscreen mode Exit fullscreen mode

Similarly, create a second container for webservice2

cd ~
mkdir webservice2
cd webservice2
vi docker-compose.yml
Enter fullscreen mode Exit fullscreen mode

Code for YAML file:

version: '2'
services:
app:
image: nginx:1.9
volumes:
- .:/usr/share/nginx/html/
expose:
- "80"
Enter fullscreen mode Exit fullscreen mode

Create an index file

vi index.html
Enter fullscreen mode Exit fullscreen mode

Create a simple HTML response

<!DOCTYPE html>
<html>
<head>
<title>Web service 2</title>
</head>
<body>
<h1>Welcome to website 2</h1>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

Build the webservice2 container using docker-compose command

docker-compose build
Enter fullscreen mode Exit fullscreen mode

Start the container

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create Dockerfile

vi Dockerfile
Enter fullscreen mode Exit fullscreen mode
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/
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
# 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;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
version: '2'
services:
proxy:
build: ./
networks:
- site1
- site2
ports:
- 80:80
- 443:443

networks:
site1:
external:
name: site1_default
site2:
external:
name: site2_default
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

For webservice 2

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout site2.key -out site2.crt
Enter fullscreen mode Exit fullscreen mode

Now let's create a standard configuration for the proxy.

cd includes
vi proxy.conf
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

Create standard SSL configuration to decode the SSL certifications and keys

vi ssl.conf
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

This configuration helps to decode the SSL certs and keys using the ciphertext.

Let's add hostnames and IP addresses

cd ~
vi /etc/hosts
Enter fullscreen mode Exit fullscreen mode
172.31.30.78 site1.test
172.31.30.78 site2.test
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

We have a container and now run the container

docker-compose up -d
Enter fullscreen mode Exit fullscreen mode

Now, you should see three running docker containers. Two for web services and one for the proxy container.

docker ps -a
Enter fullscreen mode Exit fullscreen mode

Let's verify if our reverse proxy is working

curl site1.test
Enter fullscreen mode Exit fullscreen mode

You should see this response:

<!DOCTYPE html>
<html>
<head>
<title>Web service 1</title>
</head>
<body>
<h1>Welcome to website 1</h1>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (14)

Collapse
 
alara_joel profile image
Alara Oluwatoyin Joel

Great write Sukhbir, trying to follow it but my situation is quite different.
I have a server that runs Nginx, and I am deploying a CMS (Xibo) on it.
Cms runs by default on port 80, but cannot run because Nginx is listening there.
So to run it I kill the Nginx process.
Note that the cms is running inside of a docker-compose container.

So I am supposed to change its port, then use a reverse proxy to bring it back on port 80, when this works on HTTP, then I can do the securing
Phew!!
You did help understand things better, so i will keep hacking away.

Collapse
 
xkozs01 profile image
xkozs01

Can you please fix the indentation in the yml files? I cannot build the docker-compose.yml for proxy server (the last yml in the article)

Collapse
 
sukhbirsekhon profile image
Sukhbir Sekhon

Try copying the code using ALT instead of CTRL.

Collapse
 
xkozs01 profile image
xkozs01

Thanks for response, but how can I use ALT istead of CTRL? It doesn't work on my Windows notebook. I am pasting it to VS Code. When I format it like this

version: '2'
services:
  proxy:
    build: ./
    networks:
      - site1
      - site2
    ports:
      - "80:80"
      - "443:443"

  networks:
    site1:
      external:
        name: site1_default
    site2:
      external:
        name: site2_default
Enter fullscreen mode Exit fullscreen mode

It gives me an error

ERROR: The Compose file '.\docker-compose.yml' is invalid because:
Unsupported config option for services.networks: 'site2'
Enter fullscreen mode Exit fullscreen mode

What is wrong?

Thread Thread
 
xkozs01 profile image
xkozs01

OK, I got it, but it took me some time to figure it out

Webservice 1

version: "2"
services:
  app:
    image: nginx:1.9
    volumes:
      - .:/usr/share/nginx/html/
    expose:
      - "80"
Enter fullscreen mode Exit fullscreen mode

Webservice 2

version: "2"
services:
  app:
    image: nginx:1.9
    volumes:
      - .:/usr/share/nginx/html/
    expose:
      - "80"
Enter fullscreen mode Exit fullscreen mode

proxy

version: "2"
services:
  proxy:
    build: ./
    networks:
      - site1
      - site2
    ports:
      - "80:80"
      - "443:443"

networks:
  site1:
    external:
      name: site1_default
  site2:
    external:
      name: site2_default
Enter fullscreen mode Exit fullscreen mode
Collapse
 
davbaster profile image
davbaster • Edited

Hi,
I have completed this tutorial, and it helped me a lot, however I needed to do a couple of changes in order to work on my side.

First, I changed the name of the networks, since on my side they were called webservice1_default instead of site1_default. I found the name of the network by executing this docker command: "docker network ls"

dev-to-uploads.s3.amazonaws.com/i/...

Second, I had to found out which were my containers' ip, since the ones listed here are different than mine. I found the ips by inspecting the networks I found before. I used "docker network inspect NameOfYourNetwork" command.

dev-to-uploads.s3.amazonaws.com/i/...

Third, I had to modify my hosts file with the correct ips I found in the previous step.

dev-to-uploads.s3.amazonaws.com/i/...

Finally, I needed to do some modifications in docker-compose.yml file for the proxy, since the identitation was no correct. In the below image you can see the correct identation.

dev-to-uploads.s3.amazonaws.com/i/...

Thanks to the author for taking the time to do this tutorial.

Collapse
 
alver23 profile image
Alver Grisales

hi, the implementation i have error

host not found in upstream "site1_app_1" in /etc/nginx/conf.d/default.conf:14

host not found in upstream "site1_app_1" in /etc/nginx/conf.d/default.conf:14

Collapse
 
nicolasgoudard profile image
Nicolas G • Edited

Hello
This tutorial does not work, but I success with another
1- A part is missing ; The site1_default and site2_default networks should be created before,

2- The container names site1_app_1 and site2_app_1 are not defined, because autogenerated by docker : in my Docker version it generates webservice1_app_1 and webservice1_app_2. The best practice is to give a name to containers in the docker-compose.yml file, using the "container_name" keyword .
3- Cannot resolve the container names inside the Nginx container : so the proxy_pass directive dos not work, I think I must configure something in my Docker configuration, but it is not explained in this tutorial
4- Let's encrypt does not work generate the certificates because the server does not exist in the DNS ( pointer A missing)
5- site1.test and site2.test are bad declared in the /etc/hosts file . Normally you should declare in one line, like that : "172.31.30.78 site1.test site2.test"
Best regards

Collapse
 
raulsesaj profile image
RICARDO RAUL JACINTO MONTES

First at all, thanks !!! great tutorial, with some mistakes it's true, but it shows cooperation of all involved. I followed not only this blog, but the instructions from davbaster and it worked well.

Thanks again

Collapse
 
sibelius profile image
Sibelius Seraphini

do you have a github repo for this?

Collapse
 
sukhbirsekhon profile image
Sukhbir Sekhon

Unfortunately, I don’t. Will keep in mind next time. Thanks!

Collapse
 
1nnikan1 profile image
1nnikan1

can anyone make a video about this please

Collapse
 
1nnikan1 profile image
1nnikan1

ERROR: In file './docker-compose.yml', service must be a mapping, not a NoneType.

can anyone help me with this please

Collapse
 
arvigeus profile image
Nikolay Stoynov

@1nnikan1 I know it's a year late, but if anyone stumbles upon this in the future: see comment above