Skip to content

Caddy — image, compose, Caddyfile and operation

Extracted from the master document on 2026-09-04. §N numbering kept so as not to break the cross-references.

The official Caddy image does not ship the Cloudflare DNS module that DNS-01 requires, so it is built:

/opt/stacks/caddy/Dockerfile
FROM caddy:builder AS builder
RUN xcaddy build --with github.com/caddy-dns/cloudflare
FROM caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Official images are still used; the only thing explicitly added is caddy-dns/cloudflare.

services:
caddy:
build:
context: .
dockerfile: Dockerfile
container_name: caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
environment:
CF_API_TOKEN: ${CF_API_TOKEN}
extra_hosts:
- "host.docker.internal:host-gateway" # required for Nextcloud AIO
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
networks:
- proxy
networks:
proxy:
external: true
volumes:
caddy_data:
caddy_config:
Permissions: Zone → DNS → Edit
Scope: balserosworld.net only
Location: /opt/stacks/caddy/.env (chmod 600, in .gitignore)
Format: CF_API_TOKEN=SECRET

The Global API Key is not used. Why: the Global API Key gives full control of the account; a token scoped to one zone and to DNS-Edit limits the damage if it leaks.

19.4 Lesson — docker compose config can print secrets

Section titled “19.4 Lesson — docker compose config can print secrets”
.env avoids hardcoding the token in compose.yaml
but does NOT stop another tool from expanding and printing it
docker compose config → expands ${CF_API_TOKEN} and SHOWS IT
docker compose config --quiet → validates without printing ✅

This actually happened during the deployment: the token ended up printed, it had to be considered compromised and rotated. Rule adopted: in stacks with secrets, always --quiet.

vault.balserosworld.net {
tls { dns cloudflare {env.CF_API_TOKEN} }
reverse_proxy vaultwarden:80
}
kuma.balserosworld.net {
tls { dns cloudflare {env.CF_API_TOKEN} }
reverse_proxy uptime-kuma:3001
}
portainer.balserosworld.net {
tls { dns cloudflare {env.CF_API_TOKEN} }
reverse_proxy portainer:9000
}
cloud.balserosworld.net {
tls { dns cloudflare {env.CF_API_TOKEN} }
reverse_proxy host.docker.internal:11000
}
# test.balserosworld.net {
# reverse_proxy TEST-CONTAINER:PORT
# tls { dns cloudflare {env.CF_API_TOKEN} }
# }
Site Upstream How it is resolved
vault. vaultwarden:80 Docker DNS, proxy network
kuma. uptime-kuma:3001 Docker DNS, proxy network
portainer. portainer:9000 Docker DNS, proxy network
cloud. host.docker.internal:11000 exception: goes out to the host (§29)
test. commented out laboratory (§23)

The file is mounted read-only inside the container:

/opt/stacks/caddy/Caddyfile ──► /etc/caddy/Caddyfile (RW = false)

That is why caddy fmt --overwrite inside the main container fails with a read-only filesystem. To see formatting differences:

Terminal window
sudo docker compose exec caddy caddy fmt --diff /etc/caddy/Caddyfile

The Caddyfile input is not formatted warning is not a functional error; it only indicates a difference from the canonical format. To actually format it, a temporary container was used with the file mounted writable and afterwards Caddy was reloaded.

test.balserosworld.net is kept as a reusable endpoint for HTTPS tests — it was what made it possible to validate the Vaultwarden restore (§33). Its block stays commented out when not in use:

1. bring up the test container
2. connect it to the `proxy` network
3. replace TEST-CONTAINER:PORT
4. uncomment the block
5. validate Caddy
6. reload
7. test over HTTPS
8. comment it out again when finished
User
│ https://vault.balserosworld.net
▼
DNS ──────────────────────────► 192.168.2.8
│
▼
Caddy :443
│ valid TLS (Let's Encrypt via DNS-01)
│ Caddyfile: hostname → reverse_proxy vaultwarden:80
▼
Docker network `proxy`
│ Docker DNS: vaultwarden → 172.22.0.x
▼
Vaultwarden :80
│
▼
/opt/stacks/vaultwarden/vw-data ← the persistence

The Bitwarden apps try by default to authenticate against bitwarden.com, where our accounts do not exist. A self-hosted server has to be selected:

Bitwarden app
│ settings → self-hosted → https://vault.balserosworld.net
▼
Caddy ──► Vaultwarden