"Caddy Split-Horizon Conflict: Too Many Redirects with Cloudflare Tunnel on Port 80"

1. The problem I’m having:

I am running a split-horizon reverse proxy setup in a single Caddy container for internal (LAN) and external (Cloudflare Tunnel) traffic.

  • Internal Access (LAN): Works perfectly over HTTPS. LAN clients use a local DNS record to resolve jf.mydom.com to Caddy’s container IP (192.168.4.99) on Port 443. Status 200.
  • External Access (Cloudflare Tunnel): Fails with a “Too Many Redirects” error. The Cloudflare Tunnel sends plain HTTP traffic to Caddy’s Port 80. Crucially, Caddy shows no log entries for these external requests, indicating the redirect loop is happening before Caddy successfully proxies the request, or the request is immediately hitting a non-existent implicit redirect.

Key Context: I have a separate, working dedicated Caddy instance for another domain (myotherdom.com) that only handles external Cloudflare Tunnel traffic on Port 80, and it works perfectly using the exact same proxy headers (X-Forwarded-Proto https). This confirms the issue is not with Cloudflare, the Tunnel, the backend apps (Jellyfin, Sonarr, Radarr), or the X-Forwarded-Proto header, but a conflict within the single split-horizon Caddyfile.

2. Error messages and/or full log output:

The Caddy logs show no entry for external access attempts, despite the level DEBUG setting. Caddy is simply not logging the request, yet the browser shows “Too Many Redirects.”

A log snippet for a working internal access attempt is provided for comparison.

{"level":"info","ts":1762661624.0417576,"msg":"adapted config to JSON","adapter":"caddyfile"}
{"level":"info","ts":1762661624.045819,"logger":"http.log","msg":"server running","name":"srv1","protocols":["h1","h2","h3"]}
{"level":"info","ts":1762661624.0460715,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}

# --- LOG SNIPPET FOR WORKING INTERNAL (HTTPS, Port 443) ---
{"level":"debug","ts":1762660698.438884,"logger":"http.handlers.reverse_proxy","msg":"selected upstream","dial":"192.168.4.60:8989","total_upstreams":1}
{"level":"debug","ts":1762660698.4394803,"logger":"http.handlers.reverse_proxy","msg":"upstream roundtrip","upstream":"192.168.4.60:8989","duration":0.000546789,"request":{"remote_ip":"192.168.1.10","remote_port":"31098","client_ip":"192.168.1.10","proto":"HTTP/2.0","method":"GET","host":"sarr.mydom.com","uri":"/","headers":{"X-Forwarded-Host":["sarr.mydom.com"],"X-Forwarded-For":["192.168.1.10"],"X-Forwarded-Proto":["https"],"Via":["2.0 Caddy"]},"tls":{"resumed":false,"version":772,"cipher_suite":4865,"proto":"h2","server_name":"sarr.mydom.com"}},"status":200}

3. Caddy version:

v2.10.2

4. How I installed and ran Caddy:

a. System environment:

docker run -d
–name caddy
–network custom_bridge_network
-p 80:80
-p 443:443
-v /path/to/Caddyfile:/etc/caddy/Caddyfile
-v caddy_data:/data
caddy/caddy:latest

b. Command:

c. Service/unit/compose file:

d. My complete Caddy config:

{
    email {$ACME_EMAIL}
    acme_dns cloudflare {$CLOUDFLARE_API_TOKEN}
    servers {
        trusted_proxies static 192.168.0.0/16 
    }
    log { 
        level DEBUG 
    } 
}

# Snippet for external Caddy (hardcoded HTTPS from Cloudflare Tunnel)
(proxy_headers_https) {
    header_up X-Real-IP {remote_host}
    header_up X-Forwarded-For {remote_host}
    header_up X-Forwarded-Proto https
    header_up X-Forwarded-Host {host}
}

# =========================================================
# 1. EXTERNAL / TUNNEL BLOCK (HTTP / Port 80) - NO REDIRECTS
#    - This ONLY proxies traffic coming from the Cloudflare Tunnel.
# =========================================================
:80 {
    @jellyfin host jf.mydom.com
    handle @jellyfin {
        reverse_proxy 192.168.4.40:8096 {
            import proxy_headers_https
        }
    }

    @sonarr host sarr.mydom.com
    handle @sonarr {
        reverse_proxy 192.168.4.60:8989 {
            import proxy_headers_https
        }
    }

    @radarr host rarr.mydom.com
    handle @radarr {
        reverse_proxy 192.168.4.62:7878 {
            import proxy_headers_https
        }
    }
}

# =========================================================
# 2. INTERNAL / LAN ACCESS BLOCK (HTTPS / Port 443)
#    - This handles LAN clients using HTTPS directly.
# =========================================================
jf.mydom.com, sarr.mydom.com, rarr.mydom.com {
    
    tls {
        dns cloudflare {$CLOUDFLARE_API_TOKEN}
		resolvers 1.1.1.1, 8.8.8.8 
    }
    
    @jellyfin host jf.mydom.com
    @sonarr host sarr.mydom.com
    @radarr host rarr.mydom.com

    reverse_proxy @jellyfin 192.168.4.40:8096
    reverse_proxy @sonarr 192.168.4.60:8989
    reverse_proxy @radarr 192.168.4.62:7878
}

5. Links to relevant resources:

  • Cloudflare Trace shows the external client is requesting HTTPS (visit_scheme=https).
  • The Cloudflare Tunnel’s ingress for these hosts points to http://caddy_container_ip:80.
  • A previous Caddy log confirmed the Cloudflare Tunnel source IP is 192.168.4.8. Attempts to use a remote_ip 192.168.4.8/32 matcher also resulted in the redirect loop.
  • The backend applications are running on: 192.168.4.40, 192.168.4.60, and 192.168.4.62.

By default, Caddy redirects all HTTP traffic to HTTPS. So if Cloudflare is forwarding traffic to Caddy over HTTP, Caddy will try to redirect it to HTTPS, causing your browser to go to Cloudflare again, which then sends it back to Caddy over HTTP, and the cycle repeats. That’s what’s causing the redirect loop.

You’ve got two ways to fix it:

  1. Forward traffic from Cloudflare to Caddy using HTTPS, or
  2. Disable Caddy’s automatic HTTP-to-HTTPS redirects

Thanks. That sorted everything out.

I knew I’d feel like an idiot once I found it what it was.

1 Like

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.