You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
OS: Alpine Linux (container), host is Ubuntu/Debian
Custom module: one custom Caddy middleware (document loader, unrelated to health checks)
Bug description
When using health_port to perform active health checks on a different port than the upstream's dial address, Caddy marks all upstreams as unhealthy (caddy_reverse_proxy_upstreams_healthy = 0) and returns 503 to all requests — even though the health endpoint is reachable and responds 200 OK when tested manually from inside the same container.
The upstreams never transition to healthy, regardless of how long Caddy runs.
Caddyfile (simplified, relevant parts)
The same reverse_proxy snippet is imported in multiple route blocks:
All upstreams show num_requests: 0 and fails: 0 (no traffic proxied, no passive failures). Combined with the Prometheus metrics above (upstreams_healthy = 0), this confirms the active health checker is the sole cause — it never marks any upstream as healthy.
Loaded JSON config (relevant excerpt)
The config loaded by Caddy confirms health_port is correctly parsed:
This block appears 4 times in the config (once per route).
Expected behavior
After a few health check cycles (interval is 5s), Caddy should mark the upstreams as healthy (1) and proxy traffic normally.
Actual behavior
All upstreams remain at 0 indefinitely. Every request returns 503 Service Unavailable.
Analysis and hypotheses
The health endpoint is reachable — proven by wget from inside the container, same network namespace, same DNS resolution.
The config is correctly loaded — "port": 8762 and "uri": "/health" appear in the JSON config.
Passive health checks are not the cause — fails: 0 for all upstreams (no traffic has been proxied).
Possible interaction between multiple handlers: the snippet creates 4 independent reverse_proxy handlers (one per route), each with its own health checker, all targeting the same 5 backends. The Prometheus gauge caddy_reverse_proxy_upstreams_healthy is keyed only by upstream address — could multiple handlers writing to the same gauge interfere? Or could 4 concurrent health checkers sharing the same HTTPTransport connection pool cause issues?
Possible regression of Bug in active health checker in reverse_proxy using health_port? #3691: the original health_port bug (fixed in v2.2.0) was caused by the transport's DialContext reading the dial address from the request context instead of the URL. The fix was to "construct a fresh HTTP transport for health checks." However, the current code (v2.11.1) appears to use h.Transport (the handler's transport) for health check requests. For health checks, there's no DialInfo in the context, so the custom DialContext should fall through to using the URL's address — but this warrants verification.
Environment
caddy:2.11.1-alpine)Bug description
When using
health_portto perform active health checks on a different port than the upstream's dial address, Caddy marks all upstreams as unhealthy (caddy_reverse_proxy_upstreams_healthy = 0) and returns503to all requests — even though the health endpoint is reachable and responds200 OKwhen tested manually from inside the same container.The upstreams never transition to healthy, regardless of how long Caddy runs.
Caddyfile (simplified, relevant parts)
The same
reverse_proxysnippet is imported in multiplerouteblocks:(proxy_config) { reverse_proxy 172.16.40.146:8761 172.16.40.147:8761 172.16.40.148:8761 172.16.40.157:8761 172.16.40.158:8761 { lb_policy header X-Document-ID health_uri /health health_port 8762 health_interval 5s health_timeout 3s lb_try_duration 5s lb_try_interval 250ms fail_duration 30s unhealthy_status 500 502 503 504 unhealthy_latency 30s transport http { dial_timeout 5s response_header_timeout 120s } } } :80 route /documents/*/layout { import proxy_config } route /documents/*/pages/* { import proxy_config } route /documents/*/file { import proxy_config } route { import proxy_config }Architecture
Each upstream host runs:
8761(the actual upstream for proxied traffic)8762— a lightweight Go HTTP server that returns:200 OK+{"status":"healthy"}when all co-located services are up503+{"status":"unhealthy","failing":[...]}when any service is downPort
8762is exposed on the host via Docker port mapping (-p 8762:8762).Steps to reproduce
reverse_proxywithhealth_port 8762andhealth_uri /healthas shown above200 OKon all backendscaddy_reverse_proxy_upstreams_healthy→ all0503 Service UnavailableEvidence: health endpoint works from inside the Caddy container
Evidence: Caddy reports all upstreams as unhealthy
Prometheus metrics:
Admin API
/reverse_proxy/upstreams:[ {"address":"172.16.40.147:8761","num_requests":0,"fails":0}, {"address":"172.16.40.148:8761","num_requests":0,"fails":0}, {"address":"172.16.40.157:8761","num_requests":0,"fails":0}, {"address":"172.16.40.158:8761","num_requests":0,"fails":0}, {"address":"172.16.40.146:8761","num_requests":0,"fails":0} ]All upstreams show
num_requests: 0andfails: 0(no traffic proxied, no passive failures). Combined with the Prometheus metrics above (upstreams_healthy = 0), this confirms the active health checker is the sole cause — it never marks any upstream as healthy.Loaded JSON config (relevant excerpt)
The config loaded by Caddy confirms
health_portis correctly parsed:This block appears 4 times in the config (once per route).
Expected behavior
After a few health check cycles (interval is 5s), Caddy should mark the upstreams as healthy (
1) and proxy traffic normally.Actual behavior
All upstreams remain at
0indefinitely. Every request returns503 Service Unavailable.Analysis and hypotheses
The health endpoint is reachable — proven by
wgetfrom inside the container, same network namespace, same DNS resolution.The config is correctly loaded —
"port": 8762and"uri": "/health"appear in the JSON config.Passive health checks are not the cause —
fails: 0for all upstreams (no traffic has been proxied).Possible interaction between multiple handlers: the snippet creates 4 independent
reverse_proxyhandlers (one per route), each with its own health checker, all targeting the same 5 backends. The Prometheus gaugecaddy_reverse_proxy_upstreams_healthyis keyed only byupstreamaddress — could multiple handlers writing to the same gauge interfere? Or could 4 concurrent health checkers sharing the sameHTTPTransportconnection pool cause issues?Possible regression of Bug in active health checker in reverse_proxy using health_port? #3691: the original
health_portbug (fixed in v2.2.0) was caused by the transport'sDialContextreading the dial address from the request context instead of the URL. The fix was to "construct a fresh HTTP transport for health checks." However, the current code (v2.11.1) appears to useh.Transport(the handler's transport) for health check requests. For health checks, there's noDialInfoin the context, so the customDialContextshould fall through to using the URL's address — but this warrants verification.Related issues
health_portbug where transport connected to wrong port (fixed in v2.2.0)/reverse_proxy/upstreamsshould expose active health check info