# Caddy Docker deployment for Caddy with the Cloudflare DNS module. Run all commands from the repository root. ## Persistent Data Caddy stores certificates, ACME state, and autosaved configuration in the Docker-managed named volumes `caddy_data` and `caddy_config`. Named volumes remain writable when Docker uses rootless or user-namespace UID mapping. ## Installation 1. Create `.env` if it does not exist: ```bash cp -n .env.sample .env ``` 2. Set the required values in `.env`: ```dotenv TZ=Africa/Johannesburg CADDY_TLS_EMAIL=admin@example.com CADDY_BASE_DOMAIN=home.example.com CLOUDFLARE_DNS_TOKEN= CADDY_LAN_PREFIX=10.0.1 CADDY_OFFICE_PREFIX=10.0.10 CADDY_IOT_PREFIX=10.0.30 DNS_BIND_IP=10.0.10.5 CADDY_MEM_LIMIT=256m DOCKERLOGGING_MAXFILE=3 DOCKERLOGGING_MAXSIZE=10m ``` The Cloudflare token needs zone read and DNS edit permissions. 3. Render the templates: ```bash ./substitute_env.sh docker-compose-files/caddy/Caddyfile_template docker-compose-files/caddy/Caddyfile .env ./substitute_env.sh docker-compose-files/caddy/template.yaml docker-compose.caddy.yml .env chmod 644 docker-compose-files/caddy/Caddyfile ``` Edit `Caddyfile_template`, not the generated `Caddyfile`. `docker-compose.caddy.yml` contains the rendered token and is ignored by Git. Keep that generated Compose file private. The generated `Caddyfile` does not contain the Cloudflare token; it must be readable by Caddy through the read-only bind mount. 4. Validate and start Caddy: ```bash docker compose -f docker-compose.caddy.yml config --quiet docker compose -f docker-compose.caddy.yml build docker compose -f docker-compose.caddy.yml run --rm caddy caddy validate --config /etc/caddy/Caddyfile docker compose -f docker-compose.caddy.yml up -d ``` 5. Verify the container: ```bash docker compose -f docker-compose.caddy.yml ps docker compose -f docker-compose.caddy.yml logs --tail=100 caddy ``` ## Apply Configuration Changes After changing `.env` or `template.yaml`, render the Compose file and recreate the container: ```bash ./substitute_env.sh docker-compose-files/caddy/template.yaml docker-compose.caddy.yml .env docker compose -f docker-compose.caddy.yml config --quiet docker compose -f docker-compose.caddy.yml up -d --build ``` After changing `Caddyfile_template`, render, validate, and reload it: ```bash ./substitute_env.sh docker-compose-files/caddy/Caddyfile_template docker-compose-files/caddy/Caddyfile .env chmod 644 docker-compose-files/caddy/Caddyfile docker compose -f docker-compose.caddy.yml exec caddy caddy validate --config /etc/caddy/Caddyfile docker compose -f docker-compose.caddy.yml exec caddy caddy reload --config /etc/caddy/Caddyfile ``` ## Initial Setup In your internal DNS server: 1. Add an `A` record named `caddy` pointing to the Caddy host at `10.0.15.5`. 2. Add a wildcard `CNAME` named `*` pointing to `caddy.home.example.com`. Replace `home.example.com` with `CADDY_BASE_DOMAIN`. ## Network Requirements - Caddy uses host networking and listens on ports `80` and `443`. - Allow Caddy to reach every upstream IP and port in `Caddyfile_template`. - Allow outbound DNS and HTTPS for Cloudflare DNS-01 certificate issuance. - Exclude the Caddy host from DNS redirection so it can query public resolvers. - The admin API listens only on `localhost:2019`. ### Client VLAN Access For client VLANs to reach Caddy over HTTPS, add a firewall allow rule for each VLAN that needs access. In UniFi **Settings > Policy Engine > Policy Table**: | Name | Source zone | Destination zone | Destination IP | Protocol | Destination port | |---|---|---|---|---|---| | `Allow FAMILY to Caddy` | `FAMILY` | `DMZ` | `10.0.15.5` | TCP | `443` | | `Allow MEDIA to Caddy` | `MEDIA` | `DMZ` | `10.0.15.5` | TCP | `443` | Add further rows for any other zone that needs access to internal services via Caddy. No Caddyfile changes are required — the wildcard DNS record in Technitium (`*.home.example.com` → `caddy.home.example.com` → `10.0.15.5`) handles resolution automatically for all VLANs that use Technitium as their DNS server. ## Troubleshooting Validate the generated Caddyfile and inspect container logs: ```bash chmod 644 docker-compose-files/caddy/Caddyfile docker compose -f docker-compose.caddy.yml run --rm caddy caddy validate --config /etc/caddy/Caddyfile docker compose -f docker-compose.caddy.yml logs --tail=100 caddy ``` If validation reports `open /etc/caddy/Caddyfile: permission denied`, the generated host file is not readable by the container's mapped user. Confirm its mode with `ls -l docker-compose-files/caddy/Caddyfile`, then apply the `chmod 644` command above. Do not make `docker-compose.caddy.yml` world-readable because it contains the rendered Cloudflare token. If logs report `mkdir /data/caddy: permission denied` or `mkdir /config/caddy: permission denied`, re-render `docker-compose.caddy.yml` from the current template and recreate Caddy. The current template uses Docker-managed named volumes so rootless/user-namespace UID mapping does not prevent writes: ```bash ./substitute_env.sh docker-compose-files/caddy/template.yaml docker-compose.caddy.yml .env docker compose -f docker-compose.caddy.yml down docker compose -f docker-compose.caddy.yml up -d --build docker compose -f docker-compose.caddy.yml logs --tail=100 caddy ``` If certificate issuance fails, confirm the Cloudflare token permissions and ensure Caddy's public DNS queries are not redirected to internal DNS. ### Raspberry Pi OS Memory Limits If Docker reports the following warning, Caddy still starts, but Docker ignores `CADDY_MEM_LIMIT`: ```text Your kernel does not support memory limit capabilities or the cgroup is not mounted. Limitation discarded. ``` Enable memory cgroups by appending these parameters to the existing single line in `/boot/firmware/cmdline.txt`: ```text cgroup_enable=memory cgroup_memory=1 ``` Older Raspberry Pi OS releases may use `/boot/cmdline.txt`. Do not add a new line to either file. Reboot and verify: ```bash sudo reboot docker info | grep -i warning docker stats --no-stream caddy ``` ## Useful Commands ```bash # Confirm the Cloudflare module is installed docker compose -f docker-compose.caddy.yml exec caddy caddy list-modules | grep dns.providers.cloudflare # Rebuild after image or Dockerfile updates docker compose -f docker-compose.caddy.yml build --pull --no-cache docker compose -f docker-compose.caddy.yml up -d ``` ## Backup Back up both named volumes. The data volume contains certificates and ACME state: ```bash mkdir -p backups/caddy docker run --rm \ -v homelab-blueprint_caddy_data:/source:ro \ -v "$PWD/backups/caddy:/backup" \ alpine:3.20 tar -czf /backup/caddy-data.tar.gz -C /source . docker run --rm \ -v homelab-blueprint_caddy_config:/source:ro \ -v "$PWD/backups/caddy:/backup" \ alpine:3.20 tar -czf /backup/caddy-config.tar.gz -C /source . ``` Confirm the volume keys with `docker compose -f docker-compose.caddy.yml config --volumes` and the actual Docker volume names with `docker volume ls` before backup if the Compose project name differs from `homelab-blueprint`.