timothyjmiller/cloudflare-ddns

By timothyjmiller

β€’Updated about 2 months ago

πŸ¦€ Rust based dynamic DNS updater for Cloudflare

Image
Networking
Security
Developer tools
41

5M+

timothyjmiller/cloudflare-ddns repository overview

Cloudflare DDNS ⁠

⁠🌍 Cloudflare DDNS

Access your home network remotely via a custom domain name without a static IP!

A feature-complete dynamic DNS client for Cloudflare, written in Rust. The smallest and most memory-efficient open-source Cloudflare DDNS Docker image available β€” ~1.1 MB image size and ~3.5 MB RAM at runtime, smaller and leaner than Go-based alternatives. Built as a fully static binary from scratch with zero runtime dependencies.

Configure everything with environment variables. Supports notifications, heartbeat monitoring, WAF list management, flexible scheduling, and more.

Docker Pulls Docker Image Size

⁠✨ Features

  • πŸ” Multiple IP detection providers β€” Cloudflare Trace, Cloudflare DNS-over-HTTPS, ipify, local interface, custom URL, or static IPs
  • πŸ“‘ IPv4 and IPv6 β€” Full dual-stack support with independent provider configuration
  • 🌐 Multiple domains and zones β€” Update any number of domains across multiple Cloudflare zones
  • πŸƒ Wildcard domains β€” Support for *.example.com records
  • 🌍 Internationalized domain names β€” Full IDN/punycode support (e.g. mΓΌnchen.de)
  • πŸ›‘οΈ WAF list management β€” Automatically update Cloudflare WAF IP lists
  • πŸ”” Notifications β€” Shoutrrr-compatible notifications (Discord, Slack, Telegram, Gotify, Pushover, generic webhooks)
  • πŸ’“ Heartbeat monitoring β€” Healthchecks.io and Uptime Kuma integration
  • ⏱️ Cron scheduling β€” Flexible update intervals via cron expressions
  • πŸ§ͺ Dry-run mode β€” Preview changes without modifying DNS records
  • 🧹 Graceful shutdown β€” Signal handling (SIGINT/SIGTERM) with optional DNS record cleanup
  • πŸ’¬ Record comments β€” Tag managed records with comments for identification
  • 🎯 Managed record regex β€” Control which records the tool manages via regex matching
  • 🎨 Pretty output with emoji β€” Configurable emoji and verbosity levels
  • πŸ”’ Zero-log IP detection β€” Uses Cloudflare's cdn-cgi/trace⁠ by default
  • 🏠 CGNAT-aware local detection β€” Filters out shared address space (100.64.0.0/10) and private ranges
  • 🚫 Cloudflare IP rejection β€” Optionally reject Cloudflare anycast IPs to prevent incorrect DNS updates
  • 🀏 Tiny static binary β€” ~1.1 MB Docker image built from scratch, zero runtime dependencies

β πŸš€ Quick Start

docker run -d \
  --name cloudflare-ddns \
  --restart unless-stopped \
  --network host \
  -e CLOUDFLARE_API_TOKEN=your-api-token \
  -e DOMAINS=example.com,www.example.com \
  timothyjmiller/cloudflare-ddns:latest

That's it. The container detects your public IP and updates the DNS records for your domains every 5 minutes.

⚠️ --network host is required to detect IPv6 addresses. If you only need IPv4, you can omit it and set IP6_PROVIDER=none.

β πŸ”‘ Authentication

VariableDescription
CLOUDFLARE_API_TOKENAPI token with "Edit DNS" capability
CLOUDFLARE_API_TOKEN_FILEPath to a file containing the API token (Docker secrets compatible)

To generate an API token, go to your Cloudflare Profile⁠ and create a token capable of Edit DNS.

⁠🌐 Domains

VariableDescription
DOMAINSComma-separated list of domains to update for both IPv4 and IPv6
IP4_DOMAINSComma-separated list of IPv4-only domains
IP6_DOMAINSComma-separated list of IPv6-only domains

Wildcard domains are supported: *.example.com

At least one of DOMAINS, IP4_DOMAINS, IP6_DOMAINS, or WAF_LISTS must be set.

β πŸ” IP Detection Providers

VariableDefaultDescription
IP4_PROVIDERipifyIPv4 detection method
IP6_PROVIDERcloudflare.traceIPv6 detection method

