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.
19. Caddy — image, compose and secrets
Section titled “19. Caddy — image, compose and secrets”19.1 Custom image
Section titled “19.1 Custom image”The official Caddy image does not ship the Cloudflare DNS module that DNS-01 requires, so it is built:
FROM caddy:builder AS builderRUN xcaddy build --with github.com/caddy-dns/cloudflare
FROM caddy:latestCOPY --from=builder /usr/bin/caddy /usr/bin/caddyOfficial images are still used; the only thing explicitly added is
caddy-dns/cloudflare.
19.2 Compose
Section titled “19.2 Compose”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:19.3 Cloudflare token
Section titled “19.3 Cloudflare token”Permissions: Zone → DNS → EditScope: balserosworld.net onlyLocation: /opt/stacks/caddy/.env (chmod 600, in .gitignore)Format: CF_API_TOKEN=SECRETThe 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 ITdocker 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.
20. Current Caddyfile
Section titled “20. Current Caddyfile”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) |
23. Caddyfile operation
Section titled “23. Caddyfile operation”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:
sudo docker compose exec caddy caddy fmt --diff /etc/caddy/CaddyfileThe 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.
23.1 Laboratory endpoint
Section titled “23.1 Laboratory endpoint”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 container2. connect it to the `proxy` network3. replace TEST-CONTAINER:PORT4. uncomment the block5. validate Caddy6. reload7. test over HTTPS8. comment it out again when finished24. Full Vaultwarden flow
Section titled “24. Full Vaultwarden flow”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 persistence24.1 Bitwarden client against Vaultwarden
Section titled “24.1 Bitwarden client against Vaultwarden”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