Raja's Exocortex

Nginx Static File Serving

Serving static files efficiently using:

  1. Nginx.
  2. File compression using gzip, brotli.
  3. Browser caching using Last-Modified response, If-Modified-Since conditional requests.

Files are not dynamically compressed at run time as this will increase latency and CPU utilization. Instead, files need to be precompressed (as part of CI/CD pipeline or during deployment) and the compressed versions will be served based on browser support as indicated in the Accept-Encoding request header.

Eg: index.html, index.html.br and index.html.gz are available and when the browser requests index.html, nginx will automatically server the index.html.br or index.html.gz based on browser support. This is highly efficient and usually consumes less than ~1% of CPU.

Step 1: Nginx Custom Docker Container

The official nginx docker container does not support brotli, a custom container needs to be built based on alpine.

File: Dockerfile

# Create a custom docker container including the nginx brotli module
FROM alpine:3.19
WORKDIR /root
RUN apk add --no-cache nginx nginx-mod-http-brotli

EXPOSE 80/tcp
CMD ["nginx", "-g", "daemon off;"]

File: docker-compose.yaml

# Docker compose to server static files with nginx with brotli/gzip
# Filename: docker-compose.yaml

version: "2.4"

name: static-example-com

services:
  static:
    container_name: static.example.com
    build: .
    restart: always

    volumes:
      - ./www:/usr/share/nginx/html:ro
      - ./conf/default.conf:/etc/nginx/conf.d/default.conf:ro
      - ./conf/nginx.conf:/etc/nginx/nginx.conf:ro

    labels:
      - com.centurylinklabs.watchtower.enable=true
      - traefik.enable=true
      - traefik.http.routers.static.rule=Host(`static.example.com`)
      - traefik.http.routers.static.tls=true
      - traefik.http.routers.static.tls.certresolver=lets-encrypt

Step 2: nginx Config

File: nginx.conf

# Default nginx.conf from nginx official docker container
# Adding brotli support

user  nginx;
worker_processes  auto;

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

# Add brotli static, gzip static is default
load_module modules/ngx_http_brotli_static_module.so;

events {
    worker_connections  1024;
}


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

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

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

File: default.conf

# Nginx virtual host config fragment
# Filename: /etc/nginx/conf.d/default.conf
server {
  listen       80;

  # Enable brotli/gzip support
  brotli_static on;
  gzip_static   on;

  location / {
    root   /usr/share/nginx/html;
    index  index.html index.htm;
    autoindex on;

    # Permit the browser to cache files for 1 hour
    # Browser must issue a conditional GET request with the If-Modified-Since header
    # to avoid using stale content

    add_header Cache-Control "max-age=3600, public, no-transform, must-revalidate, proxy-revalidate";
#   add_header Cache-Control "max-age=3600, public, no-transform";
  }
}

Step 3: Prepare Files

Generate compressed files using brotli and gzip and keep the originals.

# Use max compression for files, keep (don't delete) original uncompressed file
brotli -9vk *.html *.js *.json *.css *.csv *.txt
gzip   -9vk *.html *.js *.json *.css *.csv *.txt

Step 4: Verification

Use browser dev tools to ensure the compressed files are automatically loaded instead of original versions.

Notes

  1. The Accept-Encoding request header will include gzip, deflate, br to list the browser supported compression options.
  2. The Content-Encoding response header must show br to indicate that compressed content is transferred on the wire.
  3. When both br and gz encoding are supported, br is preferred over gz.