A Traefik reverse proxy setup designed for Network Optimizer that solves the HTTP/1.1 speed test problem with a single proxy instance.
OpenSpeedTest requires HTTP/1.1 for accurate throughput measurements - HTTP/2 multiplexing and flow control interfere with speed test results. Most reverse proxies (including Caddy) negotiate HTTP/2 at the TLS level and can't serve different protocols per hostname on the same port.
Traefik supports per-router TLS options, including ALPN protocol selection. This setup uses a custom TLS option (h1only) that only advertises http/1.1 during the TLS handshake for the speed test hostname, while the main app uses the default HTTP/2 negotiation. One proxy, one IP, one port 443.
git clone https://github.com/Ozark-Connect/NetworkOptimizer-Proxy.git
cd NetworkOptimizer-Proxy
# Run setup script (creates config files from examples, sets permissions)
bash setup.sh
# Edit your configuration
nano .env # Cloudflare token, email, listen IP
nano dynamic/config.yml # Update hostnames
# Start
docker compose up -dTraefik is included as an optional feature in the Network Optimizer MSI installer. When selected, the installer prompts for Cloudflare DNS settings and the service manages Traefik as a child process alongside nginx.
The windows/ directory contains the config templates used by the MSI build:
traefik.yml.template- Static config with placeholders for registry valuesconfig.yml.template- Dynamic config with placeholders for hostnames/ports
- For Linux: Docker and Docker Compose
- For Windows: Network Optimizer MSI installer (Traefik feature)
- A domain with DNS managed by Cloudflare (for automatic Let's Encrypt certificates)
- Two DNS A records pointing to the host running Traefik:
- e.g.
optimizer.yourdomain.com- Network Optimizer web UI - e.g.
speedtest.yourdomain.com- OpenSpeedTest (HTTP/1.1)
- e.g.
| Variable | Required | Default | Description |
|---|---|---|---|
ACME_EMAIL |
Yes | - | Email for Let's Encrypt registration |
CF_DNS_API_TOKEN |
Yes | - | Cloudflare API token with Zone:DNS:Edit permissions |
LISTEN_IP |
No | 0.0.0.0 |
Bind to a specific IP address |
LOG_LEVEL |
No | INFO |
Log verbosity: DEBUG, INFO, WARN, ERROR |
Copy from the example and update hostnames:
cp config.example.yml dynamic/config.ymlThe example config includes two routers:
- optimizer - Network Optimizer on HTTP/2 (default TLS)
- speedtest - OpenSpeedTest on HTTP/1.1 (
h1onlyTLS option)
Edit the Host() rules to match your DNS:
optimizer:
rule: "Host(`optimizer.yourdomain.com`)"
# ...
speedtest:
rule: "Host(`speedtest.yourdomain.com`)"
# ...Optional file for middleware that injects credentials (e.g., Basic Auth headers for backend services). Copy from example if needed:
cp secrets.example.yml dynamic/secrets.ymlThis file is gitignored and managed directly on the host.
The h1only TLS option restricts ALPN negotiation to http/1.1 only:
tls:
options:
h1only:
minVersion: VersionTLS12
alpnProtocols:
- "http/1.1"When a browser connects to the speed test hostname, the TLS handshake negotiates HTTP/1.1 instead of HTTP/2. The speed test router references this option:
speedtest:
rule: "Host(`speedtest.yourdomain.com`)"
tls:
options: h1only # Forces HTTP/1.1 on the client connectionIn addition to HTTP/1.1, the speed test route strips the Accept-Encoding header to prevent transparent compression from skewing results.
Traefik uses Let's Encrypt with Cloudflare DNS-01 challenges, so:
- No port 80 exposure required for certificate validation
- Wildcard certificates are supported
- Certificates auto-renew before expiry
By default, DNS propagation checking is disabled (disablepropagationcheck=true) and replaced with a fixed 15-second delay. This avoids certificate failures caused by local DNS resolvers (Pi-hole, NextDNS, AdGuard Home, etc.) that may not see the Cloudflare TXT records during validation. Cloudflare's API is fast enough that 15 seconds is plenty.
To proxy additional services behind Traefik, add routers and services to dynamic/config.yml. Example:
http:
routers:
my-service:
rule: "Host(`myservice.yourdomain.com`)"
entryPoints:
- websecure
service: my-service
tls:
certResolver: letsencrypt
middlewares:
- security-headers
services:
my-service:
loadBalancer:
servers:
- url: "http://localhost:9090"Use the ipAllowList middleware to restrict access:
http:
middlewares:
lan-only:
ipAllowList:
sourceRange:
- "192.168.0.0/16"
- "10.0.0.0/8"
routers:
my-service:
middlewares:
- security-headers
- lan-onlyInternet
|
v
[Traefik :443]
|
|-- optimizer.example.com (HTTP/2, default TLS) --> localhost:8042
|
|-- speedtest.example.com (HTTP/1.1, h1only TLS) --> localhost:3005
|
|-- (other services...)
|
[Traefik :80]
|-- All HTTP --> 301 redirect to HTTPS
- Single Traefik instance handles all hostnames
- Host networking (
network_mode: host) for direct access to local services - DNS-01 challenges via Cloudflare (no HTTP-01, no port 80 exposure needed)
- File provider watches
dynamic/for config changes (hot reload, no restart needed)
Traefik stores all Let's Encrypt certificates in acme/acme.json. To extract a certificate for use elsewhere (e.g., a UniFi gateway):
# Install traefik-certs-dumper (or use jq)
docker run --rm -v ./acme:/acme ldez/traefik-certs-dumper file \
--source /acme/acme.json --dest /acme/certs \
--domain-subdirCertificates not issuing: Check that your Cloudflare API token has Zone:DNS:Edit permissions and that the domain's DNS is managed by Cloudflare. Check logs with docker compose logs. If you see NXDOMAIN or propagation timeout errors, your local DNS resolver may be interfering - the default config already handles this, but you can increase delaybeforecheck in docker-compose.yml if needed.
Speed test still using HTTP/2: Verify the speed test router references options: h1only in its TLS config. Check with: curl -v https://speedtest.yourdomain.com 2>&1 | grep ALPN.
Port conflict: If another service (e.g., Caddy) is already using port 443, set LISTEN_IP in .env to bind Traefik to a specific IP address.
MIT