traefik-cloudflare-dns
Cloudflare DNS records that follow your Traefik containers. traefik-cloudflare-dns watches Docker for containers whose Traefik router rules name a host, creates a Cloudflare record for each new host, and deletes the records it created once their host has been gone long enough.
- Follows Docker events. It reacts when a container starts, stops or is removed, and resyncs every minute in case an event was missed.
- Never touches records it didn't make. It only treats a record as its own when the record points at this instance's
TARGETand carries its comment and tags. Records made by hand, or by another instance pointing at another server, stay as they are. - Waits before deleting. A host must be down for
DELETE_AFTER(1 hour by default) before its record is removed, so redeploys and restarts never cause a DNS gap. - Small and safe. A static, distroless, non-root image with no ports, built to sit behind a read-only Docker socket proxy.
How it decides what to create
Every running container's traefik.http.routers.<name>.rule labels are read. Each hostname in a Host(...) matcher that is DOMAIN or a name under it gets a record:
TARGET |
Record |
|---|---|
| IPv4 address | A |
| IPv6 address | AAAA |
| hostname | CNAME |
If a record with that name already exists and isn't owned by this instance, it is left alone, and a warning is logged once.
A typical setup is one wildcard record (*.example.com) for your main server, plus one instance of this tool on every other server, with TARGET set to that server's IP. Explicit records take priority over the wildcard, so each app's name resolves to the server it runs on.
Quick start
The repo's compose.yml is ready to deploy as is, for example as a Docker Compose app in Coolify on each server. Copy .env.example to .env and fill in the four required values. The minimal form is:
services:
traefik-cloudflare-dns:
image: ghcr.io/tiennm99/traefik-cloudflare-dns:1
restart: unless-stopped
environment:
CF_API_TOKEN: ${CF_API_TOKEN:?required}
CF_ZONE_ID: ${CF_ZONE_ID:?required}
DOMAIN: example.com
TARGET: 192.0.2.10
DOCKER_HOST: tcp://dockerproxy:2375
depends_on:
- dockerproxy
dockerproxy:
image: tecnativa/docker-socket-proxy:latest
restart: unless-stopped
environment:
CONTAINERS: 1
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
The proxy only needs CONTAINERS. Events and ping are allowed by the proxy's defaults, and every POST is refused, so the tool can read Docker but never change it.
Create the Cloudflare API token with Zone → DNS → Edit, limited to the one zone.
Configuration
| Variable | Default | Purpose |
|---|---|---|
CF_API_TOKEN |
required | Cloudflare API token with DNS edit on the zone |
CF_ZONE_ID |
required | Zone ID, from the zone's overview page |
DOMAIN |
required | Only hosts equal to or under this name are managed |
TARGET |
required | Record content: this server's IP, or a hostname for a CNAME |
PROXIED |
false |
Create orange-cloud (proxied) records |
TTL |
1 |
Record TTL in seconds; 1 means automatic |
RECORD_COMMENT |
managed by traefik-cloudflare-dns |
Comment set on every record, and part of ownership |
RECORD_TAGS |
empty | Comma-separated name:value tags, set on every record and part of ownership |
DELETE_AFTER |
1h |
How long a host must be down before its record is deleted |
RESYNC_INTERVAL |
1m |
Full resync period, on top of Docker events |
DRY_RUN |
false |
Log creates and deletes without making them |
DOCKER_HOST |
unix:///var/run/docker.sock |
Docker API endpoint, unix:// or tcp:// |
LOG_LEVEL |
info |
debug, info, warn or error |
Comments and tags
Cloudflare allows record comments on every plan. They are limited to 100 characters on Free and 500 on paid plans. Tags are only available on paid plans, and on a Free zone Cloudflare rejects any record that has them, so leave RECORD_TAGS empty there.
Ownership is matched exactly. If you change RECORD_COMMENT or RECORD_TAGS, records created under the old values are no longer this instance's, so they won't be deleted automatically. Update or remove them by hand.
Deletion timing
The clock for DELETE_AFTER starts when a resync finds an owned record whose host no longer belongs to any running container. It resets if the host comes back. The timers live in memory, so a restart starts them over. That can only delay a deletion, never bring one forward.
Give each server's instance its own TARGET. Two instances pointing at the same target with the same comment would each treat the other's records as their own.
Development
go vet ./...
go test -race ./...
docker build -t traefik-cloudflare-dns:dev .
CI runs the same checks on every push. Pushes to main publish ghcr.io/tiennm99/traefik-cloudflare-dns:edge. A vX.Y.Z tag publishes X.Y.Z, X.Y and X.