# elevenlabs.io/Caddyfile # Outbound API gateway. Two listeners, one shared route set: # :8080 — plain HTTP (published as 8082 on the host, legacy clients) # {$DOMAIN} — HTTPS 443 with automatic Let's Encrypt certificates { email {$ACME_EMAIL} admin off # The error log dumps request headers, and Caddy redacts only # Authorization/Cookie on its own: X-Proxy-Token used to land in # `docker logs` in clear text on every failed upstream call. log default { format filter { wrap json fields { request>headers>X-Proxy-Token delete } } } } # --- Access control (generated by docker-entrypoint.sh from env) --- # matchers.caddy: @denied_ip / @bad_token definitions (empty when disabled) # guard.caddy: matching respond directives (empty when disabled) # scrub_extra.caddy: strips the optional auth key header from upstream requests # --- Shared upstream request hygiene --- (scrub) { header_up Host {args[0]} header_up -X-Forwarded-For header_up -X-Forwarded-Proto header_up -X-Forwarded-Host header_up -X-Real-IP header_up -True-Client-IP header_up -CF-Connecting-IP header_up -X-Client-IP header_up -Forwarded header_up -Via header_up -X-Proxy-Token import /etc/caddy/scrub_extra.caddy } # (api) (api) { handle_path /{args[0]}/* { rewrite * {args[2]}{uri} reverse_proxy https://{args[1]} { import scrub {args[1]} flush_interval -1 transport http { dial_timeout 15s response_header_timeout 300s read_timeout 300s } } } } # --- Proxy-originated errors --- # Errors produced by the proxy itself (the upstream answered nothing) are # marked with X-Proxy-Error so that a client with several proxies can tell # them apart from an upstream's own 5xx and fail over: # dial — the upstream was never reached (DNS, connect, no route): the # request did not leave this host, retrying elsewhere is safe # even for non-idempotent calls # upstream — anything else (timeouts, broken responses): the upstream may # have processed the request # X-Proxy-Error-Detail carries the reason. Only clients that passed the # guards ever get here (the guards respond directly, without an error). # Note: handled errors are logged by Caddy at DEBUG level only — the reason # is visible to the client via X-Proxy-Error-Detail. (proxy_errors) { handle_errors { @dial `{err.message}.startsWith("dial ")` handle @dial { header X-Proxy-Error dial header X-Proxy-Error-Detail "{err.message}" respond `{"error":"proxy_dial_failed"}` {err.status_code} } handle { header X-Proxy-Error upstream header X-Proxy-Error-Detail "{err.message}" respond `{"error":"proxy_upstream_failed"}` {err.status_code} } } } (routes) { import /etc/caddy/matchers.caddy import proxy_errors route { # Health check — no auth, evaluated before the guards respond /health `{"status":"ok","version":"2.0"}` 200 import /etc/caddy/guard.caddy import api elevenlabs api.elevenlabs.io "" import api openai api.openai.com "" import api telegram api.telegram.org "" import api deepseek api.deepseek.com "" import api openrouter openrouter.ai /api/v1 respond `{"error":"unknown_upstream","hint":"use /elevenlabs/, /openai/, /telegram/, /openrouter/ or /deepseek/"}` 404 } } {$DOMAIN} { import routes } :8080 { import routes }