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} - VULTR_API_KEY=${VULTR_API_KEY:?VULTR_API_KEY must be exported in your shell} 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