Add Brotli and Gzip compression to nginx in Docker
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:
- Update the packages, install git and other dependencies
- Download the nginx package (same version as our docker image) and unzip it in the current working directory
- Clone the ngx_brotli repository from github and run update submodule to fetch all data from the project
- cd back into nginx package that we downloaded and run
configure
script with:- Default Arguments (
nginx -V
to get the default compile arguments) - Add Dynamic Module argument pointing to our downloaded ngx_brotli
repository (
../ngx_brotli
)
- Default Arguments (
- Compile the dynamic modules
using
make modules
which can be loaded usingload_module
in ournginx.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
Neat! We get the response in brotli format:
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
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:
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: