homelab-blueprint/docker-compose-files/caddy
Brian Pooe 1815beadf4 refactor: restructure docs, sync stacks with Caddyfile, harden compose templates
- Add compose stacks for apps already proxied by Caddy: bento-pdf,
  it-tools, paperless-ngx, and beszel-hub (with Makefile targets)
- Remove the unused Vault stack and the drawio Caddyfile block
- Fix Caddyfile gramps upstream to port 80 to match the compose file
- Bring immich and gramps-web up to the shared template pattern
  (no-new-privileges, log rotation, memory limits, healthchecks)
- Single-quote vaultwarden SSO_AUDIENCE_TRUSTED so regex values like
  ^\d{18}$ render as valid YAML
- Move root docs into kebab-case topic folders, rebuild the docs index
  to cover every doc, and fix all broken links
2026-07-03 17:45:16 +02:00
..
.env.sample chore: track .env.sample files for all stacks 2026-07-02 22:28:44 +02:00
Caddyfile_template refactor: restructure docs, sync stacks with Caddyfile, harden compose templates 2026-07-03 17:45:16 +02:00
Dockerfile Add Raspberry Pi Caddy deployment 2026-06-13 17:25:27 +02:00
README.md Use named volumes for application state 2026-06-15 20:07:48 +02:00
template.yaml Use named volumes for application state 2026-06-15 20:07:48 +02:00

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:

    cp -n .env.sample .env
    
  2. Set the required values in .env:

    TZ=Africa/Johannesburg
    CADDY_TLS_EMAIL=admin@example.com
    CADDY_BASE_DOMAIN=home.example.com
    CLOUDFLARE_DNS_TOKEN=<restricted-cloudflare-api-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:

    ./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:

    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:

    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:

./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:

./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.comcaddy.home.example.com10.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:

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:

./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:

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:

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:

sudo reboot
docker info | grep -i warning
docker stats --no-stream caddy

Useful Commands

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

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.