Cover Image

Modular Reverse Proxy with Nginx

March, 24. 2019f 2019 - Reading time: 8 minutes

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 job 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 forwarding works:

Request: app.mydomain.de  ➡️ localhost:9001
Request: www.mydomain.de ➡️ localhost:9002
Request: chat.mydomain.de ➡️ localhost:9003

The domain is mydomain.de and the different programs run in docker containers, each on a different port, here on 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.

Installation of 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 Configuration

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

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

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/mydomain.de/fullchain.cer;
ssl_certificate_key /etc/acme.sh/certs/mydomain.de/mydomain.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 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.mydomain.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/mydomain.de/fullchain.cer;
ssl_certificate_key /etc/acme.sh/certs/mydomain.de/mydomain.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 we will do this in a more elegant way. How to easily create these wildcard certificates with acme.sh will be explained in this article soon. I have disabled access.log, I love privacy.

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 sent zipped 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 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 by direct communication with the DNS, but by retrieval throough 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 using 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;
}

Final 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 coded an application for friends of the graphical user interface, which allows this whole configuration in a web interface.