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/distdirectory. 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.htmlgets requested, the fileindex.html.brgets 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
-rflag, so some bash magic is needed
Moving parts
- DockerFile
- nginx configuration
The Dockerfile will handle the brotli generation
Dockerfile
# 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
WORKDIR /usr
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;"]
EXPOSE 80
nginx.conf
# 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;
}
}
}
Insights
- 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
Sources:
As usual YMMV
Posted by Stephan H Wissel on 24 June 2023 | Comments (0) | categories: Docker nginx WebDevelopment