Skip to main content

NGINX Docker Ingress for your Gatsby Build

Guangzhou, China

Update 2023: NGINX HTTP/2 Docker Ingress.

Continuation from GoFiber Container for your Gatsby Build

Github Repository

I want to continue setting up my Gatsby build webpage by adding a NGINX ingress to my Docker cluster (of one container, so far). The Webpage should be served with an URL prefix en (see NGINX Multihost) and NGINX should take care of the SSL certificate (I will use a self-signed cert for my dev environment - it can later be replaced with Certbot).

Frontend Container

I prepared my GoFiber served webpage in the previous step and am able to build and start my frontend container with:

docker build -t wiki_en .
docker run -d -p 8888:8888 wiki_en

Now I can access the web frontend served on port 8888:

Go to http://localhost:8888

Prefix URL

To add the prefix URL I now have to tell Gatsby to automatically prefix all links for me. This can be done by adding the pathPrefix to the gatsby-config.js file:

module.exports = {
  pathPrefix: '/en',

  ...

Now I have to re-run the gatsby build command with the --prefix-path flag. I will add an NPM Script for it, that also raises the Node.js cache size for me --max-old-space-size=8192 (my page seems to need that):

  "scripts": {
    "prefix": "node --max-old-space-size=8192 node_modules/gatsby/dist/bin/gatsby build --prefix-paths",

    ...

To run the script run the following command from the root directory of your project:

npm run prefix

If you try to run the GoFiber container now and access it via http://localhost:8888 most of the page will be broken - due to the prefixed links. So lets prepare the NGINX container to take care of that!

NGINX Ingress

Since accessing our container directly now no longer works, we kill it and replace it with a container instance without an exposed port:

docker stop <containerID> && docker rm <containerID>
docker run -d --name wiki_en wiki_en

Update: I just noticed that I already exposed port 8888 inside my Dockerfile. Comment out the following lines and rebuild your container, if you want to prevent traffic from accessing your container directly. Or block access to port 8888 with your firewall.

# Exposes port 8888 because our program listens on that port
# EXPOSE 8888

Container Network

Now we have a container running with no exposed ports to the client, but we need to get it to communicate with with our ingress. In order to do that we need to put them all into the same virtual network. Let’s create our network:

docker network create wikinet

Now add the container by it's name:

docker network connect wikinet wiki_en

Let’s see if they have been added:

docker network inspect wikinet

The output show us that our container has been added successfully:

[
  {
    "Name": "wikinet",
    "Id": "725dfcde3015d752f8d0c4bbfe2027d9a4cb1cb3c6cc9b4f4094fb33d5a1d6bc",
    "Created": "2021-05-10T15:07:38.420998455+08:00",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
      "Driver": "default",
      "Options": {},
      "Config": [
        {
          "Subnet": "172.18.0.0/16",
          "Gateway": "172.18.0.1"
        }
      ]
    },
    "Internal": false,
    "Attachable": false,
    "Ingress": false,
    "ConfigFrom": {
      "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
      "e903e22b56f21973ade9bab9aa58aa8994b0a2399eadff36a4801fbc26fb82d4": {
        "Name": "wiki_en",
        "EndpointID": "82090263f6853a6219989828ade375a4db5aa655561293bf5bce0d7dfeb4640a",
        "MacAddress": "02:42:ac:12:00:02",
        "IPv4Address": "172.18.0.2/16",
        "IPv6Address": ""
      }
    },
    "Options": {},
    "Labels": {}
  }
]

Configuring the Docker Ingress

Before running the NGINX container we first need to provide a configuration file default.conf:

mkdir /opt/docker_ingress
nano /opt/docker_ingress/default.conf

Docker provides a DNS service for it's virtual networks. To connect our NGINX ingress we just need to set a proxy_pass to the container name wiki_en with the port 8888:

server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    #  redirect from / to /en to make it the default location (optional)

    location / {
      rewrite   ^/(.*)$  /en/$1  permanent;
    }

    location /en/ {
      proxy_pass http://wiki_en:8888/;
    }

    # redirect server error pages to the static page /50x.html

    error_page  404 /404.html;
    error_page  500 502 503 504 /50x.html;
    location = /50x.html {
    root   /usr/share/nginx/html;
    }
}

We can now start the NGINX ingress with the configuration file:

docker run -d -p 80:80 -v /opt/docker_ingress:/etc/nginx --network=wikinet --name ingress nginx:1.21.1-alpine

You can test it by accessing the docker host IP address followed by the language prefix we defined in NGINX. You should automatically be redirected to the /en/ prefix and your Gatsby App should load correctly, again.

