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

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 quic opens the UDP listener. Keep the TCP listen 443 ssl line: every browser makes its first connection over TCP and only switches to HTTP/3 after it sees the Alt-Svc header.
  • reuseport spreads QUIC packets across worker processes. It may appear only once per address and port, so on additional sites use listen 443 quic; without it.
  • add_header Alt-Svc advertises HTTP/3. Nginx does not inherit add_header directives into a location that defines its own add_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.