Getting NGINX HTTP/3 on Ubuntu to actually work is harder than the nginx docs make it look. You enable http3 on; in your NGINX config, restart, and Chrome still negotiates HTTP/2. Or worse: HTTP/3 appears to work, but connections silently die after every nginx -s reload. Both problems trace back to how stock Debian/Ubuntu nginx is built, and both are solved by a single APT package.
Stock nginx from the Debian and Ubuntu archives ships without --with-http_v3_module. The ondrej/nginx PPA fares no better on current LTS releases. Even when someone manages to self-build HTTP/3 support, they hit a second, nastier bug: upstream NGINX 1.25.0 and newer silently drops about half of new QUIC connections after nginx -s reload when quic_bpf is on. F5 has sat on the fix for over a year.
The GetPageSpeed nginx package in our APT repository solves both: it compiles --with-http_v3_module on every supported codename (Ubuntu 20.04 through 24.04, Debian 12, Debian 13) using nginx’s built-in OpenSSL QUIC compatibility layer, carries our QUIC reuseport reload patch, ships a UFW profile that opens UDP 443, and generates a persistent QUIC host key on first install. It’s a drop-in replacement for the distro nginx, nginx-core, nginx-full, nginx-extras, and nginx-light packages, and the fastest path to NGINX HTTP/3 on Ubuntu or Debian that doesn’t involve hand-compiling.
What Is HTTP/3 and Why Use It?
HTTP/3 is the third major version of the Hypertext Transfer Protocol. Unlike HTTP/1.1 and HTTP/2 which ride on TCP, HTTP/3 uses QUIC, a transport protocol built on UDP. QUIC eliminates head-of-line blocking, supports 0-RTT connection resumption, and migrates connections across network changes without dropping them. On mobile networks the difference is immediately visible: a user walking from Wi-Fi to cellular no longer loses their session.
For a deeper architectural dive covering the RHEL/Rocky/AlmaLinux install path, see the companion guide: How to Install NGINX QUIC on CentOS, RHEL, Rocky Linux, AlmaLinux, and Fedora.
Why Stock Ubuntu and Debian NGINX Falls Short
Two things stand between a typical Ubuntu or Debian admin and a working NGINX HTTP/3 on Ubuntu deployment:
- The binary has no
--with-http_v3_module. Ifnginx -V 2>&1 | grep http_v3returns nothing, no amount of configuration will turn HTTP/3 on. Debian, Ubuntu archives, andondrej/nginxdo not ship this flag on their current LTS builds. - The QUIC reuseport reload bug. Even when HTTP/3 is compiled in, upstream NGINX leaks reuseport sockets from exiting workers during a graceful reload, intercepting new QUIC Initial packets and dropping them. This is the
nginx -s reloadhazard documented in NGINX HTTP/3 Is Broken After Reload.
A common myth says you also need a QUIC-capable OpenSSL (3.2+ or quictls) just to build HTTP/3. You don’t. NGINX 1.25.2 and newer ship an OpenSSL QUIC compatibility layer that simulates the QUIC handshake on top of plain OpenSSL 1.1.1+ using TLS custom extensions. The --with-http_v3_module configure step probes three tiers: OpenSSL 3.5.1+ native QUIC, BoringSSL/quictls/LibreSSL, and finally the built-in compat layer. Every Ubuntu and Debian codename we target satisfies at least the third tier.
NGINX HTTP/3 Packages by GetPageSpeed (APT)
The GetPageSpeed nginx APT package addresses every point above and is the recommended route to NGINX HTTP/3 on Ubuntu and Debian today:
- HTTP/3 compiled in on every codename we build for. Ubuntu 20.04 focal, 22.04 jammy, 24.04 noble, Debian 12 bookworm, and Debian 13 trixie all ship with
--with-http_v3_module. Runnginx -V 2>&1 | tr ' ' '\n' | grep http_v3after install and you’ll see it on every one of them. - QUIC reuseport reload fix baked in. Our
quic_bpf_reload_fix.patchcloses reuseport sockets during graceful shutdown, removing exiting workers from the kernel reuseport group so new QUIC Initial packets never land on a dying worker. - Persistent QUIC host key. The postinst generates
/etc/nginx/quic.keyon first install (32 random bytes from/dev/urandom,0640 root:www-data). QUIC address tokens survive reloads and restarts. - UFW profile
Nginx QUICships pre-installed at/etc/ufw/applications.d/nginxand opens UDP 443 in one command. - Drop-in replacement via Breaks/Replaces on
nginx-core,nginx-full,nginx-common,nginx-extras, andnginx-light. Migrate from ondrej or the distro nginx with a singleapt install nginx.
Distribution Support Matrix
| Distribution | Codename | System OpenSSL | HTTP/3 in our package |
|---|---|---|---|
| Ubuntu 20.04 LTS | focal | 1.1.1 | Yes (compat layer) |
| Ubuntu 22.04 LTS | jammy | 3.0.2 | Yes (compat layer) |
| Ubuntu 24.04 LTS | noble | 3.0.13 | Yes (compat layer) |
| Debian 12 | bookworm | 3.0.x | Yes (compat layer) |
| Debian 13 | trixie | 3.5.x | Yes (native QUIC API) |
0-RTT Support: Coming via quictls
There’s one HTTP/3 feature the compat layer cannot deliver: 0-RTT connection resumption. NGINX’s ssl_early_data on; requires a QUIC-aware OpenSSL backend, either OpenSSL 3.5.1+ natively or a BoringSSL-like library such as quictls. On current-stack Ubuntu LTS releases (system OpenSSL 1.1.1, 3.0.2, or 3.0.13) that means 0-RTT is off even after our package is installed. Regular HTTP/3 still works exactly as documented here; only the zero-round-trip resumption path is unavailable.
We already ship a quictls package in our APT repository for every codename above: quictls, quictls-libs, quictls-dev, and quictls-static. It’s a co-installable fork of OpenSSL 3.1.x with QUIC APIs, layered at /usr/lib/<multiarch>/quictls/ so your system libssl3 is never touched. Rebuilding the GetPageSpeed nginx package against quictls on Ubuntu codenames is on the roadmap and will unlock 0-RTT out of the box.
Want 0-RTT NGINX HTTP/3 on Ubuntu 24.04, 22.04, 20.04, or Debian 12? Tell us. The more users voice demand, the sooner we prioritize the quictls-linked rebuild. Reach out via our contact form or email info@getpagespeed.com and mention which codename you need. It directly influences the build queue.
Install NGINX with HTTP/3 on Ubuntu or Debian
All commands below run as root or via sudo. The identical procedure works on focal, jammy, noble, bookworm, and trixie.
Step 1: Install the GetPageSpeed Keyring
sudo install -d -m 0755 /etc/apt/keyrings
curl -fsSL https://extras.getpagespeed.com/deb-archive-keyring.gpg \
| sudo tee /etc/apt/keyrings/getpagespeed.gpg >/dev/null
Step 2: Add the APT Source
distro=$(lsb_release -is | tr '[:upper:]' '[:lower:]')
codename=$(lsb_release -cs)
echo "deb [signed-by=/etc/apt/keyrings/getpagespeed.gpg] https://extras.getpagespeed.com/${distro} ${codename} main" \
| sudo tee /etc/apt/sources.list.d/getpagespeed-extras.list
For the nginx mainline branch, add a second line:
echo "deb [signed-by=/etc/apt/keyrings/getpagespeed.gpg] https://extras.getpagespeed.com/${distro} ${codename}-mainline main" \
| sudo tee -a /etc/apt/sources.list.d/getpagespeed-extras.list
Step 3: Pin Our nginx Over Distro and Ondrej
This is critical. Without the pin, apt may prefer a higher-versioned stock nginx over ours:
sudo tee /etc/apt/preferences.d/getpagespeed-nginx.pref > /dev/null <<'EOF'
Package: nginx nginx-common nginx-core nginx-full nginx-extras nginx-light nginx-module-* libnginx-mod-*
Pin: origin extras.getpagespeed.com
Pin-Priority: 1001
EOF
Step 4: Install
sudo apt-get update
sudo apt-get install nginx
Verify HTTP/3 Is Compiled In
The single authoritative check:
nginx -V 2>&1 | tr ' ' '\n' | grep http_v3
Expected output on every supported codename:
--with-http_v3_module
Also confirm the package source and version:
dpkg -s nginx | grep -E 'Maintainer|Version'
You should see Maintainer: GetPageSpeed LLC <builder@getpagespeed.com> and a version string like 1:1.28.3-11~gps1+ubuntu2404+stable (noble), +ubuntu2204+stable (jammy), +ubuntu2004+stable (focal), +deb12+stable (bookworm), or +deb13+stable (trixie).
If http_v3_module is absent, the pin isn’t taking effect and apt installed the distro nginx. Re-check with apt-cache policy nginx.
Minimal HTTP/3 Server Block
Drop this into /etc/nginx/conf.d/h3-example.conf and adjust the hostname and certificate paths:
server {
listen 443 ssl; # TCP listener for HTTP/1.1 and HTTP/2
listen 443 quic reuseport; # UDP listener for QUIC and HTTP/3
listen [::]:443 ssl;
listen [::]:443 quic reuseport;
server_name example.com;
ssl_protocols TLSv1.3; # QUIC requires TLS 1.3
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
http2 on;
http3 on;
add_header Alt-Svc 'h3=":443"; ma=86400';
root /var/www/html;
index index.html;
}
Reload:
sudo nginx -t && sudo systemctl reload nginx
Multiple Virtual Hosts: reuseport Goes on One Server Block
listen 443 quic reuseport must appear exactly once per address family. Put it on your first (or default_server) block and use plain listen 443 quic; on the rest:
server {
listen 443 ssl default_server;
listen 443 quic reuseport default_server;
server_name example.com;
# ...
}
server {
listen 443 ssl;
listen 443 quic;
server_name example.org;
# ...
}
System Tuning: UDP Buffer Sizes
QUIC throughput is bottlenecked by Linux’s default UDP socket buffers. Raise them by writing /etc/sysctl.d/99-quic.conf:
# Increase UDP buffers for QUIC (HTTP/3) performance
net.core.rmem_max = 7500000
net.core.wmem_max = 7500000
net.core.rmem_default = 7500000
net.core.wmem_default = 7500000
Apply immediately:
sudo sysctl -p /etc/sysctl.d/99-quic.conf
Kernel-Assisted QUIC: quic_bpf and quic_gso
NGINX exposes two kernel-offload directives. Add them at the top of /etc/nginx/nginx.conf (main context) or http context respectively:
# Main context, before events {}
quic_bpf on;
http {
quic_gso on;
quic_host_key /etc/nginx/quic.key;
# ...
}
quic_bpf on attaches a BPF program that steers QUIC packets to the correct worker by Destination Connection ID. Requires kernel 5.7+ and CAP_SYS_ADMIN on the nginx master process. Every supported codename’s stock kernel satisfies both. quic_gso on enables UDP segmentation offload, reducing per-packet syscall overhead. Requires kernel 4.18+.
This is where the reload fix earns its keep. quic_bpf on is exactly the combination that exposes the upstream reuseport reload bug. With the stock nginx, reloading a busy HTTP/3 server under quic_bpf drops roughly half of new connections for several seconds. With the GetPageSpeed nginx package, nginx -s reload is transparent to clients: the exiting worker removes itself from the reuseport group before it stops accepting. We verified this end-to-end on Ubuntu 24.04 noble (system OpenSSL 3.0.13, nginx built via the OpenSSL compat layer): 298 consecutive curl --http3 requests issued during three back-to-back nginx -s reload calls under quic_bpf on, and all 298 returned HTTP/3 200 with zero drops.
Persistent QUIC Host Key
Without quic_host_key, NGINX generates a random key at startup. Every restart invalidates all address validation tokens issued by the previous process, forcing clients through an extra round-trip. The GetPageSpeed postinst creates /etc/nginx/quic.key on first install:
ls -l /etc/nginx/quic.key
# -rw-r----- 1 root www-data 32 ...
Reference it from the http context (shown in the snippet above) and address validation tokens survive reloads and restarts.
Firewall: UFW
Our package ships a Nginx QUIC UFW profile that opens TCP 80, TCP 443, and UDP 443 in one rule:
sudo ufw app list | grep Nginx
# Nginx Full
# Nginx HTTP
# Nginx HTTPS
# Nginx QUIC
sudo ufw allow 'Nginx QUIC'
sudo ufw reload
If you prefer bare ports:
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
UDP 443 is the line most Ubuntu admins forget when rolling out NGINX HTTP/3 on Ubuntu. Without it, browsers never get past the initial h2 handshake.
DNS HTTPS Records for Instant HTTP/3
Browsers discover HTTP/3 in two ways: the Alt-Svc response header (requires a first h2 request) and DNS HTTPS (type 65) records (RFC 9460), which skip the bootstrap entirely.
If you sit behind Cloudflare with the orange cloud on, HTTPS records are synthesized automatically. For other DNS providers, add:
example.com. 300 IN HTTPS 1 . alpn="h3,h2"
Verify:
dig example.com HTTPS +short
Expected:
1 . alpn="h3,h2"
Verification
curl
On Debian 13 trixie, the stock curl (8.14) is built with HTTP/3 support. Ubuntu 20.04 through 24.04 and Debian 12 ship older curl builds without HTTP/3. Check yours with:
curl -V | grep -o HTTP3
If present, negotiate directly:
curl --http3 -sv https://example.com/ -o /dev/null -w 'protocol=%{http_version} code=%{http_code}\n'
A working response looks like:
protocol=3 code=200
On codenames whose stock curl lacks HTTP/3, use a browser or install an HTTP/3-capable curl build from a backports repository.
Browser DevTools
Open DevTools, Network tab, enable the Protocol column. Visit your site twice (the first request bootstraps over h2; the second should show h3).
Online Tester
http3check.net gives a one-shot external check including HTTPS DNS record presence.
Access Log
Add $server_protocol to your log_format to see HTTP/3 requests inline:
log_format h3aware '$remote_addr - $remote_user [$time_local] '
'"$request" $status $body_bytes_sent '
'"$http_referer" "$http_user_agent" '
'"$server_protocol"';
access_log /var/log/nginx/access.log h3aware;
HTTP/3 requests will log as HTTP/3.0.
Troubleshooting
Connections drop after nginx -s reload
If you are on our package, this should not happen. Confirm first:
dpkg -s nginx | grep Maintainer
Must show GetPageSpeed LLC. If it does and you still see drops, file an issue; we’ll want to know.
nginx -V shows no --with-http_v3_module
The pin isn’t in effect and apt installed the distro nginx or a ondrej/nginx build. Check:
apt-cache policy nginx
The extras.getpagespeed.com line should show priority 1001. If not, fix /etc/apt/preferences.d/getpagespeed-nginx.pref and reinstall:
sudo apt-get install --reinstall nginx
unknown directive "http3"
Same root cause: HTTP/3 is not compiled in. Verify as above.
Browser still shows HTTP/2
One of: missing DNS HTTPS record, missing Alt-Svc header, or cold bootstrap cache. Check the Alt-Svc header with curl -sI https://example.com/ | grep -i alt-svc.
bind() failed (13: Permission denied) on UDP 443
AppArmor is the usual culprit on Ubuntu. Check journalctl -k | grep -i apparmor right after the failed reload. If you see denials against /usr/sbin/nginx, either adjust the profile or set it to complain mode temporarily (sudo aa-complain /etc/apparmor.d/usr.sbin.nginx).
UDP buffer errors in journalctl -u nginx
You forgot to apply 99-quic.conf. Re-run sudo sysctl -p /etc/sysctl.d/99-quic.conf.
failed to create BPF map (1: Operation not permitted) or ngx_quic_bpf_module failed to initialize, check limits
Kernel is older than 5.7, the nginx master lacks CAP_SYS_ADMIN, or you’re running inside an unprivileged container. Drop quic_bpf on;, upgrade the kernel, or run the container with the capability granted.
ssl_early_data has no effect
This is not a bug. 0-RTT requires OpenSSL 3.5.1+ natively or a QUIC-capable OpenSSL backend such as quictls. On Ubuntu 20.04/22.04/24.04 and Debian 12 the system OpenSSL does not meet this requirement. Debian 13 trixie (OpenSSL 3.5.x) supports 0-RTT with our stock package; for Ubuntu, see the quictls section above.
Conclusion
One APT package delivers production-ready NGINX HTTP/3 on Ubuntu 20.04, 22.04, 24.04, and Debian 12 and 13: HTTP/3 compiled in via the OpenSSL compatibility layer, the reload-drop bug fixed, a persistent QUIC host key, and a UFW profile for UDP 443. The only feature still gated on OpenSSL backend is 0-RTT resumption, which is on the way via our quictls-linked rebuild for non-trixie codenames. Tell us which codename needs 0-RTT most and it moves up the queue.
For the RPM-based equivalent of this guide, see How to Install NGINX QUIC on CentOS, RHEL, Rocky Linux, AlmaLinux, and Fedora. For the deep dive on the reload bug we patched, see NGINX HTTP/3 Is Broken After Reload — Here’s the Fix F5 Won’t Ship.

