HTTP/3 is the version of HTTP that runs over QUIC, a transport built on UDP with TLS 1.3 integrated. Compared with HTTP/2 over TCP, it needs fewer round trips to set up a connection, avoids head-of-line blocking when packets are lost, and lets mobile clients keep a connection when they switch networks. In this tutorial you will enable HTTP/3 in Nginx on Ubuntu 24.04, open the firewall for QUIC, advertise HTTP/3 to browsers and confirm that clients actually use it. A short section at the end shows the same result with Caddy.
Prerequisites
To follow this tutorial you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - A domain name with an A (and optionally AAAA) record pointing to the server. This guide uses
your_domain. - Ports 80/tcp, 443/tcp and 443/udp reachable from the Internet.
HTTP/3 always requires a valid TLS certificate, and browsers only try it after they have learned from an HTTP/1.1 or HTTP/2 response that the server supports it. HTTP/2 over TCP therefore stays in place as the fallback.
Step 1 - Installing Nginx from the nginx.org repository
The Nginx package in the Ubuntu 24.04 repository is version 1.24, which does not include the HTTP/3 module. The official packages from nginx.org are newer and are built with --with-http_v3_module, so you do not need to compile anything.
WarningIf Nginx from the Ubuntu repository is already installed, back up
/etc/nginxand remove it first withsudo apt remove nginx nginx-common. The two packages use a different layout: the nginx.org package reads/etc/nginx/conf.d/*.confand has nosites-enableddirectory.
Install the tools needed to add the repository:
sudo apt update
sudo apt install curl gnupg2 ca-certificates lsb-release ubuntu-keyring
Download the nginx.org signing key into /etc/apt/keyrings:
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://nginx.org/keys/nginx_signing.key | sudo gpg --dearmor -o /etc/apt/keyrings/nginx-archive-keyring.gpg
Add the stable repository for your Ubuntu release:
echo "deb [signed-by=/etc/apt/keyrings/nginx-archive-keyring.gpg] https://nginx.org/packages/ubuntu $(lsb_release -cs) nginx" | sudo tee /etc/apt/sources.list.d/nginx.list
Pin the repository so apt prefers it over the Ubuntu package:
printf "Package: *\nPin: origin nginx.org\nPin: release o=nginx\nPin-Priority: 900\n" | sudo tee /etc/apt/preferences.d/99nginx
Install and start Nginx:
sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginx
Confirm that the binary includes the HTTP/3 module:
nginx -V 2>&1 | grep -o with-http_v3_module
with-http_v3_module
Step 2 - Opening the firewall for QUIC
QUIC uses UDP, so the usual rule for HTTPS on TCP port 443 is not enough. Allow SSH first if UFW is not active yet, then open HTTP, HTTPS and QUIC:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw enable
Check the rules:
sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
443/udp ALLOW Anywhere
...
If your provider or network applies its own firewall in front of the server, allow UDP 443 there too.
Step 3 - Obtaining a TLS certificate
Create a document root and a plain HTTP server block that Certbot can use for validation. Remove the package's default site first:
sudo rm /etc/nginx/conf.d/default.conf
sudo mkdir -p /var/www/your_domain
echo "Hello over HTTP/3" | sudo tee /var/www/your_domain/index.html
sudo nano /etc/nginx/conf.d/your_domain.conf
server {
listen 80;
listen [::]:80;
server_name your_domain;
root /var/www/your_domain;
}
Reload Nginx, install Certbot and request a certificate with the webroot method:
sudo nginx -t && sudo systemctl reload nginx
sudo apt install certbot
sudo certbot certonly --webroot -w /var/www/your_domain -d your_domain
Successfully received certificate.
Certificate is saved at: /etc/letsencrypt/live/your_domain/fullchain.pem
Key is saved at: /etc/letsencrypt/live/your_domain/privkey.pem
Certbot installs a systemd timer that renews the certificate automatically. Because renewal uses the same webroot, keep the /.well-known/acme-challenge/ path reachable over HTTP in the next step.
Step 4 - Enabling HTTP/3 in the server block
Replace the contents of the site configuration:
sudo nano /etc/nginx/conf.d/your_domain.conf
server {
listen 80;
listen [::]:80;
server_name your_domain;
location /.well-known/acme-challenge/ {
root /var/www/your_domain;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
listen 443 ssl;
listen [::]:443 ssl;
http2 on;
server_name your_domain;
root /var/www/your_domain;
ssl_certificate /etc/letsencrypt/live/your_domain/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Alt-Svc 'h3=":443"; ma=86400' always;
}
The important lines are:
listen 443 quicopens UDP port 443 for QUIC.listen 443 sslkeeps HTTPS over TCP for the first visit and for clients without HTTP/3.reuseportlets every worker process get its own UDP socket, which QUIC needs to route packets to the right worker. It can appear only once per address and port, so if you host several sites, putreuseporton thequiclisteners of one server block only and uselisten 443 quic;in the others.http2 onenables HTTP/2 for TCP clients.Alt-Svctells browsers that the same site is available over HTTP/3 on UDP port 443 for the next 86,400 seconds. Without this header, browsers never try HTTP/3.
Note
add_headerdirectives are inherited only if thelocationblock does not define its own. If you add headers inside alocation, repeat theAlt-Svcheader there.
Test the configuration and reload:
sudo nginx -t && sudo systemctl reload nginx
Confirm that Nginx listens on UDP port 443:
sudo ss -ulpn | grep ':443'
UNCONN 0 0 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=4312,fd=7),("nginx",pid=4311,fd=6))
UNCONN 0 0 [::]:443 [::]:* users:(("nginx",pid=4312,fd=8),("nginx",pid=4311,fd=9))
Step 5 - Verifying HTTP/3
First confirm that the Alt-Svc header is sent over the normal HTTPS connection:
curl -sI https://your_domain | grep -i alt-svc
alt-svc: h3=":443"; ma=86400
The curl package in Ubuntu 24.04 is not built with HTTP/3 support, so use a browser for the end-to-end test:
- Open
https://your_domainin Chrome, Edge or Firefox. - Open the developer tools, go to the Network tab and enable the Protocol column (right-click the column headers).
- Reload the page, once or twice. The first request usually shows
h2, because the browser only learns about HTTP/3 from theAlt-Svcheader. Following requests showh3.
The access log confirms it from the server side, because Nginx records the protocol of each request:
sudo tail -n 3 /var/log/nginx/access.log
203.0.113.25 - - [25/Sep/2026:10:31:07 +0000] "GET / HTTP/2.0" 200 18 "-" "Mozilla/5.0 ..."
203.0.113.25 - - [25/Sep/2026:10:31:12 +0000] "GET / HTTP/3.0" 200 18 "-" "Mozilla/5.0 ..."
Online HTTP/3 checkers are another quick way to test from outside your network.
Alternative: HTTP/3 with Caddy
Caddy enables HTTP/3 by default and manages certificates automatically, so it needs no QUIC specific configuration. Use it on a server where Nginx is not listening on ports 80 and 443.
Install Caddy from its official repository:
sudo apt install debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Create a document root with a test page:
sudo mkdir -p /var/www/your_domain
echo "Hello over HTTP/3" | sudo tee /var/www/your_domain/index.html
Edit the Caddyfile:
sudo nano /etc/caddy/Caddyfile
your_domain {
root * /var/www/your_domain
file_server
}
Reload Caddy:
sudo systemctl reload caddy
Caddy obtains a certificate, listens on UDP 443 and sends the Alt-Svc header on its own. Open UDP 443 in the firewall as in Step 2 and verify with the browser as in Step 5.
Troubleshooting
- The browser always shows
h2: UDP 443 is blocked somewhere between the client and the server. Checksudo ufw status, any provider firewall, and the client network: many corporate networks block outgoing UDP 443, and browsers then fall back silently to HTTP/2. nginx: [emerg] invalid parameter "quic": the Nginx binary lacks the HTTP/3 module. Make surenginx -Vshows--with-http_v3_moduleand that the package came from nginx.org (apt policy nginx).duplicate listen options for 0.0.0.0:443:reuseportappears in more than one server block for the same address and port. Keep it in only one.- Caddy logs
failed to sufficiently increase receive buffer size: the kernel limits UDP buffers. Raise them with a sysctl file:
printf "net.core.rmem_max=7500000\nnet.core.wmem_max=7500000\n" | sudo tee /etc/sysctl.d/99-quic.conf
sudo sysctl --system
Conclusion
Your site now serves HTTP/3 over QUIC on UDP port 443, advertises it with the Alt-Svc header, and keeps HTTP/2 over TCP as the fallback for clients and networks that cannot use QUIC. You confirmed it both in the browser and in the Nginx access log.
As next steps, add HTTP/3 to your reverse proxy sites by adding the quic listeners to their server blocks, compare page load times over h2 and h3 from a mobile connection, and review your TLS settings with an SSL test tool.
