Skip to main

Add Brotli and Gzip compression to nginx in Docker

8 min read

Brotli provides improved compression ratio and performance compared to gzip and can greatly improve webpage loading performance since we are sending lot smaller bytes down the wire. Web browser support for brotli is pretty good too, almost all latest browsers support brotli file format decompression. We can verify it by looking at Accept-Encoding: br request header.

nginx comes with gzip module support by default (we need to turn it on / opt in) but to setup brotli support we need to reconfigure nginx modules. I’ll be using Docker to setup nginx with brotli compression but overall steps should be somewhat similar if you’re setting it up manually in a server.

Dockerfile

Let’s create a simple DockerFile and use the latest stable nginx docker image and set the working directory to /root/ (this can be whatever you want)

FROM nginx:1.23.2-alpine
WORKDIR /root/

Add a RUN command:

FROM nginx:1.23.2-alpine
WORKDIR /root/

RUN apk add --update --no-cache git pcre-dev openssl-dev zlib-dev linux-headers build-base \
    && wget http://nginx.org/download/nginx-1.23.2.tar.gz \
    && tar zxf nginx-1.23.2.tar.gz \
    && git clone https://github.com/google/ngx_brotli.git \
    && cd ngx_brotli \
    && git submodule update --init --recursive \
    && cd ../nginx-1.23.2 \
    && ./configure --add-dynamic-module=../ngx_brotli --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-Os -fomit-frame-pointer -g' --with-ld-opt=-Wl,--as-needed,-O1,--sort-common \
    && make modules

RUN command does these things:

  1. Update the packages, install git and other dependencies
  2. Download the nginx package (same version as our docker image) and unzip it in the current working directory
  3. Clone the ngx_brotli repository from github and run update submodule to fetch all data from the project
  4. cd back into nginx package that we downloaded and run configure script with:
    • Default Arguments (nginx -V to get the default compile arguments) nginx configure default arguments
    • Add Dynamic Module argument pointing to our downloaded ngx_brotli repository (../ngx_brotli)
  5. Compile the dynamic modules using make modules which can be loaded using load_module in our nginx.conf

Note that we are combining/chaining multiple commands here using && into a single RUN command because docker executes each instruction in a new standalone container/layer (except FROM). We want our cd, wget, make, ./configure etc commands to run in the same context under same directory (PWD), there is a workaround for this and it would require us to manually add absolute paths to our commands but I find this to be much easier and it also reduces number of layers and size of our resulting docker image.

Load Brotli Module and Enable it

Now in our nginx.conf file we load the brotli module.

load_module /root/nginx-1.23.2/objs/ngx_http_brotli_filter_module.so;
load_module /root/nginx-1.23.2/objs/ngx_http_brotli_static_module.so;

We can enable the brotli module and configure it:

load_module /root/nginx-1.23.2/objs/ngx_http_brotli_filter_module.so;
load_module /root/nginx-1.23.2/objs/ngx_http_brotli_static_module.so;

user nginx;

worker_processes 1;

events {
  worker_connections 1024;
}

http {
  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  root /usr/share/nginx/html;

  server {
    listen 80;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    # Add Brotli compression config
    brotli on;
    brotli_comp_level 6;
    brotli_static on;
    brotli_types application/atom+xml application/javascript application/json application/rss+xml
             application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype
             application/x-font-ttf application/x-javascript application/xhtml+xml application/xml
             font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon
             image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml text/html;
  }

}

Dockerfile till now that sets up brotli compression and serves static files from /usr/share/nginx/html/

FROM nginx:1.23.2-alpine AS builder
WORKDIR /root/

RUN apk add --update --no-cache git pcre-dev openssl-dev zlib-dev linux-headers build-base \
    && wget http://nginx.org/download/nginx-1.23.2.tar.gz \
    && tar zxf nginx-1.23.2.tar.gz \
    && git clone https://github.com/google/ngx_brotli.git \
    && cd ngx_brotli \
    && git submodule update --init --recursive \
    && cd ../nginx-1.23.2 \
    && ./configure --add-dynamic-module=../ngx_brotli --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-Os -fomit-frame-pointer -g' --with-ld-opt=-Wl,--as-needed,-O1,--sort-common \
    && make modules

