Back in 2016 I wrapped nginx in a Docker container just to get HTTP/2 working. Not because I love Docker, but because the server’s OpenSSL was too old to do ALPN, and the cleanest way to borrow a modern OpenSSL was to lift one out of an Alpine container. It worked, but it was a hack, and I knew it at the time.
That hack is dead. In 2026 you don’t need any of it. HTTP/2 has been a one-line config option for years, HTTP/3 ships in mainline nginx, and the OpenSSL situation that drove the whole workaround is ancient history. If you’re still running nginx in a container just for the TLS stack, you can stop.
Here’s what serving HTTP/2 and HTTP/3 looks like today.
What changed since 2016#
The 2016 problem was specific. Chrome dropped NPN support and required ALPN instead, and ALPN needed OpenSSL 1.0.2. Plenty of stable distros were still shipping 1.0.1, so you were stuck. Hence the container.
Now every distro you’d reasonably run ships OpenSSL 3.x. ALPN isn’t a question anymore, it’s just there. HTTP/2 needs nothing special at build time. And HTTP/3, the thing that didn’t even exist as a finished standard in 2016, has been in mainline nginx since 1.25.0 (mid-2023) and is included in the official nginx.org binary packages.
So the job isn’t “compile a modern TLS stack” anymore. It’s “add a couple of directives and open a firewall port.”
HTTP/2: one line, and a deprecation to know about#
If you copied an nginx config from a tutorial written before mid-2023, you probably have this:
listen 443 ssl http2;
That still works, but since nginx 1.25.1 it throws a warning:
$ nginx -t
nginx: [warn] the "listen ... http2" directive is deprecated, use the "http2" directive instead
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
The fix is to split it. http2 is now its own directive:
listen 443 ssl;
http2 on;
Same result, no warning. That’s the entire HTTP/2 story in 2026. No special build, no container, no OpenSSL gymnastics.
Adding HTTP/3 (QUIC)#
HTTP/3 is where it gets interesting, because QUIC runs over UDP instead of TCP. You add a second listen line for the quic protocol next to your existing TLS listener:
server {
listen 443 ssl;
listen 443 quic reuseport;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
add_header Alt-Svc 'h3=":443"; ma=86400';
# ... your usual proxy_pass / root config
}
}
A few things worth pointing out.
The reuseport parameter goes on the quic listener so connections get balanced across your worker processes. Put it on one listen line only. If you add it to a second server block on the same address, nginx refuses to start:
$ nginx -t
nginx: [emerg] duplicate listen options for 0.0.0.0:443
nginx: configuration file /etc/nginx/nginx.conf test failed
You don’t need an explicit http3 on;. HTTP/3 is enabled by default once a quic listener exists, so the directive is redundant.
The Alt-Svc header is what makes browsers use HTTP/3. A browser connects over HTTP/2 first, sees Alt-Svc: h3=":443" in the response, and upgrades to HTTP/3 on the next request. Without that header, nothing happens; your QUIC listener just sits there. The ma=86400 is how long, in seconds, the browser remembers to prefer h3.
One caveat on OpenSSL. QUIC needs TLS 1.3, which every modern OpenSSL does. But for full QUIC support including 0-RTT (early data), nginx wants OpenSSL 3.5.1 or newer. On anything older it falls back to a compatibility layer that works fine, you just don’t get early data. So HTTP/3 works on a stock distro nginx; whether you also get 0-RTT depends on how fresh your OpenSSL is. If you specifically want early data, run openssl version before you go chasing config.
The firewall gotcha#
This is the one that’ll eat your afternoon. HTTP/3 is UDP. Your firewall almost certainly allows TCP/443 and nothing else on that port, because for the last 25 years “open 443” has meant TCP.
So you need to open the UDP side too:
$ ufw allow 443/udp
Or the nftables, iptables, or cloud-security-group equivalent. And here’s the cruel part if you forget it: everything still works. The browser falls back to HTTP/2 over TCP, the site loads fine, and you’ll swear you configured HTTP/3 correctly. It’s just silently never negotiating h3. Check the firewall first.
Verify it’s on#
Don’t trust that it works, check it. Modern curl speaks HTTP/3 directly:
$ curl --http3 -I https://example.com
HTTP/3 200
...
If your distro’s curl is built with HTTP/3 support, and most are now, that HTTP/3 200 is your proof.
In the browser, open DevTools, go to the Network tab, right-click the column headers and enable the Protocol column. HTTP/3 connections show up as h3. The first request after a fresh load is often h2, because the browser only learns about HTTP/3 from the Alt-Svc header on that first response. Reload, and you should see h3.
I validated every config in this post with nginx -t against nginx 1.31.1 (the current mainline) in a stock nginx:alpine container, so the warnings and errors above are the real thing, not approximations.
What if you still want nginx in a container?#
That’s fine. Plenty of people run nginx in containers for good reasons: reproducible deploys, orchestration, isolation. Just don’t do it for the TLS stack anymore, because that reason is gone.
If you do containerize, the 2016 approach still holds up. Use the official nginx:alpine image with host networking and bind-mount your config and certs:
$ docker run --name nginx \
--net=host \
-v /etc/nginx/:/etc/nginx/ \
-v /etc/letsencrypt/:/etc/letsencrypt/ \
--restart=always \
-d nginx:alpine
Two things got better since 2016. The official images are patched quickly when a CVE lands, so the “who updates the nginx in that container?” worry I had back then is mostly answered: you do, by pulling a fresh tag. And nginx:alpine tracks the current mainline, so you get HTTP/3 in the container too, same quic listener, same UDP firewall rule.
The whole song and dance from 2016 collapses into two directives and a UDP firewall rule. If you’re maintaining a config or a container you set up years ago, it’s worth the five-minute cleanup.