HTTP/3 carries HTTP over QUIC, a transport built on UDP with TLS 1.3 integrated into the handshake. It connects faster than HTTP/2 over TCP and avoids transport-level head-of-line blocking, which matters most on mobile and lossy networks. In this tutorial you will enable HTTP/3 on Ubuntu 24.04 using the official Nginx packages, open UDP port 443, advertise the protocol with the Alt-Svc header and verify that browsers actually use it. A short section at the end shows the same result with Caddy, which enables HTTP/3 by default.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS, for example a CubePath VPS.
- A non-root user with
sudoprivileges. - A domain name, referred to as
your_domain, with an A record (and an AAAA record if you use IPv6) pointing to the server. - TCP ports 80 and 443 and UDP port 443 reachable from the internet, including through any firewall in front of the server.
Importantthe
nginxpackage in the Ubuntu 24.04 repositories is version 1.24, which predates HTTP/3 support (added in Nginx 1.25.0). This guide installs Nginx from the official nginx.org repository instead. If Ubuntu's Nginx is already installed, back up/etc/nginxand remove it withsudo apt remove nginx nginx-commonbefore Step 1.
Step 1 - Installing Nginx from the official repository
Install the packages 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 the APT keyrings directory:
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 always prefers it over the Ubuntu package with the same name:
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 the version and that the HTTP/3 module is compiled in:
nginx -v
nginx -V 2>&1 | grep -o with-http_v3_module
nginx version: nginx/1.30.5
with-http_v3_module
Any version from 1.25.0 onwards works. The nginx.org packages differ from Ubuntu's in layout: sites go in /etc/nginx/conf.d/*.conf (there is no sites-enabled), and the worker processes run as the nginx user.
Step 2 - Opening TCP and UDP port 443
QUIC runs over UDP, so a firewall that only allows 443/tcp silently blocks HTTP/3 and browsers quietly stay on HTTP/2. Allow SSH, HTTP and both protocols on port 443 with UFW:
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 offers a network firewall in front of the server, add a rule for UDP 443 there as well.
Step 3 - Obtaining a TLS certificate
QUIC always uses TLS 1.3 and cannot run without a certificate. Install Certbot with its Nginx plugin:
sudo apt install certbot python3-certbot-nginx
Request a certificate without letting Certbot edit your configuration, since you will write the server block yourself in the next step:
sudo certbot certonly --nginx -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.
Step 4 - Configuring Nginx for HTTP/3
Create a document root with a test page:
sudo mkdir -p /var/www/your_domain
echo '<h1>HTTP/3 test</h1>' | sudo tee /var/www/your_domain/index.html
Disable the default site that ships with the package, so it does not answer for your domain:
sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.disabled
Create the configuration for your domain:
sudo nano /etc/nginx/conf.d/your_domain.conf
server {
listen 80;
listen [::]:80;
server_name your_domain;
return 301 https://$host$request_uri;
}
server {
# HTTP/1.1 and HTTP/2 over TCP
listen 443 ssl;
listen [::]:443 ssl;
# HTTP/3 over QUIC (UDP)
listen 443 quic reuseport;
listen [::]:443 quic reuseport;
http2 on;
server_name your_domain;
root /var/www/your_domain;
index index.html;
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;
# Tell browsers that HTTP/3 is available on UDP port 443 for 24 hours
add_header Alt-Svc 'h3=":443"; ma=86400' always;
location / {
try_files $uri $uri/ =404;
}
}
The key parts are:
listen 443 quicopens the UDP listener. Keep the TCPlisten 443 sslline: every browser makes its first connection over TCP and only switches to HTTP/3 after it sees theAlt-Svcheader.reuseportspreads QUIC packets across worker processes. It may appear only once per address and port, so on additional sites uselisten 443 quic;without it.add_header Alt-Svcadvertises HTTP/3. Nginx does not inheritadd_headerdirectives into alocationthat defines its ownadd_header, so repeat the line there if you add headers in a location.
Test the configuration and reload:
sudo nginx -t
sudo systemctl reload nginx
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful
Confirm that Nginx listens on UDP port 443:
sudo ss -ulnp | grep ':443'
UNCONN 0 0 0.0.0.0:443 0.0.0.0:* users:(("nginx",pid=2481,fd=8),("nginx",pid=2480,fd=7))
UNCONN 0 0 [::]:443 [::]:* users:(("nginx",pid=2481,fd=10),("nginx",pid=2480,fd=9))
Step 5 - Verifying HTTP/3
First, confirm that the Alt-Svc header is sent over the regular TCP 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. Check whether the curl you are using supports it:
curl --version | grep -o HTTP3
If the command prints HTTP3 (for example on a recent macOS or a distribution with a newer curl), test from that machine and force HTTP/3 only, so the request fails instead of falling back to TCP:
curl -sI --http3-only https://your_domain
HTTP/3 200
server: nginx/1.30.5
alt-svc: h3=":443"; ma=86400
In a browser, open https://your_domain, open DevTools, go to the Network tab, right-click a column header and enable Protocol. Reload the page: the first load shows h2, and after the reload the document shows h3.
The Nginx access log records the protocol of each request, which is a reliable server-side check:
sudo tail -n 3 /var/log/nginx/access.log
203.0.113.25 - - [25/Sep/2026:10:52:10 +0000] "GET / HTTP/2.0" 200 21 "-" "Mozilla/5.0 ..."
203.0.113.25 - - [25/Sep/2026:10:52:14 +0000] "GET / HTTP/3.0" 200 21 "-" "Mozilla/5.0 ..."
Alternative: HTTP/3 with Caddy
Caddy has enabled HTTP/3 by default since version 2.6, together with automatic HTTPS. If you prefer it over Nginx, install it from the official repository on a server where nothing else uses ports 80 and 443:
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
The repository definition references the key in /usr/share/keyrings, so keep that path. Replace the contents of the Caddyfile:
sudo nano /etc/caddy/Caddyfile
your_domain {
root * /var/www/your_domain
file_server
encode zstd gzip
}
Reload Caddy and check the header:
sudo systemctl reload caddy
curl -sI https://your_domain | grep -i alt-svc
alt-svc: h3=":443"; ma=2592000
The firewall rules from Step 2 apply to Caddy in the same way.
Troubleshooting
Alt-Svc is present but the browser stays on h2. UDP 443 is blocked somewhere between the client and the server. Check UFW, any network firewall in front of the server and the client's own network: many corporate networks block outbound UDP 443. Browsers also remember a failed QUIC attempt for a while, so test again from a private window.
nginx -t fails with invalid parameter "quic". You are running Ubuntu's Nginx 1.24 instead of the nginx.org build. Check nginx -v and apt policy nginx, and verify the pin file from Step 1.
nginx -t fails with duplicate listen options for 0.0.0.0:443. reuseport is set on more than one listen ... quic line for the same port. Keep it in one server block only.
Caddy logs "failed to sufficiently increase receive buffer size". The QUIC library in Caddy wants larger UDP buffers than the kernel default. Raise them persistently:
printf 'net.core.rmem_max=7500000\nnet.core.wmem_max=7500000\n' | sudo tee /etc/sysctl.d/60-quic-buffers.conf
sudo sysctl --system
sudo systemctl restart caddy
Conclusion
Your site now serves HTTP/3 over QUIC alongside HTTP/2, advertises it with Alt-Svc and falls back to TCP automatically for clients that cannot use UDP. Returning visitors, especially on mobile networks, get faster connection setup. As next steps, add Brotli compression and long-lived cache headers for static assets, and compare Largest Contentful Paint in Lighthouse before and after the change.
