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} # Used by caddy-dns/rfc2136 for the test-rfc2136 site -- same # secret CoreDNS's rfc2136 plugin verifies on the other side. - ACME_TSIG_SECRET=${ACME_TSIG_SECRET} # 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: # Custom build with the rfc2136 plugin baked in. The image tag is # CalVer (set in .env COREDNS_IMAGE_TAG) so we can pin specific # builds; `docker compose build coredns` produces the locally-tagged # image, then up -d picks it up. build: context: . dockerfile: coredns/Dockerfile image: coredns-rfc2136:${COREDNS_IMAGE_TAG} container_name: coredns restart: unless-stopped # Run as host's primary user so files the rfc2136 plugin writes to # /zones land owned by rpm:rpm on the host. Without this they'd # be root-owned, making manual edits / git ops painful. # # UID/GID come from env (defaulted to dell01's rpm: 1003:1004). # Override in .env for hosts where rpm has different ids. user: "${COREDNS_UID:-1003}:${COREDNS_GID:-1004}" command: ["-conf", "/etc/coredns/Corefile"] # The Corefile uses {$ACME_TSIG_SECRET} expansion to read the # TSIG secret. Passed in from compose's env (which auto-reads .env). environment: - ACME_TSIG_SECRET=${ACME_TSIG_SECRET} 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 # Read-write because the rfc2136 plugin writes zone files in-place # after each accepted UPDATE message (atomic temp-file + rename). - ./zones:/zones # 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