The kind=static branch added in #342 put handle_errors inside the @host handle{} block. handle_errors is a top-level site-block directive, not an ordered HTTP handler, so Caddy refuses to load the config: parsing caddyfile tokens for 'handle': directive 'handle_errors' is not an ordered HTTP handler This crash-loops the whole reverse proxy and takes down every *.ops.eblu.me service. Tripped today during the live cv/docs cutover. Fix: drop handle_errors and append /404.html as the final try_files candidate. The 404 page is served with status 200 instead of 404, but that's acceptable for a human-facing curated 404 — the page renders correctly. Documented inline. The running Caddy on indri already has the fixed config (deployed manually during the cutover); this lands the fix in main so future provision-indri --tags caddy runs don't re-break it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
87 lines
2.7 KiB
Django/Jinja
87 lines
2.7 KiB
Django/Jinja
# Caddy reverse proxy for blumeops services
|
|
# Managed by ansible - do not edit manually
|
|
#
|
|
# All *.{{ caddy_domain }} requests are proxied to backend services.
|
|
# TLS certificates are obtained via ACME DNS-01 challenge using Gandi.
|
|
|
|
{
|
|
# Global options
|
|
admin off
|
|
|
|
{% if caddy_tcp_services %}
|
|
# Layer 4 (TCP) routing
|
|
layer4 {
|
|
{% for tcp_svc in caddy_tcp_services %}
|
|
:{{ tcp_svc.port }} {
|
|
route {
|
|
proxy {{ tcp_svc.backend }}
|
|
}
|
|
}
|
|
{% endfor %}
|
|
}
|
|
{% endif %}
|
|
}
|
|
|
|
# Wildcard certificate for all services
|
|
*.{{ caddy_domain }}:{{ caddy_https_port }} {
|
|
tls {
|
|
dns gandi {env.GANDI_BEARER_TOKEN}
|
|
}
|
|
|
|
{% for service in caddy_services %}
|
|
@{{ service.name }} host {{ service.host }}
|
|
handle @{{ service.name }} {
|
|
{% if service.kind | default('proxy') == 'static' %}
|
|
root * {{ service.root }}
|
|
encode gzip
|
|
# Long-cache fingerprinted assets; everything else stays default.
|
|
@{{ service.name }}_assets path_regexp \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$
|
|
header @{{ service.name }}_assets Cache-Control "public, max-age=31536000, immutable"
|
|
{% for dl in service.download_paths | default([]) %}
|
|
@{{ service.name }}_dl{{ loop.index }} path {{ dl.path }}
|
|
header @{{ service.name }}_dl{{ loop.index }} Content-Disposition `attachment; filename="{{ dl.filename }}"`
|
|
{% endfor %}
|
|
{% if service.try_html | default(false) %}
|
|
# Quartz clean URLs: path → path/ → path.html → /404.html (200).
|
|
# Caddy's handle_errors is a top-level directive and can't live in
|
|
# this nested handle, so the 404 page rides as the final try_files
|
|
# candidate (served with 200 — acceptable for a human-facing 404).
|
|
try_files {path} {path}/ {path}.html /404.html
|
|
{% endif %}
|
|
file_server
|
|
{% else %}
|
|
{% if service.cache_policy | default('') == 'spa' %}
|
|
# SPA cache policy: hashed static assets are immutable, HTML must revalidate.
|
|
# Prevents stale HTML from referencing chunk hashes that no longer exist.
|
|
@{{ service.name }}_static path /static/dist/*
|
|
header @{{ service.name }}_static Cache-Control "public, max-age=31536000, immutable"
|
|
@{{ service.name }}_html path /if/*
|
|
header @{{ service.name }}_html Cache-Control "no-cache"
|
|
{% endif %}
|
|
{% if service.backend.startswith('https://') %}
|
|
reverse_proxy {{ service.backend }} {
|
|
# Caddy v2.11+ rewrites Host to upstream for HTTPS backends.
|
|
# Preserve the original Host so services see *.ops.eblu.me.
|
|
header_up Host {http.request.host}
|
|
}
|
|
{% else %}
|
|
reverse_proxy {{ service.backend }}
|
|
{% endif %}
|
|
{% endif %}
|
|
}
|
|
|
|
{% endfor %}
|
|
# Fallback for unknown hosts
|
|
handle {
|
|
respond "Unknown service" 404
|
|
}
|
|
}
|
|
|
|
# Base domain (ops.eblu.me)
|
|
{{ caddy_domain }}:{{ caddy_https_port }} {
|
|
tls {
|
|
dns gandi {env.GANDI_BEARER_TOKEN}
|
|
}
|
|
|
|
respond "blumeops services - use a subdomain (e.g., forge.{{ caddy_domain }})"
|
|
}
|