# Docker Nginx HTTP/3 en Brotli

> Hier vind je een werkende Dockerfile die ik heb gemaakt als basis voor mijn projecten. Dit is een recente Nginx met gecompileerde HTTP/3-ondersteuning en Brotli-ondersteuning voor betere compressie. Ik plaats het hier om het met iedereen te delen.

- URL: https://www.allsubjectsmatter.nl/nl/docker-nginx-http3-and-brotli/
- Language: nl
- Published: 2023-03-20
- Updated: 2024-11-13
- Author: Remco Loup (https://www.allsubjectsmatter.nl/nl/author/remco/)
- Tags: Tech, AI and LM
- Original (en, written by the author): https://www.allsubjectsmatter.nl/docker-nginx-http3-and-brotli/
- Note: this text was translated with AI from the original.

## Update (13-11-2024)

Op de een of andere manier was ik de stap kwijt om een nieuw image aan te maken en daar alle builder-items uit te kopiëren. Het veranderde niets aan de werking van het image, maar de grootte is nu veel kleiner: van ruim 2gb naar onder de 100mb. Ergens tijdens mijn eerdere debuggen is die stap verloren gegaan. De laatste update staat hieronder dus opnieuw.

[Latency Anomaly: An attempt to untangle Azure Performance issues in Western Europe](https://www.allsubjectsmatter.nl/azure-western-europe-zone-3-latency-issue-exploration/): Unveiling a significant Azure performance issue: Zone 3’s unexpected latency anomaly in the Western Europe region challenges cloud deployment strategies

## Introductie

Hieronder vind je een werkende Dockerfile die ik heb gemaakt, met de onderstaande compatibiliteitseisen die ik zelf had. Ik plaats dit hier als eigen archief, en natuurlijk om te delen. Ik heb de volgende ondersteuning toegevoegd.

- HTTP/2 HPACK
- HTTP/3
- BoringSSL
- Brotli-ondersteuning

Deze versie van Nginx ondersteunt HTTP/3 quic en Brotli-compressie als belangrijkste voordelen, plus wat andere fixes. Tegenwoordig geen Brotli-ondersteuning hebben is gewoon een stap terug in de DeLorean.

Meer info over hoe [HPACK](https://blog.cloudflare.com/hpack-the-silent-killer-feature-of-http-2/) je HTTP/2 versnelt. En hier lees je meer details over [BoringSSL](https://blog.ononoki.org/enable-http3-for-nginx/).

## Brotli vergeleken met GZIP

Zoals gezegd is dit een Nginx-versie die Brotli ondersteunt. Brotli is erg nuttig omdat het een beter compressie-algoritme is dan GZIP. Ben je nog niet overtuigd, kijk dan de video hieronder.

[YouTube: How File Compression Increases Your Website Speed: BROTLI vs GZIP Algorithms](https://www.youtube.com/watch?v=BMCvMWqoivs)

## Nginx basis Dockerfile

Dit is de basis van mijn dockerfile die alle bovengenoemde dingen regelt. Je kunt dit natuurlijk aanpassen of andere versies van alpine of Nginx gebruiken, zolang het bouwen van de andere modules maar niet breekt. Ik heb Nginx, NJS en BoringSSL ook naar de nieuwste versies geüpdatet.

Ik gebruik dit image in plaats van de officiële Nginx om al mijn webprojecten te draaien. Zie dit als een basisimage waarin je al je draaiende webserver-projecten definieert. Je krijgt er "gratis" Brotli-compressie en HTTP/3-ondersteuning bij, zodat je je sites sneller en actueler kunt maken.

```docker
##################################################
# Nginx with Quiche (HTTP/3) and Brotli
# VERSION 1.05 / 13-11-2024
##################################################
# This build compiles Nginx with Brotli and
# HTTP/3 support. It uses a multi-stage build to
# minimize the final image size by copying only
# the necessary artifacts from the builder image.
# Based on Alpine for a lean and efficient image.
##################################################

# Builder Stage
FROM alpine:latest AS builder

# Set the versions of Nginx, NJS, and BoringSSL
ENV NGINX_VERSION=1.27.2
ENV NJS_VERSION=0.8.7
ENV BORINGSSL_VERSION=c59bf8bf189dcbde868e04efcd53b705ed155231
ENV BORINGSSL="/tmp/boring-nginx"

# Build-time metadata
ARG BUILD_DATE
ARG VCS_REF

# Install dependencies for building Nginx
RUN addgroup -S nginx \
  && adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx \
  && apk update \
  && apk upgrade \
  && apk add --no-cache ca-certificates openssl \
  && update-ca-certificates \
  && apk add --no-cache --virtual .build-deps \
      gcc libc-dev make pcre-dev zlib-dev linux-headers gnupg \
      libxslt-dev gd-dev geoip-dev perl-dev \
  && apk add --no-cache --virtual .brotli-build-deps \
      autoconf libtool automake git g++ cmake go perl rust cargo patch \
      libxml2-dev byacc flex libstdc++ libmaxminddb-dev lmdb-dev file openrc

# Install and build BoringSSL
RUN mkdir $BORINGSSL \
  && cd $BORINGSSL \
  && git clone https://boringssl.googlesource.com/boringssl \
  && cd boringssl \
  && git checkout -q ${BORINGSSL_VERSION} \
  && mkdir build \
  && cd build \
  && cmake -DBUILD_SHARED_LIBS=1 .. \
  && make

# Prepare BoringSSL for Nginx compilation
RUN mkdir -p "$BORINGSSL/boringssl/.openssl/lib" \
  && cd "$BORINGSSL/boringssl/.openssl" \
  && ln -s ../include include \
  && cd "$BORINGSSL/boringssl" \
  && cp "build/crypto/libcrypto.so" ".openssl/lib" \
  && cp "build/ssl/libssl.so" ".openssl/lib" \
  && cp ".openssl/lib/libssl.so" /usr/lib/ \
  && cp ".openssl/lib/libcrypto.so" /usr/lib/

# Clone Nginx modules
RUN mkdir /usr/src \
  && cd /usr/src \
  && git clone --depth=1 --recursive --shallow-submodules https://github.com/google/ngx_brotli \
  && git clone --branch $NJS_VERSION --depth=1 --recursive --shallow-submodules https://github.com/nginx/njs

# Build Nginx with modules
RUN cd /usr/src \
  && wget -qO nginx.tar.gz https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz \
  && tar -zxC /usr/src -f nginx.tar.gz \
  && rm nginx.tar.gz \
  && cd /usr/src/nginx-$NGINX_VERSION \
  && mkdir /root/.cargo \
  && echo $'[net]\ngit-fetch-with-cli = true' > /root/.cargo/config.toml \
  && ./configure --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 \
      --user=nginx \
      --group=nginx \
      --with-pcre-jit \
      --with-http_ssl_module \
      --with-http_realip_module \
      --with-http_addition_module \
      --with-http_sub_module \
      --with-http_dav_module \
      --with-http_flv_module \
      --with-http_mp4_module \
      --with-http_gunzip_module \
      --with-http_gzip_static_module \
      --with-http_random_index_module \
      --with-http_secure_link_module \
      --with-http_stub_status_module \
      --with-http_auth_request_module \
      --with-http_xslt_module=dynamic \
      --with-http_image_filter_module=dynamic \
      --with-http_geoip_module=dynamic \
      --with-http_perl_module=dynamic \
      --with-threads \
      --with-stream \
      --with-stream_ssl_module \
      --with-stream_ssl_preread_module \
      --with-stream_realip_module \
      --with-stream_geoip_module=dynamic \
      --with-http_slice_module \
      --with-mail \
      --with-mail_ssl_module \
      --with-compat \
      --with-file-aio \
      --with-http_v2_module \
      --with-http_v3_module \
      --add-module=/usr/src/ngx_brotli \
      --add-module=/usr/src/njs/nginx \
      --with-cc-opt="-I$BORINGSSL/boringssl/include -Wno-error" \
      --with-ld-opt="-L$BORINGSSL/boringssl/build/ssl -L$BORINGSSL/boringssl/build/crypto" \
      --with-select_module \
      --with-poll_module \
      --build="docker-nginx-http3-$VCS_REF-$BUILD_DATE ngx_brotli-$(git --git-dir=/usr/src/ngx_brotli/.git rev-parse --short HEAD) njs-$(git --git-dir=/usr/src/njs/.git rev-parse --short HEAD)" \
  && touch "$BORINGSSL/boringssl/.openssl/include/openssl/ssl.h" \
  && make -j$(getconf _NPROCESSORS_ONLN) \
  && make install \
  && rm -rf /etc/nginx/html/ \
  && mkdir /etc/nginx/conf.d/ \
  && mkdir -p /usr/share/nginx/html/ \
  && install -m644 html/index.html /usr/share/nginx/html/ \
  && install -m644 html/50x.html /usr/share/nginx/html/ \
  && ln -s /usr/lib/nginx/modules /etc/nginx/modules \
  && strip /usr/sbin/nginx* \
  && strip /usr/lib/nginx/modules/*.so \
  && rm -rf /etc/nginx/*.default /etc/nginx/*.so \
  && rm -rf /usr/src \
  && apk add --no-cache --virtual .gettext gettext \
  && mv /usr/bin/envsubst /tmp/ \
  \
  # Determine runtime dependencies
  && runDeps="$( \
      scanelf --needed --nobanner /usr/sbin/nginx /usr/lib/nginx/modules/*.so /tmp/envsubst \
      | awk '{ gsub(/,/, "\nso:", $2); print "so:" $2 }' \
      | sort -u \
      | xargs -r apk info --installed \
      | sort -u \
    )" \
  && echo "$runDeps" > /nginx-run-deps.txt \
  \
  # Cleanup
  && apk del .brotli-build-deps .build-deps .gettext \
  && rm -rf /root/.cargo \
  && rm -rf /var/cache/apk/* \
  && mv /tmp/envsubst /usr/local/bin/ \
  && mkdir -p /etc/ssl/private \
  && openssl req -x509 -newkey rsa:4096 -nodes -keyout /etc/ssl/private/localhost.key -out /etc/ssl/localhost.pem -days 365 -sha256 -subj '/CN=localhost' \
  && ln -sf /dev/stdout /var/log/nginx/access.log \
  && ln -sf /dev/stderr /var/log/nginx/error.log

# Final Stage
FROM alpine:latest

# Copy Nginx and related files from the builder stage
COPY --from=builder /usr/sbin/nginx /usr/sbin/nginx
COPY --from=builder /etc/nginx /etc/nginx
COPY --from=builder /usr/lib/nginx /usr/lib/nginx
COPY --from=builder /usr/share/nginx /usr/share/nginx
COPY --from=builder /usr/local/bin/envsubst /usr/local/bin/envsubst
COPY --from=builder /var/log/nginx /var/log/nginx
COPY --from=builder /var/cache/nginx /var/cache/nginx
COPY --from=builder /var/run /var/run
COPY --from=builder /etc/ssl /etc/ssl
COPY --from=builder /usr/lib/libssl.so* /usr/lib/
COPY --from=builder /usr/lib/libcrypto.so* /usr/lib/
COPY --from=builder /nginx-run-deps.txt /nginx-run-deps.txt

# Install runtime dependencies
RUN apk add --no-cache ca-certificates openssl libstdc++ libgcc \
  && apk add --no-cache $(cat /nginx-run-deps.txt) \
  && rm /nginx-run-deps.txt \
  && update-ca-certificates \
  \
  # Forward logs to Docker
  && ln -sf /dev/stdout /var/log/nginx/access.log \
  && ln -sf /dev/stderr /var/log/nginx/error.log

# Create nginx user and group
RUN addgroup -S nginx \
  && adduser -D -S -h /var/cache/nginx -s /sbin/nologin -G nginx nginx

# Expose ports
EXPOSE 80 443

STOPSIGNAL SIGTERM

CMD ["nginx", "-g", "daemon off;"]
```

## Nginx.conf-voorbeeld

Hier vind je een voorbeeld van een nginx.conf die je een idee geeft hoe ik mijn nginx-omgeving draai.

Het belangrijkste verschil in dit bestand ten opzichte van de normale nginx.conf zijn de brotli-compressie-instellingen.

```nginx
user nginx;
worker_processes auto;

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

include /etc/nginx/modules-enabled/*.conf;

events {
	worker_connections 1024;
}

http {
	##
	# Basic Settings
	##
	sendfile on;
	tcp_nopush on;
	tcp_nodelay on;
	keepalive_timeout 65;
	types_hash_max_size 2048;

	proxy_connect_timeout  300s;
	proxy_send_timeout  300s;
	proxy_read_timeout  300s;
	fastcgi_send_timeout 300s;
	fastcgi_read_timeout 300s;

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

	##
	# SSL Settings
	##
	ssl_protocols TLSv1.2 TLSv1.3;
	ssl_prefer_server_ciphers on;

	##
	# Logging Settings
	##
    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;

	##
	# Gzip Settings
	##
	gzip on;
	gzip_vary on;
	gzip_proxied any;
	gzip_comp_level 6;
	gzip_buffers 16 8k;
	gzip_http_version 1.1;
	gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

	##
	# Brotli Settings (modify the brotli_comp_level to match your optimal cpu load vs compression)
	##
	brotli on;
	brotli_static on;
	brotli_comp_level 11;
	brotli_types
	text/plain
	text/css
	text/xml
	text/javascript
	text/x-component
	application/xml
	application/xml+rss
	application/javascript
	application/json
	application/atom+xml
	application/vnd.ms-fontobject
	application/x-font-ttf
	application/x-font-opentype
	application/x-font-truetype
	application/x-web-app-manifest+json
	application/xhtml+xml
	application/octet-stream
	font/opentype
	font/truetype
	font/eot
	font/otf
	font/woff
	font/woff2
	image/svg+xml
	image/x-icon
	image/vnd.microsoft.icon
	image/bmp;

	##
	# Virtual Host Configs
	##
	include /etc/nginx/conf.d/*.conf;
}
```

## Site.conf-voorbeeld

Let op: hoe hoger je de brotli-compressie instelt, hoe cpu-intensiever het wordt. TTFB (time to first byte) kan een probleem worden als de compressieratio te hoog staat in combinatie met een trage kubernetes-omgeving, hoge loads, of alles daartussenin. Test dus met realistische scenario's en pas het daarop aan. Dit is niet alleen een Brotli-probleem; Gzip heeft dezelfde nadelen als je de compressieniveaus te hoog zet, terwijl de afname in bestandsgrootte het niet waard is.

Je kunt dit bestand gebruiken om alle HTTP/3-instellingen hieronder over te nemen. Dit optimaliseert en activeert HTTP/3-ondersteuning voor je site.

```nginx
server {
       	# HTTP/3 and Quic Listen
      	listen 443 quic reuseport;
        http3 on;

        # HTTP/3 Add Alt-Svc header negotiation.
        add_header alt-svc 'h2=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400' always;
        
        # HTTP/2 Listen
        listen 443 ssl http2;
        http2 on;

        # Certificate locations for HTTPS.
	    ssl_certificate /etc/ssl/certs/wildcard-cert-23.crt;
        ssl_certificate_key /etc/ssl/private/wildcard-cert-23.key;

        # Enable SSL settings for performance.
        ssl_session_cache shared:SSL:50m;
        ssl_session_timeout 1d;
        ssl_session_tickets off;
        # Enable SSL settings for security.
        ssl_prefer_server_ciphers on;
        ssl_protocols TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;

        # Base settings (root and hostname).
        root /var/www/html;
        index index.php index.html index.htm;
        server_name kubernetes.servername.nl;

        # HTTP/2 settings
        http2_push_preload on;
        # /End of HTTP/2.

        # HTTP/3 settings
        # Enable TLSv1.3's 0-RTT. Use $ssl_early_data when reverse proxying to
        # prevent replay attacks.
        # @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
        ssl_early_data on;
        quic_retry on;
        quic_gso on;
        quic_host_key /etc/ssl/private/wildcard-cert-23.key;
        # /End of HTTP/3.

        location / {
                try_files $uri $uri/ /index.php?$query_string;
	    }

        location ~ \.php$ {
                try_files $uri =404;
                # Bunch of PHP and FPM settings
                fastcgi_split_path_info ^(.+\.php)(/.+)$;
                fastcgi_pass your-php-fpm-instance:9000;
                fastcgi_index index.php;
                include fastcgi_params;
                fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
                fastcgi_param PATH_INFO $fastcgi_path_info
                
                # Add a bunch of other headers based on your desired settings.
                add_header X-protocol $server_protocol always;   
                add_header X-Cache $upstream_cache_status always;
                add_header X-Frame-Options SAMEORIGIN always;
                add_header X-Content-Type-Options nosniff always;
                add_header X-XSS-Protection "1; mode=block";
                add_header Strict-Transport-Security "max-age=31536000; includeSubdomains; preload" always;
                add_header Referrer-Policy strict-origin-when-cross-origin;
                add_header Pragma public;
                add_header Cache-Control "public";
        }
}
```

## Meerdere H3-verbindingen vanaf één Nginx

Hier leg ik uit hoe je meerdere server blocks binnen Nginx met HTTP/3 kunt laten werken. Er zitten wat haken en ogen aan, omdat http3 UDP gebruikt met de instelling reuseport, waardoor je poort 443 niet meerdere keren kunt toewijzen bij gebruik van HTTP/3.

```nginx
add_header alt-svc 'h2=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400' always;
```

Dit deel van de header meldt dat onze HTTP/3-verbinding poort 443 accepteert. Heb je een enkele nginx-container met meerdere geconfigureerde server blocks, dan krijg je problemen met het hergebruik van poort 443.

Met HTTP/3 kun je een poort maar één keer definiëren binnen een server block. Heb je meerdere server blocks, dan kun je je website via andere poorten dan 443 laten communiceren met behulp van deze alt-svc-header, die de browser vertelt dat hij HTTP/3 accepteert en op welke poort.

```nginx
add_header alt-svc 'h2=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400' always;

add_header alt-svc 'h2=":443"; ma=86400, h3-29=":444"; ma=86400, h3=":444"; ma=86400' always;
```

Let op de verschillen in de h3-poorten hier. Zo kun je meerdere server blocks h3 laten gebruiken door unieke poorten op te geven.

Het enige wat je hoeft te doen is je instance naar de juiste poort laten luisteren die in je alt-svc-header staat. Als er een succesvolle verbinding mogelijk is, zal de browser proberen over te stappen op h3. Hieronder een voorbeeld van hoe je kunt luisteren op poort 443 en 444 met HTTP/3.

```nginx
server {

        server_name http3-port-443

       	# HTTP/3 and Quic Listen
      	listen 443 quic reuseport;
        http3 on;

        # HTTP/3 Add Alt-Svc header negotiation.
        add_header alt-svc 'h2=":443"; ma=86400, h3-29=":443"; ma=86400, h3=":443"; ma=86400' always;
        
        # HTTP/2 Listen
        listen 443 ssl http2;
        http2 on;
        
        # HTTP/3 settings
        # Enable TLSv1.3's 0-RTT. Use $ssl_early_data when reverse proxying to
        # prevent replay attacks.
        # @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
        ssl_early_data on;
        quic_retry on;
        quic_gso on;
        quic_host_key /etc/ssl/private/wildcard-cert-23.key;
        # /End of HTTP/3.

        location / {
                try_files $uri $uri/ /index.php?$query_string;
	}
}
```

```nginx
server {

        server_name http3-port-444

       	# HTTP/3 and Quic Listen
      	listen 444 quic reuseport;
        http3 on;

        # HTTP/3 Add Alt-Svc header negotiation.
        add_header alt-svc 'h2=":443"; ma=86400, h3-29=":444"; ma=86400, h3=":444"; ma=86400' always;
        
        # HTTP/2 Listen
        listen 443 ssl http2;
        http2 on;
        
        # HTTP/3 settings
        # Enable TLSv1.3's 0-RTT. Use $ssl_early_data when reverse proxying to
        # prevent replay attacks.
        # @see: http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_early_data
        ssl_early_data on;
        quic_retry on;
        quic_gso on;
        quic_host_key /etc/ssl/private/wildcard-cert-23.key;
        # /End of HTTP/3.

        location / {
                try_files $uri $uri/ /index.php?$query_string;
	}
}
```

Zo eenvoudig is het om je reverse proxy of meerdere server blocks een H3-verbinding te laten opzetten. Je moet wel de juiste port forwards instellen in je firewall/netwerk.

Met docker configureer je dit zoals hieronder weergegeven. Bij het opgeven van UDP moet je in docker elke poort apart specificeren. Hieronder mijn voorbeeld met UDP-poorten 443 tot en met 449.

```yaml
services:
  ############################################################
  # proxy webservice to connect to multiple nginx instances
  ############################################################
  reverse-proxy:
    image: custom-build-nginx:latest
    restart: unless-stopped
    ports:
      - '80:80'
      - '443:443'
      - '443:443/udp'
      - '444:444/udp'
      - '445:445/udp'
      - '446:446/udp'
      - '447:447/udp'
      - '448:448/udp'
      - '449:449/udp'
    volumes:
```

## Update (03-04-2024)

Wat versienummers toegevoegd aan mijn dockerfile om versies beter te kunnen vergelijken. Ik heb de build van gisteren ook opgeschoond en besloten om een ENV toe te voegen voor het instellen van de boringssl-versie. Omdat hun build mijn dockerfile kapotmaakte, updaten we die maar handmatig. Verder heb ik nginx geüpdatet naar 1.25.4 en NJS naar 0.8.3 en de build geverifieerd.

## Update (02-04-2024)

Pas recent kwam ik erachter dat een update van BoringSSL van rond 28 februari mijn build van BoringSSL had gebroken. Hieronder de bijgewerkte dockerfile die weer compileert. Ik moest libssl als shared library bouwen om het weer te laten werken. Ik ga dit image nog wat verder opruimen, maar het werkt in elk geval weer.

## Update (18-03-2024)

Ik heb een sectie toegevoegd die uitlegt hoe je meerdere H3-verbindingen kunt gebruiken als je een Nginx hebt met meer dan één server block. Dit is ook de sleutel als je Nginx als Reverse Proxy gebruikt en gebruik wilt maken van H3.

## Update (04-09-2023)

Ik kwam erachter dat deze build niet meer compileerde door een verandering in de brotli-broncode? Het had misschien te maken met updates van GitHub die ik niet had vastgelegd in deze Dockerfile. Ik heb het bestand volledig omgebouwd zodat het werkt met de bijgewerkte Nginx 1.25.2, die HTTP/3-ondersteuning "ingebouwd" heeft. Ook heb ik ervoor gezorgd dat we weer native Brotli-ondersteuning hebben. Dit zou vanaf nu moeten blijven werken.

## Update (30-08-2023)

Ik heb de modsec-ondersteuning die er eerst in zat verwijderd. Die gaf willekeurige errors en omdat ik hem toch niet gebruik, heb ik hem eruit gehaald. Misschien komt hij later terug.