Encryption

We can now continue with adding a self-signed SSL certificate to our app. My webserver uses Debian Bullseye the following steps might differ for your OS of choice. We can create a self-signed key and certificate pair with OpenSSL in a single command:

mkdir -p /opt/docker_ingress/ssl/{private,certs}

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /opt/docker_ingress/ssl/private/nginx-selfsigned.key -out /opt/docker_ingress/ssl/certs/nginx-selfsigned.crt

Make sure to use either your servers IP (public or local) or it's domain (if available) when asked for Common Name:

Generating a RSA private key
writing new private key to '/opt/docker_ingress/ssl/private/nginx-selfsigned.key'
-----
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:192.168.2.111
Email Address []:

While we are using OpenSSL, we should also create a strong Diffie-Hellman group, which is used in negotiating Perfect Forward Secrecy with clients:

openssl dhparam -out /opt/docker_ingress/ssl/private/dhparam.pem 4096

Let’s now create a new Nginx configuration snippet in the /opt/docker_ingress directory:

nano /opt/docker_ingress/self-signed.conf

Within this file, we need to set the ssl_certificate directive to our certificate file and the ssl_certificate_key to the associated key. Add the following lines to the file:

ssl_certificate /etc/nginx/conf.d/ssl/certs/nginx-selfsigned.crt;
ssl_certificate_key /etc/nginx/conf.d/ssl/private/nginx-selfsigned.key;

Just a side note: If you mix these up key :: cert you end up with the following NGINX error: cannot load certificate "/etc/nginx/conf.d/ssl/private/nginx-selfsigned.key": PEM_read_bio_X509_AUX() failed (SSL: error:0909006C:PEM routines:get_name:no start line:Expecting: TRUSTED CERTIFICATE) :)

Next, we will create another snippet that will define some SSL settings. This will set Nginx up with a strong SSL cipher suite and enable some advanced features that will help keep our server secure:

nano /opt/docker_ingress/ssl-params.conf
ssl_protocols TLSv1.3 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
ssl_ciphers ECDH+AESGCM:ECDH+CHACHA20:ECDH+AES256:ECDH+AES128:!aNULL:!SHA1:!AESCCM;
ssl_conf_command Options PrioritizeChaCha; # Requires nginx >= nginx:1.21
ssl_conf_command Ciphersuites TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256:TLS_CHACHA20_POLY1305_SHA256; # Requires nginx >= nginx:1.21
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_timeout  10m;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
# add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
# add_header X-Frame-Options DENY;
add_header X-Frame-Options "";
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";

Now we just have to link our snippets into the default NGINX configuration.

nano /opt/docker_ingress/default.conf
server {
    listen 80;
    listen [::]:80;

    server_name my.domain.com;

    return 301 https://$server_name$request_uri;
}


server {
    listen 443 ssl http2 default_server;
    listen [::]:443 ssl;
    # ssl_certificate /opt/letsencrypt/live/my.domain.com/fullchain.pem;
    # ssl_certificate_key /opt/letsencrypt/live/my.domain.com/privkey.pem;
    include ssl/self-signed.conf; # Replace with the 2 lines above when using CA Cert
    include ssl/ssl-params.conf;
    include /etc/nginx/conf.d/header.conf;

    server_name my.domain.com;

    location / {
        rewrite ^/(.*)$  /en/$1  permanent;
    }

    location /de/ {
      proxy_pass http://my_container_de:9999/;
    }

    location /en/ {
      proxy_pass http://my_container_en:8888/;
    }

    location /fr/ {
       proxy_pass http://my_container_fr:7777/;
    }


    error_page  404 /404.html;
    error_page  500 502 503 504 /50x.html;
    location = /50x.html {
    root   /usr/share/nginx/html;
  }
}

Now kill both containers and restart them with:

docker run -d --network=wikinet --name wiki_en  wiki_en

docker run -d -p 443:443 -p 80:80 -v /opt/docker_ingress:/etc/nginx --network=wikinet --name ingress nginx:1.21.1-alpine

Testing

Test if NGINX is using the correct encryption protocols:

# Test Nginx for TLS 1
curl -I -v --tlsv1 --tls-max 1.0 https://www.example.com/

# Test Nginx for TLS 1.1
curl -I -v --tlsv1.1 --tls-max 1.1 https://www.example.com/

# Test Nginx for TLS 1.2
curl -I -v --tlsv1.2 --tls-max 1.2 https://www.example.com/

# Test Nginx for TLS 1.3
curl -I -v --tlsv1.3 --tls-max 1.3 https://www.example.com/