Skip to main content

NGINX Docker Setup

Victoria Harbour, Hong Kong

I want to create a template configuration for the Alpine-based NGINX Docker image from DockerHub: nginx:stable-alpine. The idea is to have folder with configuration files that I can modify - according to my project - and then mount as a volume into vanilla NGINX container to proxy my web applications:

docker run -d -p 80:80 -v /opt/nginx_config:/etc/nginx/conf.d --name nginx nginx:stable-alpine

NGINX Configuration Overview

The default NGINX configuration file is structured like this:

# main
...
events {
    ...
}


http {
    server {
        location {
            ...
        }
    }
}

Each Block in the configuration file:

main {{events {http { server { location {}}}}}}

Is called a Context. Each Context holds Directives which are parameter / value pairs like server_name localhost;. Such blocks can be separated from the main configuration file and then included using the include keyword:

include /etc/nginx/conf.d/*.conf;

Testing Configuration

After make changes to your configuration file you can validate it in NGINX without having to reload your service:

nginx -t

To apply those changes you have to reload or restart the NGINX service. The restart will stop the service and start it again, while the reload will keep the original process running. In the latter case, if something goes wrong with the configuration update, it will simply be discarded and your server stays online with the old configuration:

systemctl reload nginx
systemctl restart nginx

However, since the Docker image does not use SystemD to run the service, you can test the configuration inside the NGINX docker container but then have to restart the container to have it take effect.

docker exec -ti name-or-id-of-my-nginx-container nginx -t

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
docker stop name-or-id-of-my-nginx-container
docker start name-or-id-of-my-nginx-container

Location Context Modifier

The location match defines what NGINX checks the incoming request against. How the match is applied can be influenced by a modifier:

ModifierDescription
nonematched as prefix value
=matched as an exact value
~matched as case sensitive regular expression
~*matched as case insensitive regular expression
^~matched as prefix value with higher importance than a regular expression

The modifiers are processed in the following order:

OrderModifier
1=
2^~
3~ and ~*
4none

Prefix

location /de {

}

No modifier matches as a prefix example matches are:

http://URL/de
http://URL/de123
http://URL/deutsch
etc.

Exact Value

location = /de {

}

Only an exact match will pass:

http://URL/de

Case Sensitive Regular Expression

location ~ /de[0-9] {

}
location ~ /de[a-z] {

}

This regular expression expects that the /de is either followed by numbers or by non-capital letters. Capital letters or mixed numbers and small letters will not match:

http://URL/de122
http://URL/deutsch
etc.

Case Insensitive Regular Expression

location ~ /de[0-9] {

}
location ~ /de[a-z] {

}

This regular expression expects that the /de is either followed by numbers or by letters. Mixed numbers and small letters will not match:

http://URL/de122
http://URL/deutsch
http://URL/deUTSCH
etc.

Try Files Directive

The try_files directive to match the incoming URL to different setups - for example to $uri or $uri/ with a trailing backslash or to a static file:

location / {
    try_files $uri $uri/ /index.html;
}

NGINX will first try to find an exact match. Then try to add a trailing backslash. And lastly redirect you to the front page if no other fit was found.

Structuring the Main Configuration File

/etc/nginx/nginx.conf

user  nginx;
worker_processes  auto;
worker_rlimit_nofile  15000;
pid  /var/run/nginx.pid;
include /usr/share/nginx/modules/*.conf;


events {
    worker_connections  2048;
    multi_accept on;
    use epoll;
}


http {
    default_type   application/octet-stream;
    # access_log   /var/log/nginx/access.log;
    # activate the server access log only when needed
    access_log     off;
    error_log      /var/log/nginx/error.log;
    # don't display server version on error pages
    server_tokens  off;
    server_names_hash_bucket_size 64;
    include        /etc/nginx/mime.types;
    sendfile       on;
    tcp_nopush     on;
    tcp_nodelay    on;

    charset utf-8;
    source_charset utf-8;
    charset_types text/xml text/plain text/vnd.wap.wml application/javascript application/rss+xml;

    include /etc/nginx/conf.d/default.conf;
    include /etc/nginx/conf.d/buffers.conf;
    include /etc/nginx/conf.d/timeouts.conf;
    # Only activate caching in production
    # include /etc/nginx/conf.d/cache.conf;
    include /etc/nginx/conf.d/gzip.conf;
}

Set SELinux policy to allow setrlimit (worker_rlimit_nofile): setsebool -P https_setrlimit 1. A low r limit might lead to an error message of too many open files under load.

This configuration file is missing the server and location context. Both of them will reside in a separate file in the included conf.d directory called default.conf. Along side we also include a couple of configuration files:

/etc/nginx/conf.d/buffers.conf

client_body_buffer_size 10k;
client_header_buffer_size 1k;
client_max_body_size 8m;
large_client_header_buffers 2                                   1k;
# large_client_header_buffers directive needs to be increased
# large_client_header_buffers                                   4 32k;

/etc/nginx/conf.d/timeouts.conf

client_header_timeout 3m;
client_body_timeout 3m;
keepalive_timeout 100;
keepalive_requests 1000;
send_timeout 3m;

/etc/nginx/conf.d/cache.conf

open_file_cache max=1500 inactive=20s;
open_file_cache_valid 30s;
open_file_cache_min_uses 5;
open_file_cache_errors off;

/etc/nginx/conf.d/gzip.conf

gzip on;
gzip_disable "MSIE [1-6]\.(?!.*SV1)";;
gzip_vary on;
gzip_proxied no-cache no-store private expired;
gzip_comp_level 5;
gzip_min_length 256;
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
image/svg+xml application/xhtml+xml application/atom+xml application/x-javascript;

Setting up the Server Context

The application that I am serving has a path prefix to /en/. That means I have to redirect all incoming traffic on / to /en/. The static HTML content that I am serving lies in /opt/test-static/public and has to be set as the root directory for my server.

/etc/nginx/conf.d/default.conf

server {
    listen       80 default_server;
    listen       [::]:80 default_server;
    server_name  localhost;
    root         /opt/test-static/public;
    index        index.html;

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

    location /en/ {
        try_files  $uri $uri/ $uri.html =404;
        include    /etc/nginx/conf.d/header.conf;
    }

    location ~ /en/download_area/.* {
        root /opt/downloads;
        # deactivate autoindex to use try_files
        # try_files $uri /en/download_area/morning.gif;
        autoindex on;
        autoindex_exact_size off;
        autoindex_format html;
        autoindex_localtime on;
    }

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

/etc/nginx/conf.d/header.conf

add_header                Cache-Control  "public, must-revalidate, proxy-revalidate, max-age=0";
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;

Docker Compose

version: '3.8'
services:
  ingress:
    image: nginx:stable-alpine
    container_name: ingress
    networks:
      - gateway
    ports:
      - '80:80'
      - '443:443'
    restart: unless-stopped
    volumes:
      - /opt/docker-ingress/configuration/conf.d:/etc/nginx/conf.d
      - /opt/docker-ingress/configuration/nginx.conf:/etc/nginx/nginx.conf
      - /opt/test-static/public:/opt/test-static/public
      - /opt/downloads:/opt/downloads

networks:
  gateway: {}

Tree structure for the mounted download directory:

/opt/downloads
└── en
    └── download_area
        ├── morning.gif
        └── top-cat.png

2 directories, 2 files