Available providers:

ProviderDescription
cloudflare.traceπŸ”’ Cloudflare's /cdn-cgi/trace endpoint (default, zero-log)
cloudflare.doh🌐 Cloudflare DNS-over-HTTPS (whoami.cloudflare TXT query)
ipify🌎 ipify.org API
local🏠 Local IP via system routing table (no network traffic, CGNAT-aware)
local.iface:<name>πŸ”Œ IP from a specific network interface (e.g., local.iface:eth0)
url:<url>πŸ”— Custom HTTP(S) endpoint that returns an IP address
literal:<ips>πŸ“Œ Static IP addresses (comma-separated)
none🚫 Disable this IP type

⁠🚫 Cloudflare IP Rejection

VariableDefaultDescription
REJECT_CLOUDFLARE_IPSfalseReject detected IPs that fall within Cloudflare's IP ranges

Some IP detection providers occasionally return a Cloudflare anycast IP instead of your real public IP. When this happens, your DNS record gets updated to point at Cloudflare infrastructure rather than your actual address.

Setting REJECT_CLOUDFLARE_IPS=true prevents this. Each update cycle fetches Cloudflare's published IP ranges⁠ and skips any detected IP that falls within them. A warning is logged for every rejected IP.

⁠⏱️ Scheduling

VariableDefaultDescription
UPDATE_CRON@every 5mUpdate schedule
UPDATE_ON_STARTtrueRun an update immediately on startup
DELETE_ON_STOPfalseDelete managed DNS records on shutdown

Schedule formats:

  • @every 5m β€” Every 5 minutes
  • @every 1h β€” Every hour
  • @every 30s β€” Every 30 seconds
  • @once β€” Run once and exit

When UPDATE_CRON=@once, UPDATE_ON_START must be true and DELETE_ON_STOP must be false.

β πŸ“ DNS Record Settings

VariableDefaultDescription
TTL1 (auto)DNS record TTL in seconds (1=auto, or 30-86400)
PROXIEDfalseExpression controlling which domains are proxied through Cloudflare
RECORD_COMMENT(empty)Comment attached to managed DNS records
MANAGED_RECORDS_COMMENT_REGEX(empty)Regex to identify which records are managed (empty = all)

The PROXIED variable supports boolean expressions:

ExpressionMeaning
true☁️ Proxy all domains
falseπŸ”“ Don't proxy any domains
is(example.com)🎯 Only proxy example.com
sub(cdn.example.com)🌳 Proxy cdn.example.com and its subdomains
is(a.com) || is(b.com)πŸ”€ Proxy a.com or b.com
!is(vpn.example.com)🚫 Proxy everything except vpn.example.com

Operators: is(), sub(), !, &&, ||, ()

β πŸ›‘οΈ WAF Lists

VariableDefaultDescription
WAF_LISTS(empty)Comma-separated WAF lists in account-id/list-name format
WAF_LIST_DESCRIPTION(empty)Description for managed WAF lists
WAF_LIST_ITEM_COMMENT(empty)Comment for WAF list items
MANAGED_WAF_LIST_ITEMS_COMMENT_REGEX(empty)Regex to identify managed WAF list items

WAF list names must match the pattern [a-z0-9_]+.

β πŸ”” Notifications (Shoutrrr)

VariableDescription
SHOUTRRRNewline-separated list of notification service URLs

Supported services:

ServiceURL format
πŸ’¬ Discorddiscord://token@webhook-id
πŸ“¨ Slackslack://token-a/token-b/token-c
✈️ Telegramtelegram://bot-token@telegram?chats=chat-id
πŸ“‘ Gotifygotify://host/path?token=app-token
πŸ“² Pushoverpushover://user-key@api-token
🌐 Generic webhookgeneric://host/path or generic+https://host/path

Notifications are sent when DNS records are updated, created, deleted, or when errors occur.

β πŸ’“ Heartbeat Monitoring

VariableDescription
HEALTHCHECKSHealthchecks.io ping URL
UPTIMEKUMAUptime Kuma push URL

Heartbeats are sent after each update cycle. On failure, a fail signal is sent. On shutdown, an exit signal is sent.

⁠⏳ Timeouts

