Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/.vitepress/config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,7 @@ export default defineConfig({
},
{ text: 'Dynamic Configurations', link: '/knowledge-base/proxy/traefik/dynamic-config' },
{ text: 'Load Balancing', link: '/knowledge-base/proxy/traefik/load-balancing' },
{ text: 'DNS Challenge', link: '/knowledge-base/proxy/traefik/dns-challenge' },
{ text: 'Wildcard SSL Certificates', link: '/knowledge-base/proxy/traefik/wildcard-certs' },
{ text: 'Protect Services with Authentik', link: '/knowledge-base/proxy/traefik/protect-services-with-authentik' }
]
Expand All @@ -454,6 +455,7 @@ export default defineConfig({
items: [
{ text: 'Overview', link: '/knowledge-base/proxy/caddy/overview' },
{ text: 'Basic Auth', link: '/knowledge-base/proxy/caddy/basic-auth' },
{ text: 'DNS Challenge', link: '/knowledge-base/proxy/caddy/dns-challenge' },
]
},
]
Expand Down
186 changes: 186 additions & 0 deletions docs/knowledge-base/proxy/caddy/dns-challenge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
---
title: "DNS Challenge"
description: "Switch Caddy from HTTP challenge to DNS challenge for ACME (Let's Encrypt) certificates — required for wildcard certs or servers without a public port 80."
---

# 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](https://caddyserver.com/docs/automatic-https#wildcard-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](https://github.com/orgs/caddy-dns/repositories?utm_source=coolify.io).
- 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.

:::code-group

```sh [Hetzner]
name: coolify-proxy
networks:
coolify:
external: true
services:
caddy:
container_name: coolify-proxy
image: 'lucaslorentz/caddy-docker-proxy:2.8-alpine' # [!code --][!code focus]
build: # [!code ++][!code focus]
dockerfile_inline: | # [!code ++][!code focus]
FROM caddy:2.9-builder AS builder # [!code ++][!code focus]
RUN xcaddy build --with github.com/lucaslorentz/caddy-docker-proxy/v2 --with github.com/caddy-dns/hetzner # [!code ++][!code focus]
FROM caddy:2.9-alpine # [!code ++][!code focus]
COPY --from=builder /usr/bin/caddy /usr/bin/caddy # [!code ++][!code focus]
CMD ["caddy", "docker-proxy"] # [!code ++][!code focus]
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> # [!code ++][!code focus]
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} # [!code ++][!code focus]
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'
```

```sh [Cloudflare]
name: coolify-proxy
networks:
coolify:
external: true
services:
caddy:
container_name: coolify-proxy
image: 'lucaslorentz/caddy-docker-proxy:2.8-alpine' # [!code --][!code focus]
build: # [!code ++][!code focus]
dockerfile_inline: | # [!code ++][!code focus]
FROM caddy:2.9-builder AS builder # [!code ++][!code focus]
RUN xcaddy build --with github.com/lucaslorentz/caddy-docker-proxy/v2 --with github.com/caddy-dns/cloudflare # [!code ++][!code focus]
FROM caddy:2.9-alpine # [!code ++][!code focus]
COPY --from=builder /usr/bin/caddy /usr/bin/caddy # [!code ++][!code focus]
CMD ["caddy", "docker-proxy"] # [!code ++][!code focus]
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> # [!code ++][!code focus]
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} # [!code ++][!code focus]
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'
```


```sh [Route53 (AWS)]
name: coolify-proxy
networks:
coolify:
external: true
services:
caddy:
container_name: coolify-proxy
image: 'lucaslorentz/caddy-docker-proxy:2.8-alpine' # [!code --][!code focus]
build: # [!code ++][!code focus]
dockerfile_inline: | # [!code ++][!code focus]
FROM caddy:2.9-builder AS builder # [!code ++][!code focus]
RUN xcaddy build --with github.com/lucaslorentz/caddy-docker-proxy/v2 --with github.com/caddy-dns/route53 # [!code ++][!code focus]
FROM caddy:2.9-alpine # [!code ++][!code focus]
COPY --from=builder /usr/bin/caddy /usr/bin/caddy # [!code ++][!code focus]
CMD ["caddy", "docker-proxy"] # [!code ++][!code focus]
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> # [!code ++][!code focus]
- AWS_SECRET_ACCESS_KEY=<Secret Access Key> # [!code ++][!code focus]
- AWS_REGION=<Region> # [!code ++][!code focus]
networks:
- coolify
ports:
- '80:80'
- '443:443'
- '443:443/udp'
labels:
- coolify.managed=true
- coolify.proxy=true
- caddy.acme_dns=route53 # [!code ++][!code focus]
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](https://github.com/orgs/caddy-dns/repositories?utm_source=coolify.io), replace the `--with github.com/caddy-dns/<provider>` line in the Dockerfile, and set the appropriate environment variable and `caddy.acme_dns` label.

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](https://letsencrypt.org/docs/rate-limits/?utm_source=coolify.io). While testing, switch to the staging CA to avoid burning your quota. Add this label to the proxy container:

```yaml
- caddy.acme_ca=https://acme-staging-v02.api.letsencrypt.org/directory
```

Remove 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.
Loading
Loading