ERR_QUIC_PROTOCOL_ERROR in Nginx Proxy Manager Home Lab Configuration 

ERR_QUIC_PROTOCOL_ERROR
ehmad.site

I recently encountered an ERR_QUIC_PROTOCOL_ERROR while accessing my Pi-hole admin interface (https://example.com/admin/) locally via Chromium-based browsers, such as Google Chrome and Microsoft Edge. Although I implemented workarounds, the root cause remains elusive. This article details the issue, troubleshooting efforts, and current mitigation—while seeking insights from others to resolve it fully.

System Architecture

The setup utilizes a single domain, example.com, with two distinct access paths:

  • Local Access: On the home network, a local DNS server resolves the domain to Nginx Proxy Manager (NPM), running in a Docker container. NPM, secured with a Let’s Encrypt SSL certificate, proxies requests to the Pi-hole server (e.g., 10.1.15.103:81).
  • Remote Access: Externally, traffic flows through a Cloudflare tunnel, secured with a Google Trust Services SSL certificate, before reaching the same local infrastructure.

Both paths generally function, but the local Chromium-specific error prompted an in-depth investigation.

Problem Description

The ERR_QUIC_PROTOCOL_ERROR occurs exclusively in Chromium-based browsers during local access to the Pi-hole admin page. Firefox loads the page without issue, and remote access via Cloudflare operates seamlessly. An initial curl -I https://example.com/admin/ test returned an HTTP/2 403 response from Cloudflare, suggesting possible DNS misrouting, though local requests should target NPM directly. This discrepancy hinted at a protocol-level issue tied to QUIC.

Troubleshooting Efforts

QUIC, the UDP-based foundation of HTTP/3, operates over UDP port 443 and is enabled by default in Chromium, unlike Firefox, which defaults to TCP-based HTTP/2. Initial findings revealed that Docker exposes only TCP ports by default—my NPM container lacked UDP/443 mapping (-p 443:443/udp). Adding this port resolved the error temporarily, allowing Chromium to connect locally.

However, enabling caching in NPM—by removing no-cache headers (Cache-Control: no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0)—caused the error to recur. Reverting to a no-cache configuration stabilized access again, but this oscillation suggests an underlying issue beyond port configuration.

Current Mitigation

The following adjustments provide a functional workaround:

  1. Docker Port Configuration: Ensured UDP/443 is exposed alongside TCP/443 in the NPM container:

    services:
      nginx-proxy-manager:
        ports:
          - "443:443/tcp"
          - "443:443/udp"
    

    Applied via docker-compose up -d or equivalent docker run command.

  2. NPM Configuration: Maintained a no-cache policy for example.com:

    server {
        listen 443 ssl http2;
        server_name example.com;
        ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
        ssl_protocols TLSv1.2 TLSv1.3;
        ssl_ciphers HIGH:!aNULL:!MD5;
        add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0";
        add_header Pragma "no-cache";
        add_header Expires "0";
        location / {
            proxy_pass http://10.1.15.103:81;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
    

    Changes were applied through NPM and the container restarted.

This configuration enables local access via Chromium and Firefox, alongside remote access through Cloudflare, without errors—as long as caching remains disabled.

Unresolved Root Cause

Despite these mitigations, the fundamental issue persists:

  • QUIC and Caching Interaction: Enabling caching in NPM consistently triggers the ERR_QUIC_PROTOCOL_ERROR in Chromium, suggesting a compatibility issue. NPM supports HTTP/2 but not HTTP/3 natively, and caching may disrupt QUIC’s UDP-based sessions—possibly due to stale responses or improper header handling.
  • Hypotheses: Potential causes include an NPM limitation with QUIC traffic, a Docker networking quirk, or an incompatibility between Pi-hole’s dynamic content and cached QUIC responses. Logs from NPM and Chromium’s chrome://net-internals/#quic showed no definitive errors beyond the protocol failure.
  • Contrast with Cloudflare: Remote access via Cloudflare, which fully supports QUIC, remains unaffected, highlighting a local configuration gap.

Technical Observations

  • Port Dependency: UDP/443 is essential for QUIC, and Docker’s TCP-only default obscured this requirement initially.
  • Browser Variability: Chromium’s QUIC adoption contrasts with Firefox’s TCP reliance, isolating the issue to QUIC-capable clients.
  • Caching Sensitivity: Disabling caching is a workaround, not a fix, as it sidesteps rather than resolves the underlying incompatibility.

Call for Insight

While the current setup is operational, I have yet to identify the root cause of the QUIC-caching conflict. If anyone has encountered similar behavior with Nginx Proxy Manager, Docker, and QUIC—or knows how to enable caching without breaking QUIC compatibility—I’d appreciate your expertise. Please share your insights or solutions to help fully resolve this issue. Github Issue

Conclusion

This investigation highlights the complexities of integrating modern protocols into containerized environments. For now, exposing UDP/443 and disabling caching in NPM ensures consistent access to example.com locally and remotely. However, the unresolved QUIC error with caching enabled remains a puzzle. Community input could unlock a more comprehensive solution, enhancing this home lab’s robustness.