Skip to main content

NGINX Docker Multihost

Victoria Harbour, Hong Kong

I do have 3 frontend containers - running node/express frontend applications in different languages. Instead of running all of them on different domains I want to have them running on sub-domains:

Current State:

https://wiki.instar.com/
https://wiki.instar.de/
https://wiki.instar.fr/

Wanted State:

https://wiki.instar.com/en/
https://wiki.instar.com/de/
https://wiki.instar.com/fr/

Frontend Container

Pull your frontend container:

docker login -u me@email-address.com gitlab.example.com:34578
docker pull gitlab.example.com:34578/wiki/wiki-instar-en-docker:latest
docker pull gitlab.example.com:34578/wiki/wiki-instar-de-docker:latest
docker pull gitlab.example.com:34578/wiki/wiki-instar-fr-docker:latest

And start them:

docker run -d --name wiki-instar-en gitlab.example.com:34578/wiki/wiki-instar-en-docker:latest
docker run -d --name wiki-instar-de gitlab.example.com:34578/wiki/wiki-instar-de-docker:latest
docker run -d --name wiki-instar-fr gitlab.example.com:34578/wiki/wiki-instar-fr-docker:latest

All apps provide their own express webserver that is hosting static page content on a unique port like 8081, 8082 and 8083. Since we did not forward this port the web applications are now trapped inside the docker network.

Container Network

Now we have 3 containers running with no exposed ports to the client, but we need to get them to communicate with each other. In order to do that we need to be able to put them all under the same network. Let’s create our network:

docker network create wikinet

Now add the container by their name:

docker network connect wikinet wiki-instar-en
docker network connect wikinet wiki-instar-de
docker network connect wikinet wiki-instar-fr

Let’s see if they have been added:

docker network inspect wikinet

The output show us that all 3 apps have been added successfully:

[
  {
    "Name": "wikinet",
    "Id": "1c5dac2e9a7496340a316c1d65f9768ed475547af6c3b7a46662d5aaedad288a",
    "Created": "2020-09-03T12:35:31.598865218Z",
    "Scope": "local",
    "Driver": "bridge",
    "EnableIPv6": false,
    "IPAM": {
      "Driver": "default",
      "Options": {},
      "Config": [
        {
          "Subnet": "172.19.0.0/16",
          "Gateway": "172.19.0.1"
        }
      ]
    },
    "Internal": false,
    "Attachable": false,
    "Ingress": false,
    "ConfigFrom": {
      "Network": ""
    },
    "ConfigOnly": false,
    "Containers": {
      "0114e569753277d3f9a6087515e5bec428cdc6088c312e112a8b34e04e08de43": {
        "Name": "wiki-instar-fr",
        "EndpointID": "befe819138c011efd0761e6cfe0daf10b70d05911034c61367b76aa3c8b6e561",
        "MacAddress": "02:42:ac:13:00:04",
        "IPv4Address": "172.19.0.4/16",
        "IPv6Address": ""
      },
      "1e14d78240c8ff11e0cd18424fb05d7f35e95d8194dfc7b2e4eba83ecee6558e": {
        "Name": "wiki-instar-en",
        "EndpointID": "8d16f5fe96448d08f995c4f67c9947e25fe56b4edcae48ad9b8b2ac1995ef8a8",
        "MacAddress": "02:42:ac:13:00:02",
        "IPv4Address": "172.19.0.2/16",
        "IPv6Address": ""
      },
      "cb68e1b721b4bd90b4176c6ffe67f87f0aa38f8bc8affeab15502abd7c708bd7": {
        "Name": "wiki-instar-de",
        "EndpointID": "75ba745de043af8224954593a24f1ce431f9b5cc0569aa8a8d6e0ee70e30c7c0",
        "MacAddress": "02:42:ac:13:00:03",
        "IPv4Address": "172.19.0.3/16",
        "IPv6Address": ""
      }
    },
    "Options": {},
    "Labels": {}
  }
]

Docker provides a DNS service to resolve IP address inside it's virtual networks. This means we should now be able to load the web content from one container by sending a wget or curl command from another. Let's try this by entering one of our containers:

docker exec -ti wiki-instar-en /bin/bash

Once we are in we can request the web content from another container:

curl localhost:8081
curl wiki-instar-de:8082
curl wiki-instar-fr:8083

