fix: drop k3s-traefik priority, wrap caddy web reverse_proxy in handle

The k3s-traefik IngressRoute was emitting priority: len(path) on the route rule but no priority on the bare-Host catch-all. Traefik defaults missing priority to rule length, so the catch-all rule got ~32 while the path rule got 7, and the catch-all always won. Same exact priority bug we fixed for docker-traefik. Dropping the priority line lets traefik use its rule-length default which gives longest-prefix-wins naturally because Host && PathPrefix is always longer than bare Host. The caddy web container's emission was a bare reverse_proxy directive that runs at caddy default order ~190, well before the handle directive at ~210 where my route emission lives. The bare reverse_proxy caught every request including paths intended for the route. When at least one route exists for the app, the web container now wraps its reverse_proxy in a catch-all handle block via caddy.9_handle.0_reverse_proxy=...; both the route's handle (at 0_) and web's catch-all (at 9_) live at the same directive order ~210 and caddy's mutually-exclusive handle logic picks the route handle for matched paths and the catch-all for everything else. The "9_" prefix orders web's catch-all after the route handles in the docker-proxy generated Caddyfile. Apps without routes keep the original bare reverse_proxy behaviour.
This commit is contained in:
Jose Diaz-Gonzalez
2026-06-03 03:56:37 -04:00
parent 3449c11a59
commit 123fdc700c
3 changed files with 31 additions and 11 deletions

View File

@@ -100,6 +100,21 @@ trigger-caddy-vhosts-docker-args-process-deploy() {
caddy_domains="${caddy_domains// /, }"
fi
# If any path-based route exists for this app, the route emission on the
# non-web container puts a `handle @<slug>` block in the site. For caddy to
# honor that, web's `reverse_proxy` must also be wrapped in a `handle`
# block (both at directive order ~210) so caddy's mutually-exclusive
# handle logic picks the route handle for matched paths and falls through
# to the catch-all handle for everything else. Without this, web's bare
# reverse_proxy (~190) catches every request before any handle runs.
local has_routes=false
while IFS='|' read -r route_proc _ _ _; do
if [[ -n "$route_proc" ]]; then
has_routes=true
break
fi
done < <(plugn trigger proxy-routes-list "$APP")
# add the labels for caddy here
# prefer the https:443 mapping to http:80 mapping
if [[ "$is_app_listening" == "true" ]] && [[ -n "$caddy_domains" ]]; then
@@ -133,7 +148,14 @@ trigger-caddy-vhosts-docker-args-process-deploy() {
proxy_container_https_port="$proxy_container_https_port_candidate"
fi
output="$output --label \"caddy.reverse_proxy={{ upstreams $proxy_container_https_port }}\""
if [[ "$has_routes" == "true" ]]; then
# The "9_" prefix orders web's catch-all handle after the route
# handles (which the non-web container emits with "0_") in the
# docker-proxy generated Caddyfile.
output="$output --label \"caddy.9_handle.0_reverse_proxy={{ upstreams $proxy_container_https_port }}\""
else
output="$output --label \"caddy.reverse_proxy={{ upstreams $proxy_container_https_port }}\""
fi
elif [[ -n "$proxy_container_http_port" ]] || [[ -n "$proxy_container_http_port_candidate" ]]; then
caddy_domains="${caddy_domains//, /:80, }"
output="--label 'caddy=${caddy_domains}:80'"
@@ -143,7 +165,11 @@ trigger-caddy-vhosts-docker-args-process-deploy() {
proxy_container_http_port="$proxy_container_http_port_candidate"
fi
output="$output --label \"caddy.reverse_proxy={{ upstreams $proxy_container_http_port }}\""
if [[ "$has_routes" == "true" ]]; then
output="$output --label \"caddy.9_handle.0_reverse_proxy={{ upstreams $proxy_container_http_port }}\""
else
output="$output --label \"caddy.reverse_proxy={{ upstreams $proxy_container_http_port }}\""
fi
fi
if [[ "$output" == *"caddy.reverse_proxy="* ]]; then

View File

@@ -41,7 +41,9 @@ spec:
{{- range $rdx, $route := $.Values.global.network.routes }}
- kind: Rule
match: Host(`{{ $domain.name }}`) && PathPrefix(`{{ $route.path }}`)
priority: {{ len $route.path }}
# No explicit priority - traefik's default (rule length) gives
# longest-prefix-wins because Host(...) && PathPrefix(...) is always
# longer than web's bare Host(...) rule below.
middlewares:
- name: {{ $.Values.global.app_name}}-{{ $processName }}-compression
namespace: {{ $.Values.global.namespace }}

View File

@@ -45,14 +45,6 @@ teardown() {
# Give the daemon a moment to read the new labels.
sleep 5
# Surface the generated Caddyfile so we can see how caddy-docker-proxy
# merged the web + api containers' labels and figure out why /api/v0/*
# routing isn't taking effect.
run /bin/bash -c "docker exec caddy-caddy-1 cat /etc/caddy/Caddyfile 2>&1"
echo "caddy config: $output"
run /bin/bash -c "docker exec caddy-caddy-1 caddy adapt --config /etc/caddy/Caddyfile 2>&1 | head -200"
echo "caddy adapt json: $output"
# Without --strip-prefix, /api/v0/Procfile reaches api as /api/v0/Procfile
# which python's http.server returns 404 for. A 404 here proves the route
# reaches api - web's catch-all would have returned 200.