homelab-blueprint/docker-compose-files/caddy/README.md
2026-06-15 20:07:48 +02:00

211 lines
7.1 KiB
Markdown

# 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=<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:
```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`.