
# π 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. Configure everything with environment variables. Supports notifications, heartbeat monitoring, WAF list management, flexible scheduling, and more.
[](https://hub.docker.com/r/timothyjmiller/cloudflare-ddns) [](https://hub.docker.com/r/timothyjmiller/cloudflare-ddns)
## β¨ 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](https://www.cloudflare.com/cdn-cgi/trace) by default
- π **CGNAT-aware local detection** β Filters out shared address space (100.64.0.0/10) and private ranges
- π€ **Tiny static binary** β ~3.5 MB Docker image built from scratch, zero runtime dependencies
## π Quick Start
```bash
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
| Variable | Description |
|----------|-------------|
| `CLOUDFLARE_API_TOKEN` | API token with "Edit DNS" capability |
| `CLOUDFLARE_API_TOKEN_FILE` | Path to a file containing the API token (Docker secrets compatible) |
To generate an API token, go to your [Cloudflare Profile](https://dash.cloudflare.com/profile/api-tokens) and create a token capable of **Edit DNS**.
## π Domains
| Variable | Description |
|----------|-------------|
| `DOMAINS` | Comma-separated list of domains to update for both IPv4 and IPv6 |
| `IP4_DOMAINS` | Comma-separated list of IPv4-only domains |
| `IP6_DOMAINS` | Comma-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
| Variable | Default | Description |
|----------|---------|-------------|
| `IP4_PROVIDER` | `cloudflare.trace` | IPv4 detection method |
| `IP6_PROVIDER` | `cloudflare.trace` | IPv6 detection method |
Available providers:
| Provider | Description |
|----------|-------------|
| `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:` | π IP from a specific network interface (e.g., `local.iface:eth0`) |
| `url:` | π Custom HTTP(S) endpoint that returns an IP address |
| `literal:` | π Static IP addresses (comma-separated) |
| `none` | π« Disable this IP type |
## β±οΈ Scheduling
| Variable | Default | Description |
|----------|---------|-------------|
| `UPDATE_CRON` | `@every 5m` | Update schedule |
| `UPDATE_ON_START` | `true` | Run an update immediately on startup |
| `DELETE_ON_STOP` | `false` | Delete 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
| Variable | Default | Description |
|----------|---------|-------------|
| `TTL` | `1` (auto) | DNS record TTL in seconds (1=auto, or 30-86400) |
| `PROXIED` | `false` | Expression 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:
| Expression | Meaning |
|------------|---------|
| `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
| Variable | Default | Description |
|----------|---------|-------------|
| `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)
| Variable | Description |
|----------|-------------|
| `SHOUTRRR` | Newline-separated list of notification service URLs |
Supported services:
| Service | URL format |
|---------|------------|
| π¬ Discord | `discord://token@webhook-id` |
| π¨ Slack | `slack://token-a/token-b/token-c` |
| βοΈ Telegram | `telegram://bot-token@telegram?chats=chat-id` |
| π‘ Gotify | `gotify://host/path?token=app-token` |
| π² Pushover | `pushover://user-key@api-token` |
| π Generic webhook | `generic://host/path` or `generic+https://host/path` |
Notifications are sent when DNS records are updated, created, deleted, or when errors occur.
## π Heartbeat Monitoring
| Variable | Description |
|----------|-------------|
| `HEALTHCHECKS` | Healthchecks.io ping URL |
| `UPTIMEKUMA` | Uptime 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
| Variable | Default | Description |
|----------|---------|-------------|
| `DETECTION_TIMEOUT` | `5s` | Timeout for IP detection requests |
| `UPDATE_TIMEOUT` | `30s` | Timeout for Cloudflare API requests |
## π₯οΈ Output
| Variable | Default | Description |
|----------|---------|-------------|
| `EMOJI` | `true` | Use emoji in output messages |
| `QUIET` | `false` | Suppress informational output |
## π CLI Flags
| Flag | Description |
|------|-------------|
| `--dry-run` | π§ͺ Preview changes without modifying DNS records |
| `--repeat` | π Run continuously (legacy config mode only; env var mode uses `UPDATE_CRON`) |
## π All Environment Variables
| Variable | Default | Description |
|----------|---------|-------------|
| `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_PROVIDER` | `cloudflare.trace` | π IPv4 detection provider |
| `IP6_PROVIDER` | `cloudflare.trace` | π IPv6 detection provider |
| `UPDATE_CRON` | `@every 5m` | β±οΈ Update schedule |
| `UPDATE_ON_START` | `true` | π Update on startup |
| `DELETE_ON_STOP` | `false` | π§Ή Delete records on shutdown |
| `TTL` | `1` | β³ DNS record TTL |
| `PROXIED` | `false` | βοΈ 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_TIMEOUT` | `5s` | β³ IP detection timeout |
| `UPDATE_TIMEOUT` | `30s` | β³ API request timeout |
| `EMOJI` | `true` | π¨ Enable emoji output |
| `QUIET` | `false` | π€« Suppress info output |
| `HEALTHCHECKS` | β | π Healthchecks.io URL |
| `UPTIMEKUMA` | β | π Uptime Kuma URL |
| `SHOUTRRR` | β | π Notification URLs (newline-separated) |
---
## π’ Deployment
### π³ Docker Compose
```yml
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:
```bash
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:
```bash
cargo build --release
sudo cp target/release/cloudflare-ddns /usr/local/bin/
```
2. Copy the systemd units from the `systemd/` directory:
```bash
sudo cp systemd/cloudflare-ddns.service /etc/systemd/system/
sudo cp systemd/cloudflare-ddns.timer /etc/systemd/system/
```
3. Place a `config.json` at `/etc/cloudflare-ddns/config.json` (the systemd service uses legacy config mode).
4. Enable the timer:
```bash
sudo systemctl enable --now cloudflare-ddns.timer
```
The timer runs the service every 15 minutes (configurable in `cloudflare-ddns.timer`).
## π¨ Building from Source
```bash
cargo build --release
```
The binary is at `target/release/cloudflare-ddns`.
### π³ Docker builds
```bash
# Single architecture (linux/amd64)
./scripts/docker-build.sh
# Multi-architecture (linux/amd64, linux/arm64, linux/ppc64le)
./scripts/docker-build-all.sh
```
## π» Supported Platforms
- π³ [Docker](https://docs.docker.com/get-docker/) (amd64, arm64, ppc64le)
- π [Docker Compose](https://docs.docker.com/compose/install/)
- βΈοΈ [Kubernetes](https://kubernetes.io/docs/tasks/tools/)
- π§ [Systemd](https://www.freedesktop.org/wiki/Software/systemd/)
- π macOS, πͺ Windows, π§ Linux β anywhere Rust compiles
---
## π 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
```bash
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:
```json
"authentication": {
"api_token": "Your cloudflare API token with Edit DNS capability"
}
```
Or with a legacy API key:
```json
"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:
```json
"a": true,
"aaaa": true
```
### βοΈ Config Options
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `cloudflare` | array | required | List of zone configurations |
| `a` | bool | `true` | Enable IPv4 (A record) updates |
| `aaaa` | bool | `true` | Enable IPv6 (AAAA record) updates |
| `purgeUnknownRecords` | bool | `false` | Delete stale/duplicate DNS records |
| `ttl` | int | `300` | DNS record TTL in seconds (30-86400, values < 30 become auto) |
Each zone entry contains:
| Key | Type | Description |
|-----|------|-------------|
| `authentication` | object | API token or API key credentials |
| `zone_id` | string | Cloudflare zone ID (found in zone dashboard) |
| `subdomains` | array | Subdomain entries to update |
| `proxied` | bool | Default proxied status for subdomains in this zone |
Subdomain entries can be a simple string or a detailed object:
```json
"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:
```json
{
"cloudflare": [{
"authentication": {
"api_token": "${CF_DDNS_API_TOKEN}"
},
...
}]
}
```
### π Example: Multiple Subdomains
```json
{
"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
```json
{
"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)
```yml
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):
```bash
cloudflare-ddns --repeat
cloudflare-ddns --repeat --dry-run
```
---
## π Helpful Links
- π [Cloudflare API token](https://dash.cloudflare.com/profile/api-tokens)
- π [Cloudflare zone ID](https://support.cloudflare.com/hc/en-us/articles/200167836-Where-do-I-find-my-Cloudflare-IP-address-)
- π [Cloudflare zone DNS record ID](https://support.cloudflare.com/hc/en-us/articles/360019093151-Managing-DNS-records-in-Cloudflare)
## π License
This project is licensed under the GNU General Public License, version 3 (GPLv3).
## π¨βπ» Author
Timothy Miller
[View my GitHub profile π‘](https://github.com/timothymiller)
[View my personal website π»](https://itstmillertime.com)