Usability - Productivity - Business - The web - Singapore & Twins

Docker, nginx, SPA and brotli compression

Contemporary web development separates front-end and back-end, resulting in the front-end being a few static files. Besides setting long cache headers, pre-compression is one way to speed up delivery

Setting the stage

  • we have a NodeJS project that outputs our SPA in /usr/dist directory. Highly recommended here: VITE. Works for multi-page applications too.
  • We target only modern browsers that understand brotli (Sorry not IE). Legacy will have to deal with uncompressed files
  • We want to go light on CPU, so we compress at build time, not runtime

Things to know

  • When nginx is configured for brotli and the file index.html gets requested, the file index.html.br gets served if present and the browser indicated (what it does by default) that it can accept br
  • There are tons of information about the need to compile nginx due to the lack of brotli support out of the box. That's not necessary (see below)
  • brotli is both OpenSource and the open standard RFC 7932
  • brotli currently lacks gzip's -r flag, so some bash magic is needed

Moving parts

  • DockerFile
  • nginx configuration

The Dockerfile will handle the brotli generation


# build container using an LTS Node version
# does not get deployed to runtime
FROM node:18-alpine AS builder

# Make sure we ot brotli
RUN apk update
RUN apk add --upgrade brotli

# Create app directory
COPY package*.json ./
COPY src ./src
COPY public ./public
RUN npm install
RUN npm run build
RUN cd /usr/dist && find . -type f -exec brotli -v -Z {} \;

# Actual runtime container
FROM alpine
RUN apk add brotli nginx nginx-mod-http-brotli

#COPY nginx/*.conf /etc/nginx/
COPY nginx/nginx.conf /etc/nginx/nginx.conf
COPY nginx/error404.* /usr/share/nginx/html/
COPY nginx/favicon.* /usr/share/nginx/html/

# Actual data
COPY --from=builder /usr/dist /usr/share/nginx/html/
CMD ["nginx", "-g", "daemon off;"]


# nginx.conf
# Adjusted nginx.conf, serve on site out of
# a container with static brotli, no regex, no dynamic compression
# comments removed check the nginx documentation https://nginx.org/en/docs/
# Error and event logs to console for Docker to capture
user nginx;

worker_processes auto;
pcre_jit off;
error_log /dev/stdout info;
include /etc/nginx/modules/*.conf;
include /etc/nginx/conf.d/*.conf;

events {
    worker_connections 1024;

http {
    access_log /dev/stdout;
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    server_tokens off;
    client_max_body_size 10m;
    sendfile on;
    tcp_nopush on;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_session_cache shared:SSL:2m;
    ssl_session_timeout 1h;
    ssl_session_tickets off;

    gzip off;
    gzip_vary off;
    brotli_static on;

    # Helper variable for proxying websockets.
    map $http_upgrade $connection_upgrade {
        default upgrade;
        '' close;

    log_format main '$remote_addr - $remote_user [$time_local] "$request" '
    '$status $body_bytes_sent "$http_referer" '
    '"$http_user_agent" "$http_x_forwarded_for"';

    server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /usr/share/nginx/html;
        error_page 404 /error404.html;

        location / {
            try_files $uri $uri/ =404;


  • I valiantly fought to get it to work with the official nginx image nginx.alpine, but that didn't work, so I had to fall back to alpine and install nginx and the module from the same repo
  • Compression sizes are impressive. E.g. svg files shrink by 50%
  • source code is available on GitHub


As usual YMMV

Posted by on 24 June 2023 | Comments (0) | categories: Docker nginx WebDevelopment


  1. No comments yet, be the first to comment