1) exec
Resolve a placeholder by running a command when the config (re)loads.
route /api* {
reverse_proxy localhost:{exec: mise -C /srv/myapp x -- printenv APP_PORT}
}
Notes: opt‑in; short timeout; non‑zero exit fails the reload; value is cached into the active config.
2) var
Provide first‑class variables addressable as {var.NAME}, set via Caddyfile, JSON import, or the Admin API.
vars {
import /etc/caddy/app-ports.json
api_port 8039
}
route /api* {
reverse_proxy localhost:{var.api_port}
}
/etc/caddy/app-ports.json:
{ "api_port": 8039, "billing_port": 9010 }
(Also allow setting vars via Admin API for tooling/CI.)
Why?
Coordinating ports is high‑friction:
- The Admin API isn’t for everyone
- Hard‑coding duplicates truth across systemd/compose/scripts and the Caddyfile; renumbering is tedious and error‑prone.
{env.*}and{file.*}push per‑app coordination onto Caddy (inject envs or create lots of tiny files).- Unix sockets avoids ports but isn’t always available (multi‑host, third‑party services, Windows, legacy).
The suggested placeholders solve the problem elegantly. I lean towards vars since I think implementation would be easier. I would use it by adding a pre-init hook to my app servers that sets/updates the JSON vars file with the current port.