Modular Reverse Proxy with Nginx

My current project: I have a virtual server (VPS) on which I would like to run all my software projects. This server has a fixed IP address, but I want to run very different software on it, in docker containers.

My current project: I have a virtual server (VPS) on which I would like to run all my software projects. This server has a fixed IP address, but I want to run very different software on it, in docker containers. The reverse proxy has the task to look which subdomain (e.g. app, www or chat) of my domain is addressed and then forward the request to the corresponding program.

All software runs on the same computer (localhost), but this is not even necessary. Here are a few examples of how the redirection works:

Anfrage: app.meinedomain.de ➡️ localhost:9001
Anfrage: www.meinedomain.de ➡️ localhost:9002
Anfrage: chat.meinedomain.de ➡️ localhost:9003

The domain is meinedomain.de and the different programs run in docker containers and each on a different port, here on the localhost.

Preparation

The following preparations are necessary:

  • Configure your DNS server so that the individual sub-domains point to the IP address of your web server.
  • Then generate the SSL certificates using an API request to your DNS.
  • Install the individual applications before or after, for example with Docker, preferably on the same host, so you can see if it really works.

Installing nginx

First the software nginx itself must be installed, here for Ubuntu/Debian;

sudo apt update; sudo apt install nginx

or Manjaro/Arch-Linux:

sudo pacman -S nginx

I myself use Debian as operating system for this project.

Modular structure of the configuration

At the beginning we configure two virtual servers, one for our application, which should be accessible later under https://app.meinedomain.de and a second default entry, which intercepts all calls to the domain meinedomain.de, which are not deposited with an application.

Here first the default entry, we change /etc/nginx/sites-available/default like this:

server {
  listen 80 default_server;

  # Everything is a 404
  error_page 404 /404.html;
  location / {
    return 404;
  }

  access_log off;
}

server {
  listen 443 ssl http2 default_server;

  # Let's Encrypt SSL
  ssl_certificate     /etc/acme.sh/certs/meinedomain.de/fullchain.cer;
  ssl_certificate_key /etc/acme.sh/certs/meinedomain.de/meinedomain.de.key;

  include include.d/ssl.conf;

  # Always just 404
  error_page 404 /404.html;
  location / {
    return 404;
  }

  access_log off;
}

So we have an entry point for the normal HTTP on port 80 and a HTTPS target on port 443.

And here is the second configuration for your own app:

server {
  server_name app.meinedomain.de;
  listen 80;
  listen 443 ssl http2;

  # Proxy Settings
  set $forward_scheme http;
  set $server         "127.0.0.1";
  set $port           9001;

  # Let's Encrypt SSL
  ssl_certificate     /etc/acme.sh/certs/meinedomain.de/fullchain.cer;
  ssl_certificate_key /etc/acme.sh/certs/meinedomain.de/meinedomain.de.key;

  if ($scheme = "http") {
    return 301 https://$host$request_uri;
  }

  include include.d/ssl.conf;
  include include.d/gzip.conf;
  include include.d/proxy.conf;
  # include include.d/acme.conf;

  access_log off;
}

The file acme.conf is commented out here. It is only needed if the certificates are to be retrieved from Letsencrypt using an HTTP challenge-response procedure. I'll explain this below, although I'm more elegant about it. How to easily create these elegant wildcard certificates with acme.sh will be explained in this article soon. I have disabled access.log, I love privacy.

The modular include files

In preparation, we create the directory /etc/nginx/include.d for our files to be included. In this directory we add the following four files, which we include in our configurations as described above:

The file ssl.conf. This file contains important general settings, such as the SSL session timeout or HSTS:

ssl_session_timeout 5m;
ssl_session_cache shared:SSL:50m;

# intermediate configuration. tweak to your needs.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-
ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AE
S128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
ssl_prefer_server_ciphers on;

# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
add_header Strict-Transport-Security max-age=15768000;

The file gzip.conf. This file specifies that all of our responses to web requests are zipped and sent over the channel so that not so much data is transported. This is especially good for mobile devices:

gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_buffers 16 8k;
gzip_http_version 1.1;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript application/activity+json application/atom+xml;

The file proxy.conf. This file is suitable for all redirects because the variables used in it for the redirection scheme http, the localhost and the respective port of the application gaz are set centrally in the server configuration (see above):

 location / {
  add_header       X-Served-By $host;
  proxy_set_header Host $host;
  proxy_set_header X-Forwarded-Scheme $scheme;
  proxy_set_header X-Forwarded-Proto  $scheme;
  proxy_set_header X-Forwarded-For    $remote_addr;
  proxy_pass       $forward_scheme://$server:$port;
}

And finally the file acme.conf. This file is only needed if the certificates required for SSL are requested with a verification not via a communication with the DNS, but directly via a retrieval via port 80 on the respective domain.

This can be especially useful if you don't have API access to the DNS or if you have many single small domains. If you include this file, then all requests are forwarded to a special directory /var/www/letsencrypt when the Letsencrypt certificates are created on port 80:

location ^~ /.well-known/acme-challenge/ {
    auth_basic off;
    default_type "text/plain";
    root /var/www/letsencrypt;
}

location = /.well-known/acme-challenge/ {
    return 404;
}

Result

With these 6 files you can now cover almost any requirement. The forwarding from the central reverse proxy to the respective target applications with their individual ports is done in no time at all. 👍🏻

Many thanks especially to jc21 from Brisbane, where I cribbed a lot of stuff. He programmed an application for friends of the graphical user interface, which allows this whole configuration in a web interface.