Dynamic, zero-touch reverse proxy configuration for Caddy on Incus, driven by instance lifecycle events.
caddy-config watches your Incus fleet via the ievent stream, renders Caddyfiles from instance labels in real time, validates them in-container, and deploys them directly into persistent storage volumes via Incus SFTP.
flowchart TD
Incus["Incus API (Events & Control)"] -->|instance events| Chain["ievent Chain<br/>(source → debounce → enricher)"]
Chain --> Plugin["caddy-config Plugin"]
subgraph Storage["Incus Storage Pool"]
Vol[("custom/caddy-config volume<br/>mounted at /config")]
end
subgraph Caddy["Serving Caddy Container"]
CaddyProc["caddy:2.11.4-alpine<br/>(admin localhost:2019)"]
end
Plugin -->|"1. SFTP write (.Caddyfile.tmp)"| Vol
Plugin -->|"2. Incus exec ('caddy validate')"| CaddyProc
Plugin -->|"3. SFTP atomic rename (.Caddyfile.tmp → Caddyfile)"| Vol
Plugin -->|"4. Incus exec ('caddy reload')"| CaddyProc
Define Caddy and your backend application in compose.yaml:
services:
caddy:
image: docker.io/library/caddy:2.11.4-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- caddy-config:/config
- caddy-data:/data
api:
image: docker.io/library/busybox:latest
command: httpd -f -p 8080
labels:
edge.domain: "api.example.com"
edge.upstream: "8080"
volumes:
caddy-config:
caddy-data:
When caddy-config runs with --caddy-instance edge:default:caddy, it automatically discovers api, resolves its Incus bridge IPv4 address, and renders the reverse proxy block into Caddy:
{
admin localhost:2019
}
api.example.com {
reverse_proxy 10.0.1.42:8080
}
When you scale, stop, start, or rename instances, Caddy reloads within milliseconds of the Incus event.
localhost:2019 inside the container. No admin port is mapped to the host or public network.conn.GetStoragePoolVolumeFileSFTP). If Caddy reboots or starts cold, it immediately boots with the current configuration./.Caddyfile.tmp, verifies syntax with caddy validate inside the container, and atomically swaps via sftp.PosixRename. Broken configurations are rejected before touching the active site.ChainCold). Deployments only run once the fleet sweep completes (ChainWarm), eliminating route churn on startup.--custom-templates-dir).