Configuring the Docker Ingress

I am going to use the NGINX reversed proxy to build an ingress to the virtual network. We could create the ingress by spawning a container from the default NGINX image and then editing the configuration file inside the container:

docker run -p 80:80 --network=wikinet --name ingress nginx:stable-alpine

Depending on how you configure your cluster and Gatsby there might be some differences in how you have to configure NGINX to work as a proxy here. I just tried to use the following configuration in a different build and ran into some difficulties with changes I made in Gatsby. Please also check out the NGINX Docker Ingress article if you have problems here as well!

But we can also create the configuration file on our host system and then mount it into the container /etc/nginx/conf.d/default.conf to persist it. We can start by modifying the default config file that comes with NGINX:

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

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    # proxy the PHP scripts to Apache listening on 127.0.0.1:80
    #
    #location ~ \.php$ {
    #    proxy_pass   http://127.0.0.1;
    #}

    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass   127.0.0.1:9000;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;
    #}

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #
    #location ~ /\.ht {
    #    deny  all;
    #}
}

Create a folder on your host system and create the default.conf file:

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

    location /en {
        proxy_pass http://wiki-instar-en:7777/;
    }

    location /fr {
        proxy_pass http://wiki-instar-fr:7778/;
    }

    location /de {
         proxy_pass http://wiki-instar-de:7779/;
    }

    # redirect server error pages to the static page /50x.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 -p 443:443 -v /opt/nginx_docker_ingress:/etc/nginx --network=wikinet --name ingress nginx:alpine

Verify that the ingress is running and that you can now finally access your apps from outside of the docker virtual network. You can test it by accessing the docker host IP address followed by the language prefix we defined in NGINX: /en, /de, /fr.

We can also add the following rewrite location block to automatically forward traffic that enters through the root URL to /en/

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

We can now add some web proxy seasoning to optimize everything:

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

    charset koi8-r;

    # Gzip Compression
        gzip                                    on;
        gzip_disable                            "MSIE [1-6]\.(?!.*SV1)";
        gzip_proxied                            no-cache no-store private expired;
        gzip_buffers                            16 8k;
        gzip_comp_level                         6;
        gzip_types                              text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
        gzip_vary                               on;


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

    location /en/ {
        proxy_set_header                        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header                        X-NginX-Proxy true;
        proxy_set_header                        X-Real-IP $remote_addr;
        proxy_set_header                        X-Forwarded-Proto http;
        proxy_hide_header                       X-Frame-Options;
        proxy_set_header                        Accept-Encoding "";
        proxy_http_version                      1.1;
        proxy_set_header                        Upgrade $http_upgrade;
        proxy_set_header                        Connection "upgrade";
        proxy_set_header                        Host $host;
        proxy_cache_bypass                      $http_upgrade;
        proxy_max_temp_file_size                0;
        proxy_redirect                          off;
        proxy_read_timeout                      240s;
        proxy_pass                              http://127.0.0.1:7777/en/;
    }

    location /fr {
        proxy_set_header                        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header                        Host $http_host;
        proxy_set_header                        X-NginX-Proxy true;
        proxy_hide_header                       X-Frame-Options;
        proxy_http_version                      1.1;
        proxy_set_header                        Upgrade $http_upgrade;
        proxy_set_header                        Connection "upgrade";
        proxy_max_temp_file_size                0;
        proxy_redirect                          off;
        proxy_read_timeout                      240s;
        proxy_pass                              http://wiki-instar-fr:7778/;
    }

    location /de {
        proxy_set_header                        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header                        Host $http_host;
        proxy_set_header                        X-NginX-Proxy true;
        proxy_hide_header                       X-Frame-Options;
        proxy_http_version                      1.1;
        proxy_set_header                        Upgrade $http_upgrade;
        proxy_set_header                        Connection "upgrade";
        proxy_max_temp_file_size                0;
        proxy_redirect                          off;
        proxy_read_timeout                      240s;
        proxy_pass                              http://wiki-instar-de:7779/;
    }

    # All static files should be cached forever.
        location ~* \.(jpg|png|ico)$ {
        add_header                                  Cache-Control "public, max-age=31536000, immutable";
        access_log                              off;
        log_not_found                           off;
        }

    # JS, CSS and HTML files should never be cached.
        location ~* \.(css|js|html)$ {
                add_header                      Cache-Control  "public, must-revalidate, proxy-revalidate, max-age=0";
    }

    # this prevents hidden files (beginning with a period) from being served
    location ~ /\. {
        access_log                              off;
        log_not_found                           off;
        deny                                    all;
    }

    error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}

