Deployment & Operations

This guide covers operational practices for running caddy-config in production environments.


Production compose.yaml Stack

Create 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:

Purpose of Each Volume

  1. 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.
  2. data:/data: Persistent volume where Caddy stores automatic TLS certificates (from Let's Encrypt / ZeroSSL) and OCSP stapling cache.
  3. 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.

Reboot & Lifecycle Behavior

Container Restarts & Host Reboots

Because /config is backed by a custom storage volume:

Offline / Cold Reconciliations

If Caddy is temporarily stopped (e.g. during maintenance or image upgrades):

  1. caddy-config detects the container is stopped.
  2. It resolves the storage volume mounted at /config and writes the updated Caddyfile directly to the volume via SFTP.
  3. Because Caddy is not running, in-container validation and reload are skipped.
  4. When Caddy starts up, it reads the updated configuration from the storage volume on its first boot cycle.

Multi-Project Routing

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

Multiple Caddy Servers

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"

Operational Troubleshooting

1. View Current Deployed Caddyfile

Inspect the active configuration directly inside the Caddy container:

incus-compose exec caddy cat /config/Caddyfile

2. Check Controller Logs

Increase log verbosity using INCUS_CADDY_LOG=DEBUG or TRACE:

incus-compose logs -f caddy-config

DEBUG logs display:

3. Test In-Container Formatting & Validation Manually

If you suspect a configuration syntax issue:

incus-compose exec caddy caddy fmt /config/Caddyfile

4. Common Error Scenarios