Docker Reverse Proxy with Nginx

This article shows how to setup a simple Docker reverse proxy on a local network using Nginx and providing encryption with self-signed certs.

I've recently been working with Docker and running a few applications on my home network. Some of the web applications have no authorization and can modify files on the machines they run on. While it's low risk to run these applications on a home network, it's nice to have some security and possibly run them in a cloud environment in the future. One solution for adding authentication is to put the applications behind a reverse proxy.

Another option would be to use the linuxserver.io SWAG image, which covers everything needed for an authenticated, SSL enabled, reverse proxy, and more. I chose not to used this image because I wanted to learn how to configure Docker and Nginx. I also wanted to use a self signed cert since this will run on my home network.

The Plan


While setting up a reverse proxy with Docker isn't terribly complicated, designing how everything communicates will help us understand how to configure each component.

We're going to be running docker on a Linux server in our local network with an Nginx container that will pass http requests to the rest of the apps we want to host. The proxy will be setup to forward requests to a specific app based on the path part of the URL. As an example if someone visits https://192.168.0.10/app1/index.html, the request will be forwarded to whatever container we will configure to recieve requests under the /app1/ location. Nginx will be setup to use SSL for encryption and HTTP basic auth for authentication.

It should be noted that with using this reverse proxy method, applications need to be able to handle relative URLs. Hacky workarounds like checking the http referer and having a location rewrite rule in Nginx might work.

Setup


Network

First we will create the network so that containers can communicate with each other and use the built in DNS. Running the following command will create the network, and I'll name it proxynet.

docker network create proxynet

That's all we need to do to create the network.

Nginx

Nginx is more complicated to setup. We need to create the configuration to proxy requests, certificates for SSL, and http authentication file that will store users and passwords.

To make things easier and so we can modify the configuration while the container is running we will create a folder that we will throw all of the files into and attach it as a bind mount. In whatever directory you want to work out of, create a new directory and enter it:

mkdir proxy_config
cd proxy_config

Create the key and cert for SSL:

openssl req -subj '/CN=localhost' -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365

The Nginx documentation describes how to implement basic authentication. Since I'm using Debian, I installed apache2-utils to get the htpasswd utility that creates the file that contians users and passwords for authentication.

Run the following command to create the basic authentication file, substituting whatever username you want:

sudo htpasswd -c htpasswd username

You will be prompted for the password for the new user.

The last file we need to create is the Nginx configuration file. At this point we need to know what applications we want requests forwarded to and configure. I'm going to use the Getting Started and MeTube images to test multiple apps with the proxy. The following configuration was saved as default.config in the proxy_config folder:

server {
    listen 443 ssl; # listen on 443, the default ssl port
    # set where our key and cert for ssl is
    ssl_certificate /etc/nginx/conf.d/cert.pem;
    ssl_certificate_key /etc/nginx/conf.d/key.pem;

    server_name  localhost;
    auth_basic "Authentication";

    # set where our basic authentication file is
    auth_basic_user_file /etc/nginx/conf.d/.htpasswd;

    # use the defaults for the root location
    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
    # configuration for proxying requests to MeTube
    location /mt/ {
        proxy_pass http://metube:8081;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
    # configuration for proxying requests to the getting started app
    location /gs/ {
        proxy_pass http://getting_started/;
    }
    # defaults for errors
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

The location directives /mt/ and /gs/ in the config file will forward requests to containers on the proxynet network to applications named metube and getting_started. The MeTube documentation recommended using the extra parameters seen in the location directive, so that's why MeTube has more parameters than Getting Started.

This should be everything we need to run the containers now.

Starting Our Containers


All that's needed now is to start the containers. This command starts the Nginx container. You need to be in the parent directory of the config directory for this to propery mount the config folder.

docker run -d --network proxynet -v "$(pwd)/proxy_config:/etc/nginx/conf.d/" -p 80 -p 443:443 nginx:stable-alpine

Explantion of flags:

  • -d - Detach the container; i.e. have it run in the background.
  • --network - Attach the container to the proxynet container.
  • -v - Setup a mount volume using the '$(pwd)/proxy_config' directory on the host and attach it to '/etc/nginx/conf.d/' in the container. Note the $(pwd), which puts the full path of the working directory before /proxy_config. If you pass an absolute path to -v it will mount it to the container and if you pass it a relative path Docker will create a volume with the contents. I did not expect this behavior when I first set this up and took me awhile to understand why my updates to the configuration file weren't being applied.
  • -p - Expose ports 80 and 443. 80 is needed to forward traffic from the proxy to the apps we have setup.

Now run the command to start the the getting-started container:

docker run -d --network proxynet --name getting_started  docker/getting-started

And finally run the command to start the MeTube container:

docker run -d --network proxynet --name metube -e URL_PREFIX='/mt/'  -v /mnt/dl:/downloads alexta69/metube

The -e flag allows us to pass environment variables to the contianer. URL_PREFIX allows us to add the /mt/ prefix to the path of the URL. The -v flag is used to mount a directory on the host machine to the download path for MeTube.

Now to test if everything is working properly. With a machine on the same network as the one running docker, visit the webpage. In my case I visit https://192.168.0.10 and I get prompted for basic authentication. After entering the username and password in the created .htpasswd file, the default Nginx page is shown. Visiting https://192.168.0.10/mt/ shows the MeTube application. However, visiting https://192.168.0.10/gs/ redirects to https://192.168.0.10/tutorial/ and shows a 404 page from Nginx. I think this has to do with the Getting Started app not initially expecting a relative URL. This is fixed by adding /gs/ before tutorial in the URL: https://192.168.0.10/gs/tutorial/. So at this point, other than the issue with the Getting Started app, everything is working as expected.

To add new applications, all you have to do is run the new application contianer on the proxynet network, add the entry to the Nginx configuration file, and restart Nginx. And remember applications need to be able to handle URL prefixes or else links in them may break.

Final Thoughts


There are cleaner ways to setup a reverse proxy but you need to start somewhere. I was using this as an exercise for me to learn about Docker and how to set up a somewhat secure reverse proxy with Nginx on my home network.

Ideas for future improvements:

  • Use Docker Compose so we don't have to keep track of the commands to start the containers.
  • Create a service to detect and automatically add new applications to the Nginx configuration.
  • Use the linuxserver.io SWAG image. This has a lot of useful features, and can probably be modified to run with self signed certificates. This is probably what I'll use at some point in the future.