π Cloudflare DDNS
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.
π Highlights
β‘ Efficiency
π€ The Docker image takes less than 3 MB (after compression).π The Go runtime re-uses existing HTTP connections.ποΈ Cloudflare API responses are cached to reduce the API usage.
π― Complete Support of Domain Names
π 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.
π΅οΈ Privacy
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.
π‘οΈ Security
-
π 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 supports Healthchecks, which can notify you 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. - mock (for testing only):
A comprehensive, semi-official framework for mocking. - testify (for testing only):
A comprehensive tool set for testing Go programs. - ttlcache:
In-memory cache to hold Cloudflare API responses.
- cap:
β·οΈ Quick Start
(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
π Deployment with Docker Compose
π¦ Step 1: Updating the Compose File
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 turn off the proxying by removing PROXIED=true
. The default value of PROXIED
is false
.
π‘ 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.
π Step 2: Building the Container
docker-compose pull cloudflare-ddns
docker-compose up --detach --build cloudflare-ddns
ποΈ Further Customization
βοΈ All Settings
(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 account ID used to distinguish multiple zone IDs with the same name | No | (unset) |
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 |
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 |
π€· In most cases,CF_ACCOUNT_ID
is not needed.
π 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 , and none |
How to detect IPv4 addresses. (See below) | No | cloudflare.trace |
IP6_PROVIDER |
cloudflare.doh , cloudflare.trace , local , and none |
How to detect IPv6 addresses. (See below) | No | cloudflare.trace |
π At least one ofDOMAINS
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 forIP4_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.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. See the documentation of cron. |
The schedule to re-check IP addresses and update DNS records (if necessary) | 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.
π§ͺ Experimental mode to disable cron:UPDATE_CRON
can be set to@disabled
(or@nevermore
as an alias); the updater will terminate immediately after updating the DNS records. This is useful when you want to use other mechanisms to schedule the updating (e.g., CronJob).
π£ 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. |
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 and Healthchecks
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) |
π©Ί ForHEALTHCHECKS
, 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.
π Restarting the Container
If you are using Docker Compose, run docker-compose up --detach
to reload settings.
π΅ Migration Guides
(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 |
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 |
π Feedback
Questions, suggestions, feature requests, and contributions are all welcome! Feel free to open a GitHub issue.