A feature-rich and robust Cloudflare DDNS updater with a small footprint. The program will detect your machine's public IP addresses and update DNS records using the Cloudflare API.
- π€ The Docker image takes less than 5 MB after compression.
- π The Go runtime re-uses existing HTTP connections.
- ποΈ Cloudflare API responses are cached to reduce the API usage.
- π You can simply list domains (e.g.,
www.a.org, hello.io
) without knowing their DNS zones. - π Internationalized domain names (e.g.,
π±.example.org
andζ₯ζ¬ο½‘coqjp
) are fully supported. - π Wildcard domains (e.g.,
*.example.org
) are also supported. - πΉοΈ You can toggle IPv4 (
A
records), IPv6 (AAAA
records) and Cloudflare proxying for each domain.
By default, public IP addresses are obtained via Cloudflare debugging page. This minimizes the impact on privacy because we are already using the Cloudflare API to update DNS records. Moreover, if Cloudflare servers are not reachable, chances are you cannot update DNS records anyways.
-
π Superuser privileges are immediately dropped, minimizing the impact of undiscovered bugs.
-
π‘οΈ The updater uses only HTTPS or DNS over HTTPS to detect IP addresses; see the Security Model.
-
π©Ί The updater can notify you via Healthchecks and Uptime Kuma when the updating fails.
-
π The updater uses only established open-source Go libraries (click to expand)
- cap:
The official Go binding of Linux capabilities. - cloudflare-go:
The official Go binding of Cloudflare API v4. - cron:
Parsing of Cron expressions. - go-retryablehttp:
HTTP clients with automatic retries and exponential backoff. - ttlcache:
In-memory cache to hold Cloudflare API responses. - mock (for testing only):
A comprehensive, semi-official framework for mocking. - testify (for testing only):
A comprehensive tool set for testing Go programs.
- cap:
(Click to expand the following items.)
π Directly run the Docker image.
docker run \
--network host \
-e CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \
-e DOMAINS=example.org,www.example.org,example.io \
-e PROXIED=true \
favonia/cloudflare-ddns
𧬠Directly run the updater from its source on Linux.
You need the Go tool to run the updater from its source.
CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN \
DOMAINS=example.org,www.example.org,example.io \
PROXIED=true \
go run github.com/favonia/cloudflare-ddns/cmd/ddns@latest
π For non-Linux operating systems, please use the Docker method instead.
Incorporate the following fragment into the compose file (typically docker-compose.yml
or docker-compose.yaml
).
version: "3"
services:
cloudflare-ddns:
image: favonia/cloudflare-ddns:latest
network_mode: host
# This makes IPv6 easier; see below
restart: always
# Restart the updater after reboot
cap_add:
- SETUID
# Capability to change user ID; needed for using PUID
- SETGID
# Capability to change group ID; needed for using PGID
cap_drop:
- all
# Drop all other capabilities
read_only: true
# Make the container filesystem read-only
security_opt:
- no-new-privileges:true
# Another protection to restrict superuser privileges
environment:
- PUID=1000
# Run the updater with user ID 1000
- PGID=1000
# Run the updater with group ID 1000
- CF_API_TOKEN=YOUR-CLOUDFLARE-API-TOKEN
# Your Cloudflare API token
- DOMAINS=example.org,www.example.org,example.io
# Your domains (separated by commas)
- PROXIED=true
# Tell Cloudflare to cache webpages and hide your IP
(Click to expand the following important tips.)
π CF_API_TOKEN
is your Cloudflare API token
The value of CF_API_TOKEN
should be an API token (not an API key), which can be obtained from the API Tokens page. Use the Edit zone DNS template to create and copy a token into the environment file. (The less secure API key authentication is deliberately not supported.)
π DOMAINS
is the list of domains to update
The value of DOMAINS
should be a list of fully qualified domain names (FQDNs) separated by commas. For example, DOMAINS=example.org,www.example.org,example.io
instructs the updater to manage the domains example.org
, www.example.org
, and example.io
. These domains do not have to be in the same zone---the updater will identify their zones automatically.
π¨ Remove PROXIED=true
if you are not running a web server
The setting PROXIED=true
instructs Cloudflare to cache webpages and hide your IP addresses. If you wish to bypass that and expose your actual IP addresses, remove PROXIED=true
. If your traffic is not HTTP(S), then Cloudflare cannot proxy it and you should probably turn off the proxying by removing PROXIED=true
. The default value of PROXIED
is false
.
π΄ Add IP6_PROVIDER=none
if you want to disable IPv6 completely
The updater, by default, will attempt to update DNS records for both IPv4 and IPv6, and there is no harm in leaving the automatic detection on even if your network does not work for one of them. However, if you want to disable IPv6 entirely (perhaps to avoid all the detection errors), add the setting IP6_PROVIDER=none
.
π‘ Expand this if you want IPv6 without using network_mode: host
The easiest way to enable IPv6 is to use network_mode: host
so that the updater can access the host IPv6 network directly. This has the downside of bypassing the network isolation. If you wish to keep the updater isolated from the host network, check out the experimental ip6tables
option. If your host OS is Linux, hereβs the tl;dr:
- Use
network_mode: bridge
instead ofnetwork_mode: host
. - Edit or create
/etc/docker/daemon.json
with these settings:{ "ipv6": true, "fixed-cidr-v6": "fd00::/8", "experimental": true, "ip6tables": true }
- Restart the Docker daemon (if you are using systemd):
systemctl restart docker.service
π‘οΈ Change PUID=1000
and PGID=1000
to the user and group IDs you want to use
Change 1000
to the user or group IDs you wish to use to run the updater. The settings cap_drop
, read_only
, and no-new-privileges
provide additional protection, especially when you run the container as a non-superuser. The updater itself will read PUID
and PGID
and attempt to drop all superuser privileges.
docker-compose pull cloudflare-ddns
docker-compose up --detach --build cloudflare-ddns
(Click to expand the following items.)
π Cloudflare accounts and API tokens
Name | Valid Values | Meaning | Required? | Default Value |
---|---|---|---|---|
CF_ACCOUNT_ID |
Cloudflare Account IDs | The Cloudflare account ID used to distinguish multiple DNS zones with the same name. It is not your email address! | No (in most cases you can leave it blank) | (unset) |
CF_API_TOKEN |
Cloudflare API tokens | The token to access the Cloudflare API | Exactly one of CF_API_TOKEN and CF_API_TOKEN_FILE should be set |
N/A |
CF_API_TOKEN_FILE |
Paths to files containing Cloudflare API tokens | A file that contains the token to access the Cloudflare API | Exactly one of CF_API_TOKEN and CF_API_TOKEN_FILE should be set |
N/A |
π Domains and IP providers
Name | Valid Values | Meaning | Required? | Default Value |
---|---|---|---|---|
DOMAINS |
Comma-separated fully qualified domain names or wildcard domain names | The domains the updater should manage for both A and AAAA records |
(See below) | (empty list) |
IP4_DOMAINS |
Comma-separated fully qualified domain names or wildcard domain names | The domains the updater should manage for A records |
(See below) | (empty list) |
IP6_DOMAINS |
Comma-separated fully qualified domain names or wildcard domain names | The domains the updater should manage for AAAA records |
(See below) | (empty list) |
IP4_PROVIDER |
cloudflare.doh , cloudflare.trace , local , url:URL , or none |
How to detect IPv4 addresses, or none to disable IPv4. (See below for the detailed description of each provider.) |
No | cloudflare.trace |
IP6_PROVIDER |
cloudflare.doh , cloudflare.trace , local , url:URL , or none |
How to detect IPv6 addresses, or none to disable IPv6. (See below for the detailed description of each provider.) |
No | cloudflare.trace |
π At least one of
DOMAINS
andIP4/6_DOMAINS
must be non-empty.At least one domain should be listed in
DOMAINS
,IP4_DOMAINS
, orIP6_DOMAINS
. Otherwise, if all of them are empty, then the updater has nothing to do. It is fine to list the same domain in bothIP4_DOMAINS
andIP6_DOMAINS
, which is equivalent to listing it inDOMAINS
. Internationalized domain names are supported using the non-transitional processing that is fully compatible with IDNA2008.
π Available providers for
IP4_PROVIDER
andIP6_PROVIDER
:
cloudflare.doh
Get the public IP address by queryingwhoami.cloudflare.
against Cloudflare via DNS-over-HTTPS and update DNS records accordingly.cloudflare.trace
Get the public IP address by parsing the Cloudflare debugging page and update DNS records accordingly. This is the default provider.local
Get the address via local network interfaces and update DNS records accordingly. When multiple local network interfaces or in general multiple IP addresses are present, the updater will use the address that would have been used for outbound UDP connections to Cloudflare servers.β οΈ You need access to the host network (such asnetwork_mode: host
in Docker Compose) for this policy, for otherwise the updater will detect the addresses inside the bridge network in Docker instead of those in the host network.url:URL
Fetch the content at a URL via the HTTP(S) protocol as the IP address. The provider format isurl:
followed by the URL. For example,IP4_PROVIDER=url:https://api4.ipify.org
will fetch the IPv4 addresses from https://api4.ipify.org, a server maintained by ipify.β οΈ Currently, the updater will not force IPv4 or IPv6 when retrieving the IPv4 or IPv6 address at the URL, and thus the service must either restrict its access to the correct IP network or return the correct IP address regardless of what IP network is used. As an example, https://api4.ipify.org has restricted its access to IPv4. The reason is that there are no elegant ways to force IPv4 or IPv6 using the Go standard library; please open a GitHub issue if you have a use case so that I might add some ugly hack to force it.none
Stop the DNS updating completely. Existing DNS records will not be removed.The option
IP4_PROVIDER
is governing IPv4 addresses andA
-type records, while the optionIP6_PROVIDER
is governing IPv6 addresses andAAAA
-type records. The two options act independently of each other; that is, you can specify different address providers for IPv4 and IPv6.Some technical details: For the providers
cloudflare.doh
andcloudflare.trace
, the updater will connect to the servers1.1.1.1
for IPv4 and2606:4700:4700::1111
for IPv6. Since version 1.9.3, the updater will switch to1.0.0.1
for IPv4 if1.1.1.1
appears to be blocked or intercepted by your ISP or your router (which is still not uncommon).
π What are wildcard domains?
Wildcard domains (
*.example.org
) represent all subdomains that would not exist otherwise. Therefore, if you have another subdomain entrysub.example.org
, the wildcard domain is independent of it, because it only represents the other subdomains which do not have their own entries. Also, you can only have one layer of*
---*.*.example.org
would not work.
β³ Schedules, triggers, and timeouts
Name | Valid Values | Meaning | Required? | Default Value |
---|---|---|---|---|
CACHE_EXPIRATION |
Positive time durations with a unit, such as 1h and 10m . See time.ParseDuration |
The expiration of cached Cloudflare API responses | No | 6h0m0s (6 hours) |
DELETE_ON_STOP |
Boolean values, such as true , false , 0 and 1 . See strconv.ParseBool |
Whether managed DNS records should be deleted on exit | No | false |
DETECTION_TIMEOUT |
Positive time durations with a unit, such as 1h and 10m . See time.ParseDuration |
The timeout of each attempt to detect IP addresses | No | 5s (5 seconds) |
TZ |
Recognized timezones, such as UTC |
The timezone used for logging and parsing UPDATE_CRON |
No | UTC |
UPDATE_CRON |
Cron expressions and the special value @once . See the documentation of cron for cron expressions. |
The schedule to re-check IP addresses and update DNS records (if necessary). If the special value @once is used, cron is disabled and the updater will terminate immediately after updating the DNS records. |
No | @every 5m (every 5 minutes) |
UPDATE_ON_START |
Boolean values, such as true , false , 0 and 1 . See strconv.ParseBool |
Whether to check IP addresses on start regardless of UPDATE_CRON |
No | true |
UPDATE_TIMEOUT |
Positive time durations with a unit, such as 1h and 10m . See time.ParseDuration |
The timeout of each attempt to update DNS records, per domain, per record type | No | 30s (30 seconds) |
β οΈ The update schedule does not take the time to update records into consideration. For example, if the schedule is βfor every 5 minutesβ, and if the updating itself takes 2 minutes, then the actual interval between adjacent updates is 3 minutes, not 5 minutes.
π£ Parameters of new DNS records
Name | Valid Values | Meaning | Required? | Default Value |
---|---|---|---|---|
PROXIED |
Boolean values, such as true , false , 0 and 1 . See strconv.ParseBool. π§ͺ See below for experimental support of per-domain proxy settings. |
Whether new DNS records should be proxied by Cloudflare | No | false |
TTL |
Time-to-live (TTL) values in seconds | The TTL values used to create new DNS records | No | 1 (This means βautomaticβ to Cloudflare) |
π The updater will preserve existing proxy and TTL settings until it has to create new DNS records (or recreate deleted ones). Only when it creates DNS records, the above settings will apply. To change existing proxy and TTL settings now, you can go to your Cloudflare Dashboard and change them directly. If you think you have a use case where the updater should actively overwrite existing proxy and TTL settings in addition to IP addresses, please let me know. It is not hard to implement optional overwriting.
π§ͺ Experimental per-domain proxy settings (subject to changes):
The
PROXIED
can be a boolean expression. Here are some examples:
PROXIED=is(example.org)
: proxy only the domainexample.org
PROXIED=is(example1.org) || sub(example2.org)
: proxy only the domainexample1.org
and subdomains ofexample2.org
PROXIED=!is(example.org)
: proxy every managed domain except forexample.org
PROXIED=is(example1.org) || is(example2.org) || is(example3.org)
: proxy only the domainsexample1.org
,example2.org
, andexample3.org
A boolean expression has one of the following forms (all whitespace is ignored):
- A boolean value accepted by strconv.ParseBool, such as
t
astrue
orFALSE
asfalse
.is(d)
which matches the domaind
. Note thatis(*.a)
only matches the wildcard domain*.a
; usesub(a)
to match all subdomains ofa
(including*.a
).sub(d)
which matches subdomains ofd
, such asa.d
andb.d
. It does not match the domaind
itself.! e
wheree
is a boolean expression, representing logical negation ofe
.e1 || e2
wheree1
ande2
are boolean expressions, representing logical disjunction ofe1
ande2
.e1 && e2
wheree1
ande2
are boolean expressions, representing logical conjunction ofe1
ande2
.One can use parentheses to group expressions, such as
!(is(a) && (is(b) || is(c)))
. For convenience, the engine also accepts these short forms:
is(d1, d2, ..., dn)
isis(d1) || is(d2) || ... || is(dn)
sub(d1, d2, ..., dn)
issub(d1) || sub(d2) || ... || sub(dn)
For example, these two settings are equivalent:
PROXYD=is(example1.org) || is(example2.org) || is(example3.org)
PROXIED=is(example1.org,example2.org,example3.org)
π‘οΈ Dropping superuser privileges
Name | Valid Values | Meaning | Required? | Default Value |
---|---|---|---|---|
PGID |
Non-zero POSIX group ID | The group ID the updater should assume | No | Effective group ID; if it is zero, then the real group ID; if it is still zero, then 1000 |
PUID |
Non-zero POSIX user ID | The user ID the updater should assume | No | Effective user ID; if it is zero, then the real user ID; if it is still zero, then 1000 |
π The updater will also try to drop supplementary group IDs.
ποΈ Logging, Healthchecks, and Uptime Kuma
Name | Valid Values | Meaning | Required? | Default Value |
---|---|---|---|---|
QUIET |
Boolean values, such as true , false , 0 and 1 . See strconv.ParseBool |
Whether the updater should reduce the logging | No | false |
EMOJI |
Boolean values, such as true , false , 0 and 1 . See strconv.ParseBool |
Whether the updater should use emojis in the logging | No | true |
HEALTHCHECKS |
Healthchecks ping URLs, such as https://hc-ping.com/<uuid> or https://hc-ping.com/<project-ping-key>/<name-slug> (see below) |
If set, the updater will ping the URL when it successfully updates IP addresses | No | (unset) |
π§ͺ UPTIMEKUMA (experimental) |
π§ͺ Uptime Kumaβs Push URLs, such as https://<host>/push/<id> . For convenience, you can directly copy the βPush URLβ from the Uptime Kuma configuration page. |
π§ͺ If set, the updater will ping the URL when it successfully updates IP addresses. UPDATE_CRON . |
No | (unset) |
π©Ί For
HEALTHCHECKS
, the updater can work with any server following the same notification protocol, including but not limited to self-hosted instances of Healthchecks. Both UUID and Slug URLs are supported, and the updater works regardless whether the POST-only mode is enabled.
β οΈ If using Healthchecks or Uptime Kuma, please note that a failure of IPv6 would be reported as down even if IPv4 records are updated successfully (and similarly if IPv6 works but IPv4 fails). If your setup does not support IPv6, please addIP6_PROVIDER=none
to disable IPv6 completely.
If you are using Docker Compose, run docker-compose up --detach
to reload settings.
(Click to expand the following items.)
I am migrating from oznu/cloudflare-ddns (now archived)
Old Parameter | Note | |
---|---|---|
API_KEY=key |
βοΈ | Use CF_API_TOKEN=key |
API_KEY_FILE=file |
βοΈ | Use CF_API_TOKEN_FILE=file |
ZONE=example.org and SUBDOMAIN=sub |
βοΈ | Use DOMAINS=sub.example.org directly |
PROXIED=true |
βοΈ | Same (PROXIED=true ) |
RRTYPE=A |
βοΈ | Both IPv4 and IPv6 are enabled by default; use IP6_PROVIDER=none to disable IPv6 |
RRTYPE=AAAA |
βοΈ | Both IPv4 and IPv6 are enabled by default; use IP4_PROVIDER=none to disable IPv4 |
DELETE_ON_STOP=true |
βοΈ | Same (DELETE_ON_STOP=true ) |
INTERFACE=iface |
βοΈ | Not required for local providers; we can handle multiple network interfaces |
CUSTOM_LOOKUP_CMD=cmd |
β | There are no shells in the minimal Docker image |
DNS_SERVER=server |
β | Only Cloudflare is supported, except the url:URL provider via HTTP(S) |
I am migrating from timothymiller/cloudflare-ddns
Old JSON Key | Note | |
---|---|---|
cloudflare.authentication.api_token |
βοΈ | Use CF_API_TOKEN=key |
cloudflare.authentication.api_key |
β | Please use the newer, more secure API tokens |
cloudflare.zone_id |
βοΈ | Not needed; automatically retrieved from the server |
cloudflare.subdomains[].name |
βοΈ | Use DOMAINS with fully qualified domain names (FQDNs) directly; for example, if your zone is example.org and your subdomain is sub , use DOMAINS=sub.example.org |
cloudflare.subdomains[].proxied |
π§ͺ | (experimental) Write boolean expressions for PROXIED to specify per-domain settings; see above for the detailed documentation for this experimental feature |
load_balancer |
β | Not supported yet; please make a request if you want it |
a |
βοΈ | Both IPv4 and IPv6 are enabled by default; use IP4_PROVIDER=none to disable IPv4 |
aaaa |
βοΈ | Both IPv4 and IPv6 are enabled by default; use IP6_PROVIDER=none to disable IPv6 |
proxied |
βοΈ | Use PROXIED=true or PROXIED=false |
purgeUnknownRecords |
β | The updater never deletes unmanaged DNS records |
Questions, suggestions, feature requests, and contributions are all welcome! Feel free to open a GitHub issue.