VariableDefaultDescription
DETECTION_TIMEOUT5sTimeout for IP detection requests
UPDATE_TIMEOUT30sTimeout for Cloudflare API requests

⁠πŸ–₯️ Output

VariableDefaultDescription
EMOJItrueUse emoji in output messages
QUIETfalseSuppress informational output

⁠🏁 CLI Flags

FlagDescription
--dry-runπŸ§ͺ Preview changes without modifying DNS records
--repeatπŸ” Run continuously (legacy config mode only; env var mode uses UPDATE_CRON)

β πŸ“‹ All Environment Variables

VariableDefaultDescription
CLOUDFLARE_API_TOKENβ€”πŸ”‘ API token
CLOUDFLARE_API_TOKEN_FILEβ€”πŸ“„ Path to API token file
DOMAINSβ€”πŸŒ Domains for both IPv4 and IPv6
IP4_DOMAINSβ€”4️⃣ IPv4-only domains
IP6_DOMAINSβ€”6️⃣ IPv6-only domains
IP4_PROVIDERipifyπŸ” IPv4 detection provider
IP6_PROVIDERcloudflare.traceπŸ” IPv6 detection provider
UPDATE_CRON@every 5m⏱️ Update schedule
UPDATE_ON_STARTtrueπŸš€ Update on startup
DELETE_ON_STOPfalse🧹 Delete records on shutdown
TTL1⏳ DNS record TTL
PROXIEDfalse☁️ Proxied expression
RECORD_COMMENTβ€”πŸ’¬ DNS record comment
MANAGED_RECORDS_COMMENT_REGEXβ€”πŸŽ― Managed records regex
WAF_LISTSβ€”πŸ›‘οΈ WAF lists to manage
WAF_LIST_DESCRIPTIONβ€”πŸ“ WAF list description
WAF_LIST_ITEM_COMMENTβ€”πŸ’¬ WAF list item comment
MANAGED_WAF_LIST_ITEMS_COMMENT_REGEXβ€”πŸŽ― Managed WAF items regex
DETECTION_TIMEOUT5s⏳ IP detection timeout
UPDATE_TIMEOUT30s⏳ API request timeout
REJECT_CLOUDFLARE_IPSfalse🚫 Reject Cloudflare anycast IPs
EMOJItrue🎨 Enable emoji output
QUIETfalse🀫 Suppress info output
HEALTHCHECKSβ€”πŸ’“ Healthchecks.io URL
UPTIMEKUMAβ€”πŸ’“ Uptime Kuma URL
SHOUTRRRβ€”πŸ”” Notification URLs (newline-separated)

⁠🚒 Deployment

⁠🐳 Docker Compose
version: '3.9'
services:
  cloudflare-ddns:
    image: timothyjmiller/cloudflare-ddns:latest
    container_name: cloudflare-ddns
    security_opt:
      - no-new-privileges:true
    network_mode: 'host'
    environment:
      - CLOUDFLARE_API_TOKEN=your-api-token
      - DOMAINS=example.com,www.example.com
      - PROXIED=true
      - IP6_PROVIDER=none
      - HEALTHCHECKS=https://hc-ping.com/your-uuid
    restart: unless-stopped

⚠️ Docker requires network_mode: host to access the IPv6 public address.

⁠☸️ Kubernetes

The included manifest uses the legacy JSON config mode. Create a secret containing your config.json and apply:

kubectl create secret generic config-cloudflare-ddns --from-file=config.json -n ddns
kubectl apply -f k8s/cloudflare-ddns.yml
⁠🐧 Linux + Systemd
  1. Build and install:
cargo build --release
sudo cp target/release/cloudflare-ddns /usr/local/bin/
  1. Copy the systemd units from the systemd/ directory:
sudo cp systemd/cloudflare-ddns.service /etc/systemd/system/
sudo cp systemd/cloudflare-ddns.timer /etc/systemd/system/
  1. Place a config.json at /etc/cloudflare-ddns/config.json (the systemd service uses legacy config mode).

  2. Enable the timer:

sudo systemctl enable --now cloudflare-ddns.timer

The timer runs the service every 15 minutes (configurable in cloudflare-ddns.timer).

β πŸ”¨ Building from Source

cargo build --release

The binary is at target/release/cloudflare-ddns.

⁠🐳 Docker builds
# Single architecture (linux/amd64)
./scripts/docker-build.sh