Gatsby App

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

docker run -d --network host -v /opt/hapi-container-en/public:/wiki_en_ssr/public --name wiki_en wiki-instar-com

docker run -d --network host -v /opt/hapi-container-de/public:/wiki_de_ssr/public --name wiki_de wiki-instar-de

docker run -d --network host -v /opt/hapi-container-fr/public:/wiki_fr_ssr/public --name wiki_fr wiki-instar-fr
server {
    listen       80;
    listen  [::]:80;
    server_name  localhost;

    charset koi8-r;

    # Gzip Compression
        gzip                                    on;
        gzip_disable                            "MSIE [1-6]\.(?!.*SV1)";
        gzip_proxied                            no-cache no-store private expired;
        gzip_buffers                            16 8k;
        gzip_comp_level                         6;
        gzip_types                              text/plain application/javascript application/x-javascript text/javascript text/xml text/css;
        gzip_vary                               on;


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

    location /en/ {
        proxy_set_header                        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header                        X-NginX-Proxy true;
        proxy_set_header                        X-Real-IP $remote_addr;
        proxy_set_header                        X-Forwarded-Proto http;
        proxy_hide_header                       X-Frame-Options;
        proxy_set_header                        Accept-Encoding "";
        proxy_http_version                      1.1;
        proxy_set_header                        Upgrade $http_upgrade;
        proxy_set_header                        Connection "upgrade";
        proxy_set_header                        Host $host;
        proxy_cache_bypass                      $http_upgrade;
        proxy_max_temp_file_size                0;
        proxy_redirect                          off;
        proxy_read_timeout                      240s;
        proxy_pass                              http://127.0.0.1:7777/en/;
	    root									/opt/docker_ingress/app;
    }

    location /fr/ {
        proxy_set_header                        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header                        X-NginX-Proxy true;
        proxy_set_header                        X-Real-IP $remote_addr;
        proxy_set_header                        X-Forwarded-Proto http;
        proxy_hide_header                       X-Frame-Options;
        proxy_set_header                        Accept-Encoding "";
        proxy_http_version                      1.1;
        proxy_set_header                        Upgrade $http_upgrade;
        proxy_set_header                        Connection "upgrade";
        proxy_set_header                        Host $host;
        proxy_cache_bypass                      $http_upgrade;
        proxy_max_temp_file_size                0;
        proxy_redirect                          off;
        proxy_read_timeout                      240s;
        proxy_pass                              http://127.0.0.1:7778/;
	    root									/opt/docker_ingress/app;
    }

    location /de/ {
        proxy_set_header                        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header                        X-NginX-Proxy true;
        proxy_set_header                        X-Real-IP $remote_addr;
        proxy_set_header                        X-Forwarded-Proto http;
        proxy_hide_header                       X-Frame-Options;
        proxy_set_header                        Accept-Encoding "";
        proxy_http_version                      1.1;
        proxy_set_header                        Upgrade $http_upgrade;
        proxy_set_header                        Connection "upgrade";
        proxy_set_header                        Host $host;
        proxy_cache_bypass                      $http_upgrade;
        proxy_max_temp_file_size                0;
        proxy_redirect                          off;
        proxy_read_timeout                      240s;
        proxy_pass                              http://127.0.0.1:7779/;
	    root									/opt/docker_ingress/app;
    }

    # All static files should be cached forever.
        location ~* \.(jpg|png|ico)$ {
        add_header                                  Cache-Control "public, max-age=31536000, immutable";
        access_log                              off;
        log_not_found                           off;
        }

    # JS, CSS and HTML files should never be cached.
        location ~* \.(css|js|html)$ {
                add_header                      Cache-Control  "public, must-revalidate, proxy-revalidate, max-age=0";
    }

    # this prevents hidden files (beginning with a period) from being served
    location ~ /\. {
        access_log                              off;
        log_not_found                           off;
        deny                                    all;
    }

    error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    #
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }
}