Coolify DNS Challenge with Cloudflare: Wildcard SSL in Traefik
By default Coolify’s Traefik proxy gets certificates with the Let’s Encrypt HTTP challenge, where Let’s Encrypt connects to your server on port 80 to check a token. That stops working when:
- you want a wildcard certificate (
*.example.com). Let’s Encrypt only issues those through the DNS challenge - you want to keep records proxied through Cloudflare (orange cloud)
- port 80 is closed, or something in front of the server doesn’t pass the challenge through
The DNS challenge (DNS-01) proves you own the domain by creating a temporary _acme-challenge TXT record through your DNS provider’s API instead. This is the Cloudflare version, following the Coolify DNS challenge and wildcard certificate docs, with the errors I’d check first. Other DNS providers only differ in the variable names.
This pairs with Setting Up Coolify with a Subdomain Wildcard (routing every subdomain to one server) and How to Add a Custom Domain in Coolify.
1. Create a scoped Cloudflare API token
Cloudflare dashboard → My Profile → API Tokens → Create Token → template Edit zone DNS:
- Permissions: Zone → DNS → Edit (the template also adds Zone → Zone → Read, keep it)
- Zone Resources: Include → Specific zone → example.com
Copy the token. Don’t use the Global API Key here. If the server ever gets compromised, a scoped token only gives away DNS for that one zone.
2. Switch the Traefik resolver to DNS-01
In Coolify: Servers → your server → Proxy → Configuration. This is the proxy’s Docker Compose file. Edit it in the UI, not in /data/coolify/proxy/docker-compose.yml on disk, because Coolify writes its saved version over that file when it restarts the proxy.
Inside the traefik service, make two changes:
services:
traefik:
environment:
- CF_DNS_API_TOKEN=<your-cloudflare-token>
command:
# ...keep every other existing line...
# REMOVE these two:
# - '--certificatesresolvers.letsencrypt.acme.httpchallenge=true'
# - '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http'
# ADD these:
- '--certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare'
- '--certificatesresolvers.letsencrypt.acme.dnschallenge.delaybeforecheck=0'
- '--certificatesresolvers.letsencrypt.acme.storage=/traefik/acme.json'
Keep the resolver called letsencrypt. Coolify’s generated routers use that name, and if you rename it every app starts logging Router uses a nonexistent certificate resolver.
For other DNS hosts, swap cloudflare and the env variable for the ones in the Lego DNS provider list (Traefik uses Lego under the hood): e.g. hetzner with HETZNER_API_KEY, route53 with the AWS credentials.
Save, then Restart Proxy.
3. Request the wildcard certificate
DNS-01 is now how certificates get issued, but nothing has asked for a wildcard yet. Go to Servers → your server → Proxy → Dynamic Configurations and add a file, e.g. wildcard-example-com.yaml:
http:
routers:
wildcard-example-com:
rule: 'HostRegexp(`[a-z0-9-]+\.example\.com`)'
entryPoints:
- https
priority: 1
service: wildcard-placeholder
tls:
certResolver: letsencrypt
domains:
- main: example.com
sans:
- '*.example.com'
services:
wildcard-placeholder:
loadBalancer:
servers: []
This router is only there to get the certificate issued. With priority 1, any real app router for the same hostname wins, and Traefik hands out the wildcard cert from its store. Dynamic config files reload on their own, no proxy restart needed.
When I first set this up I put the tls.domains labels on the Traefik dashboard router instead (that’s what my wildcard post shows). That still works, but the dynamic config file is tidier and it’s what the Coolify docs use now.
A wildcard covers one level: app.example.com yes, example.com and api.app.example.com no. That’s why example.com is the main above. Add *.app.example.com as another SAN if you need deeper names.
4. Point apps at it
DNS: an A record for * and one for @, both to the server IP. With the DNS challenge these can stay proxied in Cloudflare. Just set SSL/TLS mode to Full (strict), since the origin now has a valid certificate.
In each app, add its domain as normal, e.g. https://api.example.com, and redeploy. To have Coolify generate subdomains automatically, set Servers → General → Wildcard Domain to https://example.com and use Generate domain on new apps.
5. Verify
Watch Servers → Proxy → Logs for the ACME request, then:
echo | openssl s_client -connect api.example.com:443 -servername api.example.com 2>/dev/null \
| openssl x509 -noout -issuer -ext subjectAltName
You want a Let’s Encrypt issuer and DNS:*.example.com, DNS:example.com in the SAN list. (If Cloudflare proxies the record, run this against the server IP with -connect <server-ip>:443 to see the origin certificate rather than Cloudflare’s edge one.)
Troubleshooting
Router uses a nonexistent certificate resolver. Either the resolver was renamed, or Traefik couldn’t load acme.json. Scroll to the top of the proxy logs. A permissions error means running this in the server terminal fixes it:
sudo chmod 600 /data/coolify/proxy/acme.json
then restart the proxy.
cloudflare: failed to find zone / 403 / authentication errors. The token doesn’t cover that zone or lacks DNS edit. Recreate it from the Edit zone DNS template. Make sure you used CF_DNS_API_TOKEN (token), not CF_API_KEY (global key, needs CF_API_EMAIL too).
propagation: time limit exceeded / TXT record not found. Usually a split or delegated DNS setup: the zone Cloudflare edits isn’t the one the internet resolves. Check the TXT record shows up publicly with dig TXT _acme-challenge.example.com @1.1.1.1 while the challenge runs. If you set delaybeforecheck=0 and your DNS is slow, raise it (e.g. 30).
Still the Traefik default certificate. The browser may have cached the old one. Test with curl -v or the openssl line above. If the logs show a successful issue but the app gets the default cert, check that the app’s domain actually matches the wildcard (one level only).
429 too many requests. Let’s Encrypt rate limits. Stop restarting the proxy, wait, and test against the staging CA (--certificatesresolvers.letsencrypt.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory) while you debug. Remove that line once it works.
Last resort. Delete /data/coolify/proxy/acme.json and restart the proxy. Every certificate gets re-requested, so skip this if you’re close to a rate limit.
Related: wildcard subdomains in Coolify · adding a custom domain in Coolify