How to use NGINX with Brotli on Docker

How to use NGINX with Brotli on Docker

Docker

Speed is an important metric for any website. You can reduce the amount of underlying code your site employs without impairing its functionality. Brotli compression is an alternative fledgling method to GZIP compression.

In a nutshell, Broti is a data compression algorithm. It provides “lossless” compression and is developed by Google under an MIT license. Brotli looks to take what GZIP does, improve on it, and offer an enhanced experience to users and sites. Brotli compression uses the same core base technologies as GZIP compression.

Why you should choose Brotli over GZIP:

  1. It takes the same technology as GZIP uses and enhances it with modern methods.
  2. Brotli’s dictionary-based parsing means it can compress more of your files to a deeper level.
  3. While Brotli needs more computational power compared to GZIP, the results mean smaller files.
  4. At the compression levels most web hosts use — something mid-range such as level four or five — Brotli performs better than GZIP without breaking a sweat.
  5. You’ll find that Brotli has near-universal support across browsers, if not some of the benchmark tools you’re used to.
  6. Brotli is free to use and open source. This is an advantage if you use a Broti-compatible CDN, such as Cloudflare.

It’s worth noting that Cloudflare uses Brotli compression on all its servers. In fact, it uses a modified and optimized version of Brotli to give you further gains with regard to speed and file delivery.

How to enable Brotli

This is a technical article that expects you to have experience with these types of technologies. So, I'm going straight to what needs to be done

Working on a back-end of a SaaS application, I noticed that Brotli was not enabled. Since it uses Docker and NGINX, I started searching for a way to enable it.

Most of the articles I found relied on a pre-built docker image, and this is fine. However, there are two major downsides for me; I'll be dependent on the author to update the NGINX version (imagine a vulnerability) and will not be able to easily add new NGINX modules. I could also fork it, but I would also be dependent on myself.

I found a solution that enables me to use the Brotli module and allows me to easily enable more modules in the future.

dockerfile (root folder)

FROM nginx:mainline-alpine as builder

ARG ENABLED_MODULES

RUN set -ex \\
    && if [ "$ENABLED_MODULES" = "" ]; then \\
    echo "No additional modules enabled, exiting"; \\
    exit 1; \\
    fi

COPY ./ /modules/

RUN set -ex \\
    && apk update \\
    && apk add linux-headers openssl-dev pcre-dev zlib-dev openssl abuild \\
    musl-dev libxslt libxml2-utils make mercurial gcc unzip git \\
    xz g++ coreutils \\
    # allow abuild as a root user \\
    && printf "#!/bin/sh\\\\nSETFATTR=true /usr/bin/abuild -F \\"\\[email protected]\\"\\\\n" > /usr/local/bin/abuild \\
    && chmod +x /usr/local/bin/abuild \\
    && hg clone -r ${NGINX_VERSION}-${PKG_RELEASE} <https://hg.nginx.org/pkg-oss/> \\
    && cd pkg-oss \\
    && mkdir /tmp/packages \\
    && for module in $ENABLED_MODULES; do \\
    echo "Building $module for nginx-$NGINX_VERSION"; \\
    if [ -d /modules/$module ]; then \\
    echo "Building $module from user-supplied sources"; \\
    # check if module sources file is there and not empty
    if [ ! -s /modules/$module/source ]; then \\
    echo "No source file for $module in modules/$module/source, exiting"; \\
    exit 1; \\
    fi; \\
    # some modules require build dependencies
    if [ -f /modules/$module/build-deps ]; then \\
    echo "Installing $module build dependencies"; \\
    apk update && apk add $(cat /modules/$module/build-deps | xargs); \\
    fi; \\
    # if a module has a build dependency that is not in a distro, provide a
    # shell script to fetch/build/install those
    # note that shared libraries produced as a result of this script will
    # not be copied from the builder image to the main one so build static
    if [ -x /modules/$module/prebuild ]; then \\
    echo "Running prebuild script for $module"; \\
    /modules/$module/prebuild; \\
    fi; \\
    /pkg-oss/build_module.sh -v $NGINX_VERSION -f -y -o /tmp/packages -n $module $(cat /modules/$module/source); \\
    BUILT_MODULES="$BUILT_MODULES $(echo $module | tr '[A-Z]' '[a-z]' | tr -d '[/_\\-\\.\\t ]')"; \\
    elif make -C /pkg-oss/alpine list | grep -E "^$module\\s+\\d+" > /dev/null; then \\
    echo "Building $module from pkg-oss sources"; \\
    cd /pkg-oss/alpine; \\
    make abuild-module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \\
    apk add $(. ./abuild-module-$module/APKBUILD; echo $makedepends;); \\
    make module-$module BASE_VERSION=$NGINX_VERSION NGINX_VERSION=$NGINX_VERSION; \\
    find ~/packages -type f -name "*.apk" -exec mv -v {} /tmp/packages/ \\;; \\
    BUILT_MODULES="$BUILT_MODULES $module"; \\
    else \\
    echo "Don't know how to build $module module, exiting"; \\
    exit 1; \\
    fi; \\
    done \\
    && echo "BUILT_MODULES=\\"$BUILT_MODULES\\"" > /tmp/packages/modules.env

FROM nginx:mainline-alpine

COPY --from=builder /tmp/packages /tmp/packages

RUN set -ex \\
    && . /tmp/packages/modules.env \\
    && for module in $BUILT_MODULES; do \\
    apk add --no-cache --allow-untrusted /tmp/packages/nginx-module-${module}-${NGINX_VERSION}*.apk; \\
    done \\
    && rm -rf /tmp/packages \\
    && rm /etc/nginx/conf.d/default.conf

COPY main.conf /etc/nginx/nginx.conf

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

docker-compose.yml (root folder) - merge with your current docker-compose.yml. Take a look at the ENABLED_MODULES, which will communicate with the Dockerfile, install and compile it.

version: '3.7'

services:
  nginx:
    build:
      context: .
      args:
        ENABLED_MODULES: brotli
      dockerfile: Dockerfile
    ports:
      - 80:80
      - 443:443
    command: "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \\"daemon off;\\"'"

main.conf (root folder) - in the NGINX configuration file you need to load the module (first 2 lines) and add both Brotli and GZIP to your server block.

+ load_module modules/ngx_http_brotli_filter_module.so;
+ load_module modules/ngx_http_brotli_static_module.so;

user  nginx;
worker_processes  auto;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

...

http {
  ...
   server {
    listen 443 ssl  http2;
    listen [::]:443 ssl  http2;
    
+    # GZIP Compression - In case Brotli is not supported
+    gzip on;
+    gzip_vary on;
+    gzip_proxied any;
+    gzip_comp_level 6;
+    gzip_types *;
+
+    # Brotli Compression
+    brotli on;
+    brotli_static off;
+    brotli_min_length 20;
+    brotli_comp_level 6;
+    brotli_types *;
    }
}

After all is up and running you can easily test if Brotli is working properly with this tool.

Reaching the Next Level?


I'm always looking for new challenges that can make a difference in our collective world. Feel free to shoot me an email! Whether you want to hire me, have a question or just want to say hi, I'll get back to you as soon as I can!

Engage with Me