This guide walks through deploying Caddy alongside caddy-config on Incus using incus-compose.
/config to preserve configuration across reboots and recreations.caddy-config communicates with the Incus daemon over HTTPS. You can enroll it using a one-time trust token:
incus config trust add caddy-config
Incus outputs a trust token (e.g. eyJzZXJ2ZXJfbmFtZSI6...). Save this token into a .env file in the same directory as your compose.yaml:
echo "INCUS_TOKEN=eyJzZXJ2ZXJfbmFtZSI6..." > .env
incus-compose automatically loads .env files and injects secrets into the stack without requiring --os-env.
compose.yamlCreate a compose.yaml file defining the Caddy proxy and caddy-config:
services:
caddy:
image: docker.io/library/caddy:2.11.4-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- config:/config
- data:/data
command: >-
sh -c '
if [ ! -f /config/Caddyfile ]; then
echo "{\n\tadmin localhost:2019\n}\n:80 {\n\trespond \"Caddy initializing...\" 503\n}\n" > /config/Caddyfile;
fi;
exec caddy run --config /config/Caddyfile --adapter caddyfile'
caddy-config:
image: ghcr.io/jochumdev/incus-caddy-config/caddy-config:1.0.0-beta.1
restart: unless-stopped
ports:
- "9153:9153"
environment:
INCUS_CADDY_INCUS: https://10.0.1.1:8443
INCUS_CADDY_DATA_DIR: /var/lib/caddy-config
INCUS_CADDY_INSTANCES: "edge:default:caddy"
INCUS_CADDY_HTTP_ADDRESS: ":9153"
INCUS_CADDY_LOG: "INFO"
secrets:
- token
volumes:
- caddy-config:/var/lib/caddy-config
secrets:
token:
environment: INCUS_TOKEN
volumes:
config:
data:
caddy-config:
Launch the stack:
incus-compose up -d
On first start:
caddy-config reads the secret token from /run/secrets/token.config-data volume at /var/lib/caddy-config/client.crt and client.key. Future restarts reuse the certificate without requiring a token.To expose any container or virtual machine through Caddy, add edge.domain and edge.upstream labels:
services:
api:
image: docker.io/library/busybox:latest
command: httpd -f -p 8080
labels:
edge.domain: "api.example.test"
edge.upstream: "8080"
Deploy the service:
incus-compose up -d api
caddy-config immediately catches the instance-started event, resolves the instance's bridge IP address, writes the updated configuration directly to the caddy-config storage volume, validates syntax inside Caddy, and executes a reload.
Verify that Caddy routes traffic to the new service:
curl -H "Host: api.example.test" http://127.0.0.1/
caddy-config exposes observability endpoints on port 9153:
GET http://localhost:9153/health): Returns HTTP 200 once the HTTP server is listening.GET http://localhost:9153/ready):
ChainWarm).ChainCold) or disconnected.curl -i http://localhost:9153/ready
If you run Caddy directly on the host OS or inside a VM alongside caddy-config, use --os-path instead of --caddy-instance:
# Listen to Incus and update local Caddyfile directly
caddy-config run \
--os-path edge:/etc/caddy/Caddyfile \
--remote default
caddy-config validates the syntax and reloads the local Caddy daemon directly on the host filesystem.