Automatically manage Cloudflare DNS records using Docker container labels. A single container replaces running multiple instances of traditional DDNS updaters — just add labels to your containers and DNS records are created and kept up to date with your public IP.
services:
nginx:
image: nginx:latest
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "www"
cloudflareddns.proxy: "true"- Single container — one service manages DNS for all your containers
- Label-driven — no config files, just Docker labels
- Multiple records per container — indexed label support
- Real-time — listens for Docker events and syncs immediately
- Periodic sync — full reconciliation on a configurable interval
- Deduplication — multiple containers with the same record won't conflict
- Cloudflare proxy toggle — enable or disable the orange cloud per record
- Go to Cloudflare Dashboard → API Tokens
- Click Create Token
- Use the Edit zone DNS template
- Under Zone Resources, select the zones you want to manage
- Copy the token
services:
cloudflare-ddns:
image: hunterreadca/cloudflare-ddns-sync
container_name: cloudflare-ddns-sync
restart: unless-stopped
environment:
API_KEY: "your-cloudflare-api-token"
SYNC_INTERVAL: "300"
LOG_LEVEL: "INFO"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:roservices:
my-app:
image: nginx:latest
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "app"
cloudflareddns.proxy: "true"The DNS record is created when the container starts and updated whenever your public IP changes.
| Variable | Default | Description |
|---|---|---|
API_KEY |
(required) | Cloudflare API token with DNS edit permissions |
SYNC_INTERVAL |
300 |
Seconds between full sync cycles |
LABEL_PREFIX |
cloudflareddns |
Prefix for Docker labels |
LOG_LEVEL |
INFO |
Logging level (DEBUG, INFO, WARNING, ERROR) |
DOCKER_SOCKET |
unix:///var/run/docker.sock |
Docker socket path |
| Label | Required | Default | Description |
|---|---|---|---|
cloudflareddns.zone |
Yes | — | Cloudflare zone / domain name |
cloudflareddns.subdomain |
No | @ |
Subdomain (@ for root domain) |
cloudflareddns.proxy |
No | true |
Enable Cloudflare proxy (orange cloud) |
cloudflareddns.type |
No | A |
DNS record type (A or AAAA) |
cloudflareddns.ttl |
No | 1 |
TTL in seconds (1 = automatic) |
services:
web:
image: nginx:latest
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "www"
cloudflareddns.proxy: "true"Creates: www.example.com → <your public IP> (proxied)
services:
web:
image: nginx:latest
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "@"
cloudflareddns.proxy: "true"Creates: example.com → <your public IP> (proxied)
Use numeric indexes to define multiple records on a single container:
services:
app:
image: nginx:latest
labels:
cloudflareddns.0.zone: "example.com"
cloudflareddns.0.subdomain: "app"
cloudflareddns.0.proxy: "true"
cloudflareddns.1.zone: "example.com"
cloudflareddns.1.subdomain: "api"
cloudflareddns.1.proxy: "true"
cloudflareddns.2.zone: "other.com"
cloudflareddns.2.subdomain: "@"
cloudflareddns.2.proxy: "false"Creates:
app.example.com → <your public IP>(proxied)api.example.com → <your public IP>(proxied)other.com → <your public IP>(not proxied)
services:
frontend:
image: nginx:latest
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "www"
cloudflareddns.proxy: "true"
backend:
image: node:20
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "api"
cloudflareddns.proxy: "true"
blog:
image: ghost:latest
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "blog"
cloudflareddns.proxy: "true"services:
mail:
image: mailserver:latest
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "mail"
cloudflareddns.proxy: "false"Creates: mail.example.com → <your public IP> (DNS only, grey cloud)
services:
app:
image: nginx:latest
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "app"
cloudflareddns.proxy: "false"
cloudflareddns.ttl: "3600"Note: TTL is only respected when proxy is disabled. Proxied records always use automatic TTL.
services:
app:
image: nginx:latest
labels:
cloudflareddns.zone: "example.com"
cloudflareddns.subdomain: "v6"
cloudflareddns.type: "AAAA"
cloudflareddns.proxy: "true"- Startup — verifies the Cloudflare API token, detects your public IP, scans all running containers for labels, and creates/updates DNS records.
- Event-driven — listens on the Docker socket for container
start/stop/die/destroyevents and triggers a re-sync with a 2-second debounce. - Periodic sync — a full sync runs every
SYNC_INTERVALseconds to catch IP changes even when no containers change. - Deduplication — if multiple containers define the same zone + subdomain + type, only one DNS record is created.
- Idempotent — if the record already exists with the correct IP and proxy setting, no API call is made.
The minimum required permissions for the API token:
| Permission | Access |
|---|---|
| Zone → DNS | Edit |
| Zone → Zone | Read |
Scope the token to only the zones you need for better security.
GNU General Public License v3.0 — see LICENSE for details.