This guide covers operational practices for running caddy-config in production environments.
compose.yaml StackCreate a .env file alongside compose.yaml to provide the trust token (no --os-env needed):
echo "INCUS_TOKEN=$(incus config trust add --quiet caddy-config)" > .env
Here is the recommended production stack deploying Caddy alongside 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 -e "{\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
depends_on:
caddy:
condition: service_healthy
ports:
- "9153:9153"
environment:
INCUS_CADDY_INCUS: "${INCUS_CADDY_INCUS:-https://10.0.1.1:8443}"
INCUS_CADDY_DATA_DIR: /var/lib/caddy-config
INCUS_CADDY_INSTANCES: "edge,instance=caddy-1,project=default"
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:
config:/config:
Persistent Incus custom storage volume holding /config/Caddyfile. caddy-config connects directly to this volume over SFTP to write and update the configuration.data:/data:
Persistent volume where Caddy stores automatic TLS certificates (from Let's Encrypt / ZeroSSL) and OCSP stapling cache.caddy-config:/var/lib/caddy-config:
Stores caddy-config's enrolled client TLS certificate (client.crt, client.key) so trust tokens are only needed once.Because /config is backed by a custom storage volume:
incus-compose restart caddy) or the Incus host reboots, Caddy immediately starts up using the persisted Caddyfile on the volume.caddy-config to initialize.If Caddy is temporarily stopped (e.g. during maintenance or image upgrades):
caddy-config detects the container is stopped./config and writes the updated Caddyfile directly to the volume via SFTP.By default, caddy-config monitors all visible Incus projects. You can restrict monitoring to specific projects:
caddy-config run \
--project prod \
--project staging \
--caddy-instance edge,instance=caddy,project=default
You can route different services to different Caddy instances using separate label prefixes (via repeated flags or comma-separated):
# Repeated flags
caddy-config run \
--caddy-instance public,instance=caddy-external,project=default \
--caddy-instance internal,instance=caddy-internal,project=default
# Or comma-separated
caddy-config run \
--caddy-instance "public,instance=caddy-external,project=default,internal,instance=caddy-internal,project=default"
user.label.public route to caddy-external.user.label.internal route to caddy-internal.Inspect the active configuration directly inside the Caddy container:
incus-compose exec caddy cat /config/Caddyfile
Increase log verbosity using INCUS_CADDY_LOG=DEBUG or TRACE:
incus-compose logs -f caddy-config
DEBUG logs display:
instance-started, instance-stopped, instance-renamed).caddy fmt exit codes and output.If you suspect a configuration syntax issue:
incus-compose exec caddy caddy fmt /config/Caddyfile
opening SFTP session for volume ... not found:
Verify that Caddy's /config directory is backed by a named storage volume in compose.yaml.executing caddy reload: connection refused:
Verify that Caddy's global block includes { admin localhost:2019 }. If admin off was specified, Caddy cannot process reload signals over its internal Admin API.readiness 503 Service Unavailable:
Indicates the chain is still performing the initial fleet sweep or is disconnected from the Incus API. Check network connectivity to Incus.