coredns/docker-compose.yml
Ryan Malloy 083e29bd3e docker-compose: make VULTR_API_KEY optional
Caddy needs this only for DNS-01 cert renewal via Vultr's API, which
happens within the final 30 days of the cert's 90-day lifetime --
roughly once a quarter. Requiring it to be exported on every `docker
compose up` was friction for routine ops (CoreDNS recreations during
unrelated config changes).

Empty default keeps the stack startable without the key in scope. When
renewal is imminent, set the var properly OR (preferred long-term)
migrate Caddy to caddy-dns/rfc2136 pointing at our own plugin and
retire the Vultr dependency entirely.
2026-05-21 11:17:56 -06:00

81 lines
3.5 KiB
YAML

services:
# Caddy runs as a dedicated ACME client + cert renewer. It provisions
# a Let's Encrypt cert for ${CADDY_HOSTNAME} via DNS-01 (Vultr API)
# and persists it to ./caddy-data. CoreDNS reads from that same path
# read-only. The container's HTTP/HTTPS ports are NOT published — we
# only care about the cert files on disk.
caddy:
build: ./caddy
container_name: coredns-caddy
restart: unless-stopped
environment:
- CADDY_HOSTNAME=${CADDY_HOSTNAME}
- ACME_EMAIL=${ACME_EMAIL}
# Optional: only required for Caddy's DNS-01 cert renewal via Vultr's
# API. Cert is valid ~90 days; this env var only matters within the
# final 30d renewal window. Empty default keeps `docker compose up`
# working without the key in scope. Set it when renewal is imminent,
# OR migrate Caddy to caddy-dns/rfc2136 (via our plugin) and retire
# the Vultr dependency entirely.
- VULTR_API_KEY=${VULTR_API_KEY:-}
volumes:
- ./caddy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./caddy-data:/data
- ./caddy-config:/config
healthcheck:
# Two-jobs-in-one: (1) maintain stable filenames (cert.pem / key.pem)
# as symlinks into Caddy's hostname-keyed storage, so the Corefile
# doesn't have to encode the hostname. (2) Flip to "healthy" once
# the symlink dereferences successfully (i.e. Caddy has issued).
# Relative symlink targets so paths work the same from host or
# from any container mounting this directory.
test:
- "CMD-SHELL"
- >
ln -sf certificates/acme-v02.api.letsencrypt.org-directory/${CADDY_HOSTNAME}/${CADDY_HOSTNAME}.crt /data/caddy/cert.pem &&
ln -sf certificates/acme-v02.api.letsencrypt.org-directory/${CADDY_HOSTNAME}/${CADDY_HOSTNAME}.key /data/caddy/key.pem &&
chmod 755 /data/caddy &&
chmod -R a+rX /data/caddy/certificates 2>/dev/null;
test -e /data/caddy/cert.pem
interval: 10s
timeout: 3s
retries: 60 # ~10 min ceiling for initial issuance
start_period: 5s
coredns:
image: ${COREDNS_IMAGE}
container_name: coredns
restart: unless-stopped
command: ["-conf", "/etc/coredns/Corefile"]
depends_on:
caddy:
condition: service_healthy
ports:
- "${DNS_PORT}:53/udp"
- "${DNS_PORT}:53/tcp"
- "${DOT_PORT}:853/tcp"
- "${DOH_PORT}:443/tcp"
- "${METRICS_PORT}:9153/tcp"
- "${HEALTH_PORT}:8080/tcp"
volumes:
- ./Corefile:/etc/coredns/Corefile:ro
- ./zones:/zones:ro
# Subpath mount of Caddy's data dir. The healthcheck maintains
# cert.pem / key.pem symlinks at the top of this tree, so CoreDNS
# sees stable filenames regardless of hostname. The /accounts dir
# (ACME registration private key) is sibling to /caddy and is NOT
# exposed to CoreDNS — only /caddy is mounted.
- ./caddy-data/caddy:/etc/coredns/certs:ro
# CoreDNS's official image is distroless (no shell, no wget/curl), so
# the conventional `wget /health` healthcheck silently fails forever
# and Docker reports the container as unhealthy. The coredns binary
# itself supports a version flag, which exits 0 only if the binary
# is runnable — a thin but honest liveness probe. For deeper checks,
# query :8081/health from outside the container (curl from the host).
healthcheck:
test: ["CMD", "/coredns", "-version"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s