# Updated nginx.conf file with load_module for brotli
COPY nginx.conf /etc/nginx/nginx.conf

WORKDIR /usr/share/nginx/html/
COPY www .

I have made a www directory and added index.html and a styles.css file to test brotli compression. If you see the brotli_types in the nginx.conf file, we have text/css and text/html added as the potential file types that we would like to be compressed.

Let’s test it out by building and starting the container.

docker build -t nginx-gzip-brotli .
docker run --rm -itd -p 3000:80 nginx-gzip-brotli

Make sure the browser supports brotli compression, i.e sends Accept-Encoding: br header

Browser Supports Brotli using Accept-Encoding Header

Neat! We get the response in brotli format:

Brotli Compression headers in browser Brotli Compression headers in browser

Add fallback Gzip compression

Now let’s also add gzip compression to nginx so browsers that don’t yet support brotli format can use gzip. Since its support is built into nginx (I mean nginx compiled with --with-http_gunzip_module --with-http_gzip_static_module flags, which is usually the case) we just have to update our nginx.conf file based on nginx_gzip_module configuration parameters.

gzip on;
gzip_vary on;
gzip_min_length 20;
gzip_types text/plain application/json application/javascript application/x-javascript text/javascript text/css;
gzip_proxied expired no-cache no-store private auth;
gzip_disable "MSIE [1-6]\.";

Let’s test it out if the fallback works with:

curl -I -H "Accept-Encoding: gzip" localhost:3333
Curl Command with Response Headers for gzip

As we can see the response header Content-Encoding value as gzip indicating response type is compressed with gzip. Let’s also check if brotli works when gzip and br headers are sent:

Curl Command with Response Headers for gzip and br

Cleaning up Dockerfile

If you see the Dockerfile, its setup is tied to a specific version of nginx (i.e 1.23.2) and if we want to change the version number we would have to update it in multiple lines in the Dockerfile as well as update the brotli module paths in our nginx.conf files. Let’s try to clean all that up and improve this.

We’ll use Docker ARG to specify the version number of nginx and use it across our Dockerfile. Also note that we need add multiple ARG instruction because of how SCOPE works in Dockerfile after every FROM instruction.

ARG version=1.23.2
FROM nginx:${version}-alpine
ARG version

RUN apk add --update --no-cache git pcre-dev openssl-dev zlib-dev linux-headers build-base \
    && wget http://nginx.org/download/nginx-${version}.tar.gz \
    && tar zxf nginx-${version}.tar.gz \
    && git clone https://github.com/google/ngx_brotli.git \
    && cd ngx_brotli \
    && git submodule update --init --recursive \
    && cd ../nginx-${version} \
    && ./configure --add-dynamic-module=../ngx_brotli --with-compat --with-file-aio --with-threads --with-http_addition_module --with-http_auth_request_module --with-http_dav_module --with-http_flv_module --with-http_gunzip_module --with-http_gzip_static_module --with-http_mp4_module --with-http_random_index_module --with-http_realip_module --with-http_secure_link_module --with-http_slice_module --with-http_ssl_module --with-http_stub_status_module --with-http_sub_module --with-http_v2_module --with-mail --with-mail_ssl_module --with-stream --with-stream_realip_module --with-stream_ssl_module --with-stream_ssl_preread_module --with-cc-opt='-Os -fomit-frame-pointer -g' --with-ld-opt=-Wl,--as-needed,-O1,--sort-common \
    && make modules

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

WORKDIR /usr/share/nginx/html/
COPY www .

Okay now let’s try to clean-up our nginx.conf file and free it from nginx version number. i.e these lines

load_module /root/nginx-1.23.2/objs/ngx_http_brotli_filter_module.so;
load_module /root/nginx-1.23.2/objs/ngx_http_brotli_static_module.so;

We can copy the compiled dynamic modules from our builder image into /usr/lib/nginx/modules directory in a clean nginx image as a new layer and load those in nginx.conf file. Note: I have also updated the Dockerfile ./configure command and added few extra parameters there for this to work properly.

Complete DockerFile:

ARG version=1.23.2
FROM nginx:${version}-alpine AS builder
ARG version

WORKDIR /root/

RUN apk add --update --no-cache git pcre-dev openssl-dev zlib-dev linux-headers build-base \
    && wget http://nginx.org/download/nginx-${version}.tar.gz \
    && tar zxf nginx-${version}.tar.gz \
    && git clone https://github.com/google/ngx_brotli.git \
    && cd ngx_brotli \
    && git submodule update --init --recursive \
    && cd ../nginx-${version} \
    && ./configure \
    --add-dynamic-module=../ngx_brotli \
    --prefix=/etc/nginx \
    --sbin-path=/usr/sbin/nginx \
    --modules-path=/usr/lib/nginx/modules \
    --conf-path=/etc/nginx/nginx.conf \
    --error-log-path=/var/log/nginx/error.log \
    --http-log-path=/var/log/nginx/access.log \
    --pid-path=/var/run/nginx.pid \
    --lock-path=/var/run/nginx.lock \
    --http-client-body-temp-path=/var/cache/nginx/client_temp \
    --http-proxy-temp-path=/var/cache/nginx/proxy_temp \
    --http-fastcgi-temp-path=/var/cache/nginx/fastcgi_temp \
    --http-uwsgi-temp-path=/var/cache/nginx/uwsgi_temp \
    --http-scgi-temp-path=/var/cache/nginx/scgi_temp \
    --with-perl_modules_path=/usr/lib/perl5/vendor_perl \
    --user=nginx \
    --group=nginx \
    --with-compat \
    --with-file-aio \
    --with-threads \
    --with-http_addition_module \
    --with-http_auth_request_module \
    --with-http_dav_module \
    --with-http_flv_module \
    --with-http_gunzip_module \
    --with-http_gzip_static_module \
    --with-http_mp4_module \
    --with-http_random_index_module \
    --with-http_realip_module \
    --with-http_secure_link_module \
    --with-http_slice_module \
    --with-http_ssl_module \
    --with-http_stub_status_module \
    --with-http_sub_module \
    --with-http_v2_module \
    --with-mail \
    --with-mail_ssl_module \
    --with-stream \
    --with-stream_realip_module \
    --with-stream_ssl_module \
    --with-stream_ssl_preread_module \
    --with-cc-opt='-Os -fomit-frame-pointer -g' \
    --with-ld-opt=-Wl,--as-needed,-O1,--sort-common \
    && make modules

FROM nginx:${version}-alpine

ARG version

COPY --from=builder /root/nginx-${version}/objs/ngx_http_brotli_filter_module.so /usr/lib/nginx/modules/
COPY --from=builder /root/nginx-${version}/objs/ngx_http_brotli_static_module.so /usr/lib/nginx/modules/

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

WORKDIR /usr/share/nginx/html/
COPY www .

Complete nginx.conf:

load_module /usr/lib/nginx/modules/ngx_http_brotli_filter_module.so;
load_module /usr/lib/nginx/modules/ngx_http_brotli_static_module.so;

user nginx;

worker_processes 1;

events {
  worker_connections 1024;
}

http {
  include /etc/nginx/mime.types;
  default_type application/octet-stream;

  root /usr/share/nginx/html;

  server {
    listen 80;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    gzip on;
    gzip_vary on;
    gzip_min_length 20;
    gzip_types text/plain application/json application/javascript application/x-javascript text/javascript text/css;
    gzip_proxied expired no-cache no-store private auth;
    gzip_disable "MSIE [1-6]\.";

    # Add Brotli compression config
    brotli on;
    brotli_comp_level 6;
    brotli_static on;
    brotli_types application/atom+xml application/javascript application/json application/rss+xml
             application/vnd.ms-fontobject application/x-font-opentype application/x-font-truetype
             application/x-font-ttf application/x-javascript application/xhtml+xml application/xml
             font/eot font/opentype font/otf font/truetype image/svg+xml image/vnd.microsoft.icon
             image/x-icon image/x-win-bitmap text/css text/javascript text/plain text/xml;
  }

}

Now if we ever want to update our nginx version we just update one ARG version instruction in our Dockerfile

Further Reading / References: