Custom Nginx + PHP-FPM (HTTP/3 + HTTP/2) not working

I would like to use HTTP/3 (and HTTP/2 as fallback) in my custom Nginx + PHP-FPM setup. I am not using Apache or Hestia’s specific “Nginx Proxy Mode”. With my current template, I am getting this error in nginx -t

nginx: [emerg] duplicate listen options for 123.123.123.123:443 in /etc/nginx/conf.d/domains/mydomain.com.ssl.conf:6
nginx: configuration file /etc/nginx/nginx.conf test failed

Why is it not possible to use 2 listen directives? What would be the workaround if I still want to use HTTP/3 (and HTTP/2 as fallback)?

And these are the custom templates.

http3.stpl

server {
    # ──────────────────────────────────────────────────────────────── #
    # LISTEN SOCKETS                                                 #
    #----------------------------------------------------------------#
    listen      %ip%:%web_ssl_port% ssl;              # HTTP/2
    listen      %ip%:%web_ssl_port% quic reuseport;   # HTTP/3
    http2       on;

    # ──────────────────────────────────────────────────────────────── #
    # CORE SETTINGS                                                  #
    #----------------------------------------------------------------#
    server_name %domain_idn% %alias_idn%;
    error_log   /var/log/%web_system%/domains/%domain%.error.log error;

    ssl_certificate     %ssl_pem%;
    ssl_certificate_key %ssl_key%;
    ssl_stapling        on;
    ssl_stapling_verify on;

    # ─── TLS 1.3 & TLS 1.2  ───────────────────────────────────── #
    ssl_protocols TLSv1.3 TLSv1.2;
    ssl_prefer_server_ciphers off;

    # Advertise HTTP/3 to the browser
    add_header alt-svc 'h3=":%web_ssl_port%"; ma=86400' always;

    # TLS 1.3 0‑RTT anti‑replay
    if ($anti_replay = 307) { return 307 https://$host$request_uri; }
    if ($anti_replay = 425) { return 425; }

    include %home%/%user%/conf/web/%domain%/nginx.hsts.conf*;

    # ──────────────────────────────────────────────────────────────── #
    # SECURITY                                                       #
    #----------------------------------------------------------------#
    location ~ /\.(?!well-known\/|file) {
        deny all;
        return 404;
    }

    # WEB ROOT & INDEX FILES                                         #
    #----------------------------------------------------------------#
    root        %sdocroot%;
    index       index.php index.html index.htm;
    
    # ──────────────────────────────────────────────────────────────── #
    # PHP Handler & Basic Routing                                    #
    #----------------------------------------------------------------#
    location / {
        # If a file or directory exists, serve it directly.
        # Otherwise, pass request to index.php (common for frameworks).
        # Adjust this try_files directive if your app needs different routing.
        try_files $uri $uri/ /index.php?$args;
    }
    
    # Include the specific PHP-FPM backend configuration selected in Hestia panel
    # This contains the fastcgi_pass directive.
    include %home%/%user%/conf/web/%domain%/nginx.php.conf*;
    
    # ──────────────────────────────────────────────────────────────── #
    # STATIC ASSETS (Optional: Enhance caching/headers if needed)    #
    #----------------------------------------------------------------#
    # You can add specific location blocks for static assets if you want
    # custom expiry headers or other rules, e.g.:
     location ~* \.(jpg|jpeg|gif|png|webp|svg|css|js|ico|woff2)$ {
        expires max;
        access_log off;
    }

    # ──────────────────────────────────────────────────────────────── #
    # ERROR PAGES                                                    #
    #----------------------------------------------------------------#
    location /error/ {
        alias %home%/%user%/web/%domain%/document_errors/;
    }

    include %home%/%user%/conf/web/%domain%/nginx.ssl.conf_*;
}

http3.tpl

server {
        listen      %ip%:%web_port%;
        server_name %domain_idn% %alias_idn%;
        error_log   /var/log/%web_system%/domains/%domain%.error.log error;

        # Tell the browser that HTTP/3 is available over TLS (port 443)
        add_header  alt-svc 'h3=":%web_ssl_port%"; ma=86400' always;

        # Force‑SSL include (keeps existing 301 redirect logic)
        include %home%/%user%/conf/web/%domain%/nginx.forcessl.conf*;

        # ─────────────────────────── security ──────────────────────────
        location ~ /\.(?!well-known\/|file) {
                deny all;
                return 404;
        }

        include %home%/%user%/conf/web/%domain%/nginx.conf_*;
}

It’s possible to add two like this:

    listen      %ip%:%web_ssl_port% ssl;              # HTTP/2
    listen      %ip%:%web_ssl_port% quic;   # HTTP/3

However, reuseport is allowed just once per ip:port.

You can create one template with reuseport (to be used on a single domain) and another template without reuseport for all other domains.

1 Like

Ok, that definitely fixed the issue. I was not aware of that nginx limitation with the reuseport directive when using multiple domains over the same IP. Now, I am wondering why I am still seeing HTTP/2 as protocol in my browser when loading the website that uses this template. For example, requests to google related domains (gtm, googleapis, recaptcha) are loading with HTTP/3 while requests to my website are loading with HTTP/2.

It is curious because when I activate Cloudflare (orange clouds), I do see HTTP/3 as protocol in my browser. But when I keep it deactivated, I see HTTP/2. Like if something in the nginx conf was not working as expected.

Did you add a firewall rule to open port 443 UDP?

Yes, firewall rule is added to allow UDP traffic on port 443.

If you share your domain I can test it on my end.

How can I correctly verify it myself? - Online tools to check HTTP/3 seem unreliable.

If this site says your site is using http3 then you can trust it

Or use the browser’s developer tools to inspect your site:

In this case you see the first connection uses http2 but once the browser sees the header saying your site supports http3, next connections use http3.

From command line you can use a curl version that supports http3:

❯ curl --http3 -fsIL https://7j.gg
HTTP/3 200 
server: nginx
date: Mon, 21 Apr 2025 10:20:48 GMT
content-type: text/html; charset=utf-8
content-length: 582
last-modified: Tue, 19 Dec 2023 09:24:00 GMT
vary: accept-encoding
etag: "65816130-246"
alt-svc: h3=":443"; ma=86400
strict-transport-security: max-age=31536000;
accept-ranges: bytes
3 Likes