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 sudo privileges.
  • 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.

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 quic opens UDP port 443 for QUIC. listen 443 ssl keeps HTTPS over TCP for the first visit and for clients without HTTP/3.
  • reuseport lets 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, put reuseport on the quic listeners of one server block only and use listen 443 quic; in the others.
  • http2 on enables HTTP/2 for TCP clients.
  • Alt-Svc tells 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.

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:

  1. Open https://your_domain in Chrome, Edge or Firefox.
  2. Open the developer tools, go to the Network tab and enable the Protocol column (right-click the column headers).
  3. Reload the page, once or twice. The first request usually shows h2, because the browser only learns about HTTP/3 from the Alt-Svc header. Following requests show h3.

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. Check sudo 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 sure nginx -V shows --with-http_v3_module and that the package came from nginx.org (apt policy nginx).
  • duplicate listen options for 0.0.0.0:443: reuseport appears 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.