# Multi-architecture (linux/amd64, linux/arm64, linux/ppc64le)
./scripts/docker-build-all.sh

β πŸ’» Supported Platforms


β πŸ“ Legacy JSON Config File

For backwards compatibility, cloudflare-ddns still supports configuration via a config.json file. This mode is used automatically when no CLOUDFLARE_API_TOKEN environment variable is set.

β πŸš€ Quick Start
cp config-example.json config.json
# Edit config.json with your values
cloudflare-ddns
β πŸ”‘ Authentication

Use either an API token (recommended) or a legacy API key:

"authentication": {
  "api_token": "Your cloudflare API token with Edit DNS capability"
}

Or with a legacy API key:

"authentication": {
  "api_key": {
    "api_key": "Your cloudflare API Key",
    "account_email": "The email address you use to sign in to cloudflare"
  }
}
β πŸ“‘ IPv4 and IPv6

Some ISP provided modems only allow port forwarding over IPv4 or IPv6. Disable the interface that is not accessible:

"a": true,
"aaaa": true
β βš™οΈ Config Options
KeyTypeDefaultDescription
cloudflarearrayrequiredList of zone configurations
abooltrueEnable IPv4 (A record) updates
aaaabooltrueEnable IPv6 (AAAA record) updates
purgeUnknownRecordsboolfalseDelete stale/duplicate DNS records
ttlint300DNS record TTL in seconds (30-86400, values < 30 become auto)

Each zone entry contains:

KeyTypeDescription
authenticationobjectAPI token or API key credentials
zone_idstringCloudflare zone ID (found in zone dashboard)
subdomainsarraySubdomain entries to update
proxiedboolDefault proxied status for subdomains in this zone

Subdomain entries can be a simple string or a detailed object:

"subdomains": [
  "",
  "@",
  "www",
  { "name": "vpn", "proxied": true }
]

Use "" or "@" for the root domain. Do not include the base domain name.

β πŸ”„ Environment Variable Substitution

In the legacy config file, values can reference environment variables with the CF_DDNS_ prefix:

{
  "cloudflare": [{
    "authentication": {
      "api_token": "${CF_DDNS_API_TOKEN}"
    },
    ...
  }]
}
β πŸ“  Example: Multiple Subdomains
{
  "cloudflare": [
    {
      "authentication": {
        "api_token": "your-api-token"
      },
      "zone_id": "your_zone_id",
      "subdomains": [
        { "name": "", "proxied": true },
        { "name": "www", "proxied": true },
        { "name": "vpn", "proxied": false }
      ]
    }
  ],
  "a": true,
  "aaaa": true,
  "purgeUnknownRecords": false,
  "ttl": 300
}
⁠🌐 Example: Multiple Zones
{
  "cloudflare": [
    {
      "authentication": { "api_token": "your-api-token" },
      "zone_id": "first_zone_id",
      "subdomains": [
        { "name": "", "proxied": false }
      ]
    },
    {
      "authentication": { "api_token": "your-api-token" },
      "zone_id": "second_zone_id",
      "subdomains": [
        { "name": "", "proxied": false }
      ]
    }
  ],
  "a": true,
  "aaaa": true,
  "purgeUnknownRecords": false
}
⁠🐳 Docker Compose (legacy config file)
version: '3.9'
services:
  cloudflare-ddns:
    image: timothyjmiller/cloudflare-ddns:latest
    container_name: cloudflare-ddns
    security_opt:
      - no-new-privileges:true
    network_mode: 'host'
    volumes:
      - /YOUR/PATH/HERE/config.json:/config.json
    restart: unless-stopped
⁠🏁 Legacy CLI Flags

In legacy config mode, use --repeat to run continuously (the TTL value is used as the update interval):

cloudflare-ddns --repeat
cloudflare-ddns --repeat --dry-run

β πŸ“œ License

This project is licensed under the GNU General Public License, version 3 (GPLv3).

β πŸ‘¨β€πŸ’» Author

Timothy Miller

View my GitHub profile πŸ’‘β 

View my personal website πŸ’»β 

Tag summary

Content type

Image

Digest

sha256:37c99677e…

Size

1.1 MB

Last updated

about 2 months ago

docker pull timothyjmiller/cloudflare-ddns:2.1.2