Switch Caddy to DNS Challenge
By default, Coolify configures Caddy to obtain SSL certificates using the HTTP challenge, which requires port 80 to be publicly reachable. There are two common reasons to switch to the DNS challenge instead:
- You want wildcard SSL certificates (e.g.,
*.example.com) — these require DNS challenge. - Your server does not have a public port 80 (e.g., internal network, behind a firewall, or a Tailscale-only node).
How It Works
Instead of proving domain ownership over HTTP, Caddy asks your DNS provider to create a temporary TXT record under _acme-challenge.<your-domain>. Let's Encrypt reads that record to confirm ownership, then issues the certificate.
Unlike Traefik, Caddy DNS provider support must be compiled into the binary. The default lucaslorentz/caddy-docker-proxy image does not include any DNS provider modules, so the configuration below uses a dockerfile_inline build to produce the correct binary automatically — no separate build step or registry required.
Prerequisites
- A domain managed by a supported DNS provider.
- An API token / key for that provider with permission to create and delete DNS records.
Configuration
Go to Servers → your server → Proxy and apply the changes shown below to your existing Caddy configuration.
name: coolify-proxy
networks:
coolify:
external: true
services:
caddy:
container_name: coolify-proxy
image: 'lucaslorentz/caddy-docker-proxy:2.8-alpine'
build:
dockerfile_inline: |
FROM caddy:2.9-builder AS builder
RUN xcaddy build --with github.com/lucaslorentz/caddy-docker-proxy/v2 --with github.com/caddy-dns/hetzner
FROM caddy:2.9-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
CMD ["caddy", "docker-proxy"]
restart: unless-stopped
extra_hosts:
- 'host.docker.internal:host-gateway'
environment:
- CADDY_DOCKER_POLLING_INTERVAL=5s
- CADDY_DOCKER_CADDYFILE_PATH=/dynamic/Caddyfile
- HETZNER_API_TOKEN=<Hetzner API Token>
networks:
- coolify
ports:
- '80:80'
- '443:443'
- '443:443/udp'
labels:
- coolify.managed=true
- coolify.proxy=true
- caddy.acme_dns=hetzner {env.HETZNER_API_TOKEN}
volumes:
- '/var/run/docker.sock:/var/run/docker.sock:ro'
- '/data/coolify/proxy/caddy/dynamic:/dynamic'
- '/data/coolify/proxy/caddy/config:/config'
- '/data/coolify/proxy/caddy/data:/data'name: coolify-proxy
networks:
coolify:
external: true
services:
caddy:
container_name: coolify-proxy
image: 'lucaslorentz/caddy-docker-proxy:2.8-alpine'
build:
dockerfile_inline: |
FROM caddy:2.9-builder AS builder
RUN xcaddy build --with github.com/lucaslorentz/caddy-docker-proxy/v2 --with github.com/caddy-dns/cloudflare
FROM caddy:2.9-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
CMD ["caddy", "docker-proxy"]
restart: unless-stopped
extra_hosts:
- 'host.docker.internal:host-gateway'
environment:
- CADDY_DOCKER_POLLING_INTERVAL=5s
- CADDY_DOCKER_CADDYFILE_PATH=/dynamic/Caddyfile
- CF_API_TOKEN=<Cloudflare API Token>
networks:
- coolify
ports:
- '80:80'
- '443:443'
- '443:443/udp'
labels:
- coolify.managed=true
- coolify.proxy=true
- caddy.acme_dns=cloudflare {env.CF_API_TOKEN}
volumes:
- '/var/run/docker.sock:/var/run/docker.sock:ro'
- '/data/coolify/proxy/caddy/dynamic:/dynamic'
- '/data/coolify/proxy/caddy/config:/config'
- '/data/coolify/proxy/caddy/data:/data'name: coolify-proxy
networks:
coolify:
external: true
services:
caddy:
container_name: coolify-proxy
image: 'lucaslorentz/caddy-docker-proxy:2.8-alpine'
build:
dockerfile_inline: |
FROM caddy:2.9-builder AS builder
RUN xcaddy build --with github.com/lucaslorentz/caddy-docker-proxy/v2 --with github.com/caddy-dns/route53
FROM caddy:2.9-alpine
COPY --from=builder /usr/bin/caddy /usr/bin/caddy
CMD ["caddy", "docker-proxy"]
restart: unless-stopped
extra_hosts:
- 'host.docker.internal:host-gateway'
environment:
- CADDY_DOCKER_POLLING_INTERVAL=5s
- CADDY_DOCKER_CADDYFILE_PATH=/dynamic/Caddyfile
- AWS_ACCESS_KEY_ID=<Access Key ID>
- AWS_SECRET_ACCESS_KEY=<Secret Access Key>
- AWS_REGION=<Region>
networks:
- coolify
ports:
- '80:80'
- '443:443'
- '443:443/udp'
labels:
- coolify.managed=true
- coolify.proxy=true
- caddy.acme_dns=route53
volumes:
- '/var/run/docker.sock:/var/run/docker.sock:ro'
- '/data/coolify/proxy/caddy/dynamic:/dynamic'
- '/data/coolify/proxy/caddy/config:/config'
- '/data/coolify/proxy/caddy/data:/data'For other DNS providers, find the module path at github.com/caddy-dns, replace the
--with github.com/caddy-dns/<provider>line in the Dockerfile, and set the appropriate environment variable andcaddy.acme_dnslabel.
Restart the proxy after saving. Caddy will build the image on first start and then use the DNS challenge to obtain and renew SSL certificates.
Troubleshooting
Certificate not issuing / DNS record not found
DNS propagation can be slow. You can add an explicit delay per-site by editing /data/coolify/proxy/caddy/dynamic/Caddyfile on your server:
your.domain.com {
tls {
dns hetzner {env.HETZNER_API_TOKEN}
propagation_delay 30s
}
}Rate limits
Let's Encrypt enforces rate limits. While testing, switch to the staging CA to avoid burning your quota. Add this label to the proxy container:
- caddy.acme_ca=https://acme-staging-v02.api.letsencrypt.org/directoryRemove this label once everything works.
Wrong or missing DNS module
If Caddy starts but certificates fail with an unknown provider error, the DNS module was not included in the image build. Verify the --with github.com/caddy-dns/<provider> line in the dockerfile_inline matches your provider, then restart the proxy to trigger a rebuild.
