How to Compile NGINX with QUIC + HTTP/3 and Brotli on Debian 11
Introduction
Introduction to QUIC + HTTP/3
QUIC is a lower-latency transport-layer protocol, initially developed by Google, that uses multiplexed UDP streams.# The Internet Engineering Task Force (IETF) formalized QUIC as RFC 9000 in May 2021. HTTP/3 is the application-layer protocol that maps HTTP over QUIC, which the IETF formalized as RFC 9114 in June 2022. HTTP/3’s improvements over HTTP/2 include faster TLS handshakes, zero round-trip time connection resumption (0-RTT), and TLS 1.3 encryption by default.# Chrome, Edge, Firefox, and Opera support HTTP/3 by default, and Safari offers support as an experimental feature.#, #, # Synthetic benchmarks aside, HTTP/3 appears to have similar performance to HTTP/2.#
Introduction to Brotli
Brotli is gzip’s successor for HTTP compression. It is a lossless data compression algorithm developed by Google.# Although Brotli uses more CPU resources to compress data than gzip, and marginally more resources to decompress data than gzip,# the resulting smaller transmission payloads often result in faster resource loading.# All major browsers accept Brotli encoding.#
The Problem with NGINX
NGINX is the most popular web server application.# However, its developers have been slow to add support for QUIC + HTTP/3 and Brotli into the mainline distribution. They are developing QUIC + HTTP/3 support in a separate branch, and they have restricted Brotli support to paying NGINX Plus customers. Of the top five most popular web server applications, Cloudflare Server (#3), LiteSpeed (#4), and Microsoft IIS (#5) each support QUIC + HTTP/3;#, #, #, # and NGINX is the only application of the five that doesn’t support Brotli.#, #, #, #
One Solution
One way to add QUIC + HTTP/3 and Brotli support to NGINX is to build NGINX from source, including Cloudflare’s unofficial Quiche patch (which is reportedly more mature than NGINX’s QUIC branch#) and Google’s Brotli module.
Cloudflare and Google’s respective GitHub pages include brief instructions for how to compile NGINX with support for QUIC + HTTP/3 and Brotli. This tutorial’s aim is to provide end-to-end instructions, starting with a minimal Debian 11 (Bullseye) installation and ending with a fully-operational NGINX installation. We’ll compile and install build dependencies; pull the source code for NGINX, Quiche, and Brotli; patch, compile, and install NGINX; configure NGINX; test that NGINX serves QUIC + HTTP/3 and Brotli; and then configure systemd to manage NGINX.
Compiling and Installing NGINX
Prerequisites
I wrote this tutorial based on a minimal installation of Debian 11 (Bullseye). If you’re using a different distribution of Linux, you may need to alter some of the package management commands, pre-compiled dependencies, and build configurations shown below.
You’ll need a system with an active internet connection to download dependencies and source code. This tutorial assumes you’ll be running commands as a non-root user with sudo
privileges. (If you’ll be running commands as root, omit sudo
as applicable.) Lastly, if you already have NGINX installed on your system, you’ll need to remove it before proceeding.
Installing Pre-Compiled Dependencies
Let’s begin by installing pre-compiled binaries from Debian’s repository. We’ll need git
and wget
to download source code. Compiling CMake and NGINX will require GCC, which is available through build-essential
. Compiling CMake will require the OpenSSL developer library, which is available as libssl-dev
. (Normally building NGINX would also require the OpenSSL developer library, but in this case, NGINX will use the BoringSSL library as provided through Quiche.) Lastly, building the NGINX HTTP rewrite module will require the PCRE developer library, which is available as libpcre++-dev
, and the gzip module will require the zlib developer library, which is available as zlib1g-dev
.
sudo apt update
sudo apt install build-essential git libpcre++-dev libssl-dev wget zlib1g-dev -y
Building Rust from Source
Now let’s build our remaining dependencies. Quiche builds using Rust. The Debian repository has an old version of Rust that breaks building NGINX,# so we’ll need to pull and compile Rust from source.
First, let’s make a working directory inside /tmp
for easy cleanup:
mkdir /tmp/rustscript
cd /tmp/rustscript
The installation script for Rust includes a prompt mid-installation. To automate a default installation, let’s run the following:
wget https://sh.rustup.rs -O rust.sh
chmod +x rust.sh
./rust.sh -y
source "$HOME/.cargo/env"
Building CMake from Source
We also need to build CMake, which BoringSSL (the SSL library included with Quiche) uses to build itself.
First, let’s make a working directory inside /tmp
for easy cleanup:
mkdir /tmp/cmakebuild
cd /tmp/cmakebuild
Now let’s pull the latest stable branch, unpack, build, and install CMake:
wget https://github.com/Kitware/CMake/releases/download/v3.22.5/cmake-3.22.5.tar.gz
tar xf cmake-3.22.5.tar.gz
cd /tmp/cmakebuild/cmake-3.22.5
./bootstrap
make
sudo make install
Preparing the NGINX Source Code
Now that we’ve prepared our build environment, let’s pull and modify the NGINX source code.
First, let’s make a working directory inside /tmp
for easy cleanup:
mkdir /tmp/nginxbuild
cd /tmp/nginxbuild
Next let’s pull and unpack NGINX v1.16.1, which Cloudflare’s unofficial Quiche patch is built against.#
wget https://nginx.org/download/nginx-1.16.1.tar.gz
tar xf nginx-1.16.1.tar.gz
Now let’s patch NGINX with Cloudflare’s unofficial Quiche patch:
git clone --recursive https://github.com/cloudflare/quiche
cd /tmp/nginxbuild/nginx-1.16.1
patch -p01 < /tmp/nginxbuild/quiche/nginx/nginx-1.16.patch
Finally, let’s add Google’s Brotli module:
cd /tmp/nginxbuild
git clone --recursive https://github.com/google/ngx_brotli
Building and Installing NGINX
Now that we’ve prepared the source code for NGINX, let’s set its build configurations. (Refer to NGINX’s configure documentation for an explanation of options.)
The configuration options shown below in sections 1–4 mimic those of Debian’s distribution of NGINX, which may be shown by running nginx -V
on a system with a Debian-packaged NGINX installation. Section 5 includes settings for Quiche. Section 6 adds Google’s Brotli module to NGINX. Section 7 contains my own configurations, which add the versions of Quiche and Brotli to the output of nginx -v
and install the NGINX binary to /usr/local/sbin/nginx
, which is in the privileged system $PATH
.
cd /tmp/nginxbuild/nginx-1.16.1
./configure \
\
--with-cc-opt='-g -O2 -fstack-protector-strong -Wformat -Werror=format-security -fPIC -Wdate-time -D_FORTIFY_SOURCE=2' \
--with-ld-opt='-Wl,-z,relro -Wl,-z,now -fPIC' \
\
--prefix=/usr/share/nginx \
--conf-path=/etc/nginx/nginx.conf \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--lock-path=/var/lock/nginx.lock \
--modules-path=/usr/lib/nginx/modules \
--pid-path=/run/nginx.pid \
\
--http-client-body-temp-path=/var/lib/nginx/body \
--http-fastcgi-temp-path=/var/lib/nginx/fastcgi \
--http-proxy-temp-path=/var/lib/nginx/proxy \
--http-scgi-temp-path=/var/lib/nginx/scgi \
--http-uwsgi-temp-path=/var/lib/nginx/uwsgi \
\
--with-compat \
--with-debug \
--with-http_addition_module \
--with-http_auth_request_module \
--with-http_dav_module \
--with-http_gunzip_module \
--with-http_gzip_static_module \
--with-http_realip_module \
--with-http_slice_module \
--with-http_ssl_module \
--with-http_stub_status_module \
--with-http_sub_module \
--with-http_v2_module \
--with-pcre-jit \
--with-threads \
\
--with-http_v3_module \
--with-openssl=/tmp/nginxbuild/quiche/quiche/deps/boringssl \
--with-quiche=/tmp/nginxbuild/quiche \
\
--add-module=/tmp/nginxbuild/ngx_brotli \
\
--build="brotli_$(git --git-dir=/tmp/nginxbuild/ngx_brotli/.git describe --tags) quiche_$(git --git-dir=/tmp/nginxbuild/quiche/.git describe --tags)" \
--sbin-path=/usr/local/sbin/nginx
Now that we’ve set the build configuration, let’s build and install NGINX:
make
sudo make install
Managing Compile Bloat
Evaluating Filesystem Size Before and After Compiling NGINX
Compiling NGINX with QUIC + HTTP/3 and Brotli creates 2.8 GiB of intermediate files, which is likely undesirable, especially in a container environment. Below I’ll trace where the intermediate files are created and then show how to recover file system space.
For simplicity, I’ll display file sizes in terminal outputs using the -h
flag, which displays, for instance, 31G
instead of 31861548 1K-blocks
. The -h
flag displays sizes in base 2, and it always rounds up to the nearest figure shown. In my own descriptions, I’ll calculate values using kibibytes, convert them to the largest applicable KiB/MiB/GiB unit, and then round them to the nearest decimal so to prevent rounding errors.
I benchmarked filesystem size by installing Debian 11.4 on a single ext4 partition without any optional package bundles. I updated the repository cache, installed openssh-server
and sudo
, and configured a non-root user with SSH access and sudo permissions. That placed the filesystem size at 1.3 GiB:
admin@debian:~$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 31G 1.3G 28G 5% /
Here’s how file space was distributed:
admin@debian:~$ sudo du -sh /* 2>&- | grep -E 'K|M|G' | sort -h
4.0K /mnt
4.0K /opt
4.0K /srv
8.0K /media
16K /lost+found
32K /tmp
36K /root
48K /home
572K /run
2.8M /etc
79M /boot
257M /var
946M /usr
After compiling and installing NGINX, the filesystem size became 4 GiB:
admin@debian:~$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 31G 4.1G 25G 15% /
Here’s how file space was distributed after compiling and installing NGINX:
admin@debian:~$ sudo du -sh /* 2>&- | grep -E 'K|M|G' | sort -h
4.0K /mnt
4.0K /opt
4.0K /srv
8.0K /media
16K /lost+found
36K /root
572K /run
3.6M /etc
79M /boot
260M /var
1.2G /tmp
1.3G /home
1.4G /usr
Even though compiling and installing NGINX took an additional 2.8 GiB of file space, only 36 MiB was for NGINX itself:
admin@debian:/$ sudo find / -name nginx -exec du -sh {} \; | grep -v "/tmp" | sort -h
4.0K /var/log/nginx
16K /usr/share/nginx
72K /etc/nginx
36M /usr/local/sbin/nginx
Here’s the distribution of file space for those 2.8 GiB of intermediate files, which I calculated using the above measures as [post-compile file space distribution] - [NGINX installation files] - [pre-compile file space distribution].
764 KiB /etc
3.4 MiB /var
453 MiB /usr
1.1 GiB /tmp
1.2 GiB /home
Cleaning Compile Bloat
We can easily recover the intermediary file space consumed inside /home
and /tmp
. /etc
, /usr
, and /var
contain a mixture of build intermediaries and built NGINX dependencies, so they won’t be as easy to recover file system space from.
First, let’s clean the home directory by removing directories that Rust created, removing the Cargo environment variable from .bashrc
and .profile
, and removing the HSTS file:
cd ~
rm -rf .cargo .rustup
sed -i '$d' .bashrc
sed -i '$d' .profile
rm .wget-hsts
Next, let’s clean the /tmp
directory by removing the directories we created earlier:
cd /tmp
sudo rm -rf cmakebuild nginxbuild rustscript
There isn’t a simple way to revert /etc
, /usr
, and /var
to their original states. The intermediary files created in these directories include a combination of build intermediaries and newly-built NGINX dependencies. (See this changes report made with pkgdiff for a list of every file that changed.) If we wished to keep intermediary files to a minimum, we could package our custom NGINX installation and then install it and the necessary dependencies on a target system. For this tutorial, though, removing the pre-compiled dependencies we originally installed is satisfactory. Doing so frees an additional 101 MiB of space on our file system:
sudo apt purge --auto-remove build-essential git libpcre++-dev libssl-dev wget zlib1g-dev -y
Let’s check our progress:
admin@debian:~$ df -h /
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 31G 1.7G 28G 6% /
All together, we’ve brought the filesystem size back down to 1.6 GiB (remember that -h
always rounds up), or just 356 MiB above the filesystem size before we compiled and installed NGINX.
Serving Hello World with NGINX
Now that we’ve compiled NGINX with support for QUIC + HTTP/3 and Brotli and recovered file system space, let’s test a basic NGINX configuration to make sure everything’s working.
Creating a Needed Directory
NGINX doesn’t create the parent directory for its temporary directories. Let’s create it so NGINX can run:
sudo mkdir /var/lib/nginx/
Creating an SSL Certificate
We’ll need an SSL certificate for serving HTTP/3. Let’s create a self-signed certificate for testing:
sudo openssl ecparam -out /etc/nginx/selfsign.key -name prime256v1 -genkey
sudo openssl req -new -key /etc/nginx/selfsign.key -x509 -sha256 -days 7200 -out /etc/nginx/selfsign.pem
Running a Basic NGINX Configuration
Now let’s replace the stock NGINX configuration with a short configuration file.
First, let’s clear the stock NGINX configuration…
sudo bash -c "cat /dev/null > /etc/nginx/nginx.conf"
… and paste the below configuration into /etc/nginx/nginx.conf
with our text editor of choice running as sudo
:
events {
worker_connections 1024;
}
http {
# Brotli
brotli on;
server {
# QUIC requires 'reuseport' if listening on multiple workers
# declare 'reuseport' on first port use only
listen 443 ssl http2 reuseport;
# QUIC
listen 443 quic;
ssl_certificate selfsign.pem;
ssl_certificate_key selfsign.key;
# QUIC requires TLSv1.3
ssl_protocols TLSv1.2 TLSv1.3;
return 200 '<!DOCTYPE html><title>Hello World</title>Hello World';
default_type text/html;
# HTTP/3
add_header alt-svc 'h3=":443"; ma=86400';
}
}
Then run NGINX:
sudo nginx
Verifying QUIC + HTTP/3
There are three methods we can use to verify that NGINX is serving over QUIC + HTTP/3:
- We can compile a custom
curl
binary with support for QUIC + HTTP/3 on the host system and then run:curl -kI --http3 https://localhost
- We can use an online tool like LiteSpeed’s HTTP/3 Check. (This method requires a publicly reachable hostname mapped to our server.)
- We can use the developer tools inside a browser that supports QUIC + HTTP/3 to verify the connection protocol.
Verifying Brotli
There are three methods we can use to verify that NGINX is serving Brotli encoded responses:
- We can run
curl
from the host system:curl -kIH 'Accept-Encoding: br' https://localhost
- We can use an online tool like KeyCDN Tools Brotli Test. (This method requires a publicly reachable hostname mapped to our server.)
- We can use the developer tools inside a browser that supports Brotli to verify the content encoding.
Managing NGINX with systemd
We can use systemd to manage NGINX on our system. Since we’re not installing NGINX from a package, we’ll need to create the systemd file ourselves.
First, let’s kill the NGINX process we started earlier:
sudo pkill nginx
Creating a Custom systemd File
Next, let’s create our systemd file. Our systemd file will be a copy of the systemd file Debian packages with NGINX with two changes:
- We’ll change the location of the systemd file from
/usr/lib/systemd/system/nginx.service
to/etc/systemd/system/nginx.service
to indicate that we’ve built a custom NGINX installation rather than installed it from a package.# - We’ll change the binary location to match that of our installation.
Let’s paste the below configuration into /etc/systemd/system/nginx.service
with our text editor of choice running as sudo
:
# Stop dance for nginx
# =======================
#
# ExecStop sends SIGSTOP (graceful stop) to the nginx process.
# If, after 5s (--retry QUIT/5) nginx is still running, systemd takes control
# and sends SIGTERM (fast shutdown) to the main process.
# After another 5s (TimeoutStopSec=5), and if nginx is alive, systemd sends
# SIGKILL to all the remaining processes in the process group (KillMode=mixed).
#
# nginx signals reference doc:
# http://nginx.org/en/docs/control.html
#
[Unit]
Description=A high performance web server and a reverse proxy server
Documentation=man:nginx(8)
After=network.target
[Service]
Type=forking
PIDFile=/run/nginx.pid
ExecStartPre=/usr/local/sbin/nginx -t -q -g 'daemon on; master_process on;'
ExecStart=/usr/local/sbin/nginx -g 'daemon on; master_process on;'
ExecReload=/usr/local/sbin/nginx -g 'daemon on; master_process on;' -s reload
ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/nginx.pid
TimeoutStopSec=5
KillMode=mixed
[Install]
WantedBy=multi-user.target
Running NGINX with systemd
Now that we have our systemd file, we need to enable it so NGINX continues running across system restarts:
sudo systemctl enable nginx
Finally, let’s start NGINX and verify that it’s running:
sudo systemctl start nginx
sudo systemctl status nginx
And there we have it! We now have NGINX running, serving HTTP/3 content over QUIC, and encoding content with Brotli.