Routing Traffic to Docker Containers

How to Setup Nginx Reverse Proxy for Routing Incoming Traffic to Different Containers and Certbot for Auto-Renewing SSL Certificates

For small applications or test environments where separate machines for different web servers are cost prohibitive, one option is to have different servers run on the same machine in different Docker containers. Docker doesn’t support exposing the same port to multiple containers simultaneously (source). Still, we can install Nginx on the host machine and have it conditionally route the requests to the different containers.

Incoming traffic routed to two docker containers with different web servers on the same machine

The problem

After exposing port 80 to one container, if we try to have that port exposed to another container, we get the following error:

$ docker run -ti -d -p 80:80 httpd
docker: Error response from daemon: driver failed programming external connectivity on endpoint hopeful_haibt (...): Bind for 0.0.0.0:80 failed: port is already allocated.

The solution: Reverse Proxy

In this example, we want to have one container to serve a Flask application for flaskapp.example.com and another container to serve a Node.js application for nodeapp.example.com; both from port 80 of the same machine.

We will set up a reverse proxy that routes the request for hosts to different containers:

1. Install Docker

There are plenty of tutorials out there for installing Docker, such as this one. This article will focus on the routing part.

2. Install Nginx on the host machine

Again, there are plenty of other good tutorials on this. Here is one. (You can also install Nginx in a separate container.)

3. Point your domains to the server

For our example, we would setup A records for flaskapp and nodeapp that point to the IP of the server in the DNS records of example.com.

4. Run the Docker containers with the web servers you need, but on ports other than 80 and 443

Say we have two Docker images already built or pulled: flaskApp and nodeApp. We can expose port 8080 for flaskApp and port 8081 for nodeApp:

$ docker run -dit -rm --name flaskApp -p 8080:80 my-flask-app
$ docker run -dit -rm --name nodeApp -p 8081:80 my-nodejs-app

At this point, you should be able to see your applications served at flaskapp.example.com:8080 and nodeapp.example.com:8081.

5. Configure Nginx reverse proxy

To serve both these apps on port 80, we will set up server blocks in the host machine.

Create the server block for flaskapp.example.com:

$ sudo nano /etc/nginx/sites-available/flaskapp.example.com

Paste the following in this file:

server {
        listen 80;
        server_name flaskapp.example.com;
        location / {
                proxy_pass http://localhost:8080;
        }
}

Create the server block for nodeapp.example.com:

$ sudo nano /etc/nginx/sites-available/nodeapp.example.com

Paste the following in this file:

server {
        listen 80;
        server_name nodeapp.example.com;
        location / {
                proxy_pass http://localhost:8080;
        }
}

Create symlinks in the sites-enabled directory:

$ sudo ln -s /etc/nginx/sites-available/flaskapp.example.com /etc/nginx/sites-enabled/
$ sudo ln -s /etc/nginx/sites-available/nodeapp.example.com /etc/nginx/sites-enabled/

Ensure that the configuration we did is valid:

$ sudo nginx -t

Restart Nginx:

$ sudo systemctl restart nginx

6. Setup automatically renewing SSL certificates with Certbot

Install Certbot and its Nginx plugin.

$ sudo apt-get update
$ sudo apt-get install certbot
$ sudo apt-get install python3-certbot-nginx

Then generate certificates for your websites.

$ sudo certbot --nginx -d flaskapp.example.com -d www.flaskapp.example.com
$ sudo certbot --nginx -d nodeapp.example.com -d www.nodeapp.example.com

Now, if you look at the server blocks we’ve set up, you will see the new lines added for the SSL configuration automatically.

Of course, “-d www.flaskapp.example.com” is not necessary if you are not using the www version for the subdomain. After following the prompts, you will see a success message as below.

Requesting a certificate for flaskapp.example.com

Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/flaskapp.example.com/fullchain.pem
Key is saved at:
/etc/letsencrypt/live/flaskapp.example.com/privkey.pem
This certificate expires on 2022-10-25.
These files will be updated when the certificate renews.
Certbot has set up a scheduled task to automatically renew this certificate in the background.

Deploying certificate
Successfully deployed certificate for test4.cansin.net to /etc/nginx/sites-enabled/flaskapp.example.com
Congratulations! You have successfully enabled HTTPS on https://flaskapp.example.com

You can list your certificates with:

$ certbot certificates

… and delete them with:

$ sudo certbot delete --cert-name flaskapp.example.com

.. and delete the server blocks for that site:

$ sudo rm /etc/nginx/sites-enabled/flaskapp.example.com
$ sudo rm /etc/nginx/sites-available/flaskapp.example.com

Further reading:

  1. I originally published this post on Medium.
  2. We’ve set this up in the host machine because it is not recommended to install Certbot inside the containers, as you can read more about it here.
  3. This article covers various methods of exposing multiple containers on the same port.
  4. The accepted answer to this question covers the same method described here.
  5. This page on Nginx website describes setting up certbot-nginx.