Setting Up Reverse Proxy and Automated SSL with Docker Compose

A reverse proxy is a type of proxy server that sits behind the firewall in a private network and directs client requests to the appropriate backend server. Why would you use such a setup? Well, for several reasons. The reverse proxy hides the identity of the servers behind it and presents its own IP address to the outside world. This can improve security and performance by offloading tasks such as encryption and decryption to the reverse proxy.

I will not go into the technical details of how reverse proxy works. Instead, I will write how I set up multiple services using this concept with Nginx, Let’s Encrypt, and Docker.

As shown in the diagram above, we can run the Nginx server in a container and configure it to know which other services are running in their corresponding containers. This allows Nginx to route incoming requests to the appropriate container.

Things you need first.

First, you have to have Docker installed on your machine. In my previous post, I covered how I installed Docker on Ubuntu 22.04 LTS. If you are using a different operating system, please refer to the official Docker documentation for instructions specific to your OS. Keep in mind that some steps may vary depending on your OS.

Next, to set up SSL, you also need to own a domain name and have already set up A, AAAA, or CNAME that points to your server’s IP address.

Also, to ensure that your containerized applications have sufficient memory, you should have enough swap space on your system. The amount of swap space you need will depend on the amount of available RAM on your system, as well as the memory usage of the containers running on the server. You can adjust the amount of swap space based on these factors, to make sure that your containers have enough memory to run smoothly.

Finally, I personally like to keep all my things in the same folder. Since this is just the start, and we will be setting up a number of docker containers, it’s a good idea to keep everything organized.

$ cd ~
$ mkdir docker

Use Docker Compose.

We will be using Docker Compose to define and run everything we need. To use Compose, we need to define our application’s services in a YAML file, in this case, docker-compose.yml.

This file specifies the containers, networks, and volumes that make up our application, as well as any dependencies between these components. Once we have defined our services in this file, we can use a single docker compose command to create and start all the services in our application. This makes it easy to manage and deploy complex multi-container applications.

Set up the Nginx reverse proxy container.

This container will be the middle green container in the diagram above. It will run Nginx to handle incoming requests. This container will be responsible for routing incoming requests to other containers.

Let’s begin by creating a directory named nginx.

$ cd ~/docker
$ mkdir nginx
$ cd nginx

Create a file named docker-compose.yml, and open it. This will be the YAML file for our application.

$ touch docker-compose.yml
$ vim docker-compose.yml

I will be using the nginxproxy/nginx-proxy image.

nginx-proxy is a tool that sets up a container running nginx as well as docker-gen.

docker-gen is a utility that automatically generates the reverse proxy configuration for nginx based on the containers that are running on the host. It also reloads the nginx server whenever containers are started or stopped, ensuring that the reverse proxy is always up-to-date.

By using nginx-proxy, we can easily manage and deploy complex multi-container applications without having to manually configure the reverse proxy.

Copy and paste the following into your docker-compose.yml file.

version: '3.8'

services:

  nginx:
    image: 'nginxproxy/nginx-proxy:latest'
    container_name: 'nginx-proxy'
    volumes:
      - 'html:/usr/share/nginx/html'
      - 'dhparam:/etc/nginx/dhparam'
      - 'vhost:/etc/nginx/vhost.d'
      - 'certs:/etc/nginx/certs'
      - 'conf:/etc/nginx/conf.d'
      - '/var/run/docker.sock:/tmp/docker.sock:ro'
    restart: 'always'
    networks:
      - 'net'
    ports:
      - '80:80'
      - '443:443'

Let’s take a look what we have done here:

  • We declared four volumes in our nginx-proxy container: html, dhparam, vhost, and certs. These volumes are used to store persistent data that you want to keep even after the containers have been stopped or removed.
  • docker.sock allows the container to communicate with the Docker daemon and manage the containers on the host.
    • In the case of our container, the docker socket is mounted in read-only mode, which allows the container to access information about the other containers on the host. This is necessary in order for it to generate the nginx configuration files and detect other containers that have a specific environment variable set.
    • By mounting the docker socket in this way, the nginx-proxy container can automatically configure itself based on the containers that are running on the host. This makes it easy to manage and deploy complex multi-container applications.
  • The always restart policy. It instructs Docker to always attempt to restart a container if it is stopped or crashes.
  • A user-defined network net. By creating a network for the containers that are to be proxied, you can isolate these containers from the other containers on the host. The custom network also allows the containers to communicate with each other, which is not possible with the default bridge network.
  • Finally, we open up the 80 and 443 ports for HTTP and HTTPS requests.

Save the file but do not close it yet.

Note: YAML files does not allow tabs. Make sure you indent with spaces.

Set up a container for automatic SSL management.

Now we can add configurations for our Let’s Encrypt container in the same docker compose file. I will be using the nginxproxy/acme-companion image.

acme-companion provides support for automatically creating, renewing, and using SSL certificates for proxied Docker containers. With it, you can easily add SSL support to your proxied containers, without having to manually manage the SSL certificates on your own.

Copy and paste the following into the same docker compose file:

  acme:
    image: 'nginxproxy/acme-companion:latest'
    container_name: 'nginx-proxy-acme-companion'
    depends_on:
      - 'nginx'
    volumes_from:
      - 'nginx'
    volumes:
      - 'acme:/etc/acme.sh'
      - '/var/run/docker.sock:/var/run/docker.sock:ro'
    environment:
      NGINX_PROXY_CONTAINER: 'nginx-proxy'
      DEFAULT_EMAIL: 'you@example.com'
    restart: 'always'
    networks:
      - 'net'

Again, let’s take a look what we have done here:

  • We are declaring that the Let’s Encrypt container should depend on the nginx container, so that it waits for nginx to start first. This ensures that the Let’s Encrypt container will not start until the reverse proxy container is up and running.
  • We are using the same volumes as we used for the nginx-proxy container. However, the important change here is that the Let’s Encrypt container now requires a volume mounted to /etc/acme.sh in order to persist ACME account keys and SSL certificates. 
  • We defined two environment variables: The NGINX_PROXY_CONTAINER variable specifies the name of our reverse proxy container. The DEFAULT_EMAIL variable specifies the email that will be used when generating SSL certificates for each domain or subdomain.
  • Finally, we defined the two container with the same network, allowing the two to communicate.

Finalize the Docker Compose file.

With these two containers added, we can finalize the Compose file with volumes and networks.

volumes:
  html:
  dhparam:
  vhost:
  certs:
  conf:
  acme:

networks:
  net:
    external: true

The “net” network is set to external because the proxied containers (e.g. WordPress) need to use this network in order to communicate with the reverse proxy.

Since we are done with the docker compose file, you can save the file and return to your terminal.

Run the following command to deploy the two containers (Nginx and SSL) we have created:

$ docker compose up -d

Test to see if the containers are working.

We can use the following command to test our containers.

docker run --rm --name nginx-test \
  -e VIRTUAL_HOST=sub.example.com \
  -e LETSENCRYPT_HOST=sub.example.com \
  -e VIRTUAL_PORT=80 \
  --network net -d nginx:latest
  • VIRTUAL_HOST is for Nginx to generate the reverse proxy config.
  • LETSENCRYPT_HOST is for Let’s Encrypt to generate SSL settings.
  • The --rm flag makes sure that this container gets automatically removed when it exits.

If everything is set up correctly, you should see a Welcome to Nginx message when you visit the domain you used in a browser.

For your reference, here is the completed docker_compose.yml file.

Future Directions

Here, we have successfully set up Nginx and Let’s Encrypt containers. This allows us to easily deploy multiple web apps on the same server and serve them under different subdomains in the future. It’s a pretty convenient and efficient way to host multiple services on a single server.

We have successfully set up Nginx for reverse proxy and Let’s Encrypt for SSL.

In a future post, I will explain how I used Docker Compose to set up Nextcloud in another container, providing readers with more information on how to use Docker Compose to manage multiple containers.


Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.