Skip to content

paulgit/cloudflare-ddns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Cloudflare Dynamic DNS  Version

Introduction

If you run a server from your home network, or want to access your home computer remotely, one challenge is tracking your public IP address, as most domestic internet connections use a dynamic IP that changes periodically. A Dynamic DNS (DDNS) service solves this by keeping a DNS record up to date automatically.

This script uses the Cloudflare API (v4) to act as a DDNS client. It detects your current public IP address and updates a Cloudflare DNS A record whenever a change is detected. It runs well on a Raspberry Pi, Ubuntu, Debian, or any modern Linux system with the prerequisites installed.


Prerequisites

Only two tools are required:

Tool Purpose
curl HTTP requests (public IP lookup and Cloudflare API)
jq JSON parsing (config file and API responses)

On Debian / Ubuntu:

sudo apt install curl jq

Note: dig is no longer required. The current DNS record value is now fetched directly from the Cloudflare API, which is more accurate than a DNS lookup and eliminates DNS propagation lag.


Installation

Quick install (recommended)

Download the script into ~/.local/bin and make it executable in one step. No sudo is required:

mkdir -p ~/.local/bin \
  && curl -fsSL \
    https://raw.githubusercontent.com/paulgit/cloudflare-ddns/master/cloudflare-ddns \
    -o ~/.local/bin/cloudflare-ddns \
  && chmod +x ~/.local/bin/cloudflare-ddns

Note: Inspect the script before running it if you prefer:

curl -fsSL https://raw.githubusercontent.com/paulgit/cloudflare-ddns/master/cloudflare-ddns | less

~/.local/bin is on $PATH by default on most modern Linux distributions (Ubuntu 20.04+, Fedora, Arch, Raspberry Pi OS). If cloudflare-ddns is not found after installation, add the directory to your shell profile:

echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

Install from source

Clone the repository and symlink (or copy) the script into your PATH:

git clone https://github.com/paulgit/cloudflare-ddns.git
mkdir -p ~/.local/bin
ln -s "$(pwd)/cloudflare-ddns/cloudflare-ddns" ~/.local/bin/cloudflare-ddns

Or copy it instead of symlinking:

git clone https://github.com/paulgit/cloudflare-ddns.git
mkdir -p ~/.local/bin
cp cloudflare-ddns/cloudflare-ddns ~/.local/bin/cloudflare-ddns
chmod +x ~/.local/bin/cloudflare-ddns

Verify the installation

cloudflare-ddns --version

Configuration

File format

Configuration is stored as a JSON file. The default location is:

~/.config/cloudflare-ddns/cloudflare-ddns.json

This respects the XDG Base Directory Specification. If the environment variable $XDG_CONFIG_HOME is set, it is used as the base instead of ~/.config.

A different path can be specified at runtime with the --config flag (see Command-Line Options below).

Creating the config file

Run the script once without a config file and it will create a template automatically:

./cloudflare-ddns

The template will be written to the default location with permissions 600. Open it in your editor and fill in your values:

{
  "auth_token": "YOUR_CLOUDFLARE_API_TOKEN",
  "zone_name": "example.com",
  "record_name": "ddns.example.com",
  "ip_check_urls": [
    "https://ipv4.icanhazip.com",
    "https://api.ipify.org"
  ]
}

Configuration fields

Field Required Description
auth_token Yes* Cloudflare API Token (recommended)
auth_email Yes* Account e-mail for legacy Global API Key auth
auth_key Yes* Global API Key for legacy auth
zone_name Yes The apex domain managed in Cloudflare (e.g. example.com)
record_name Yes The A record to keep updated (e.g. ddns.example.com)
ip_check_urls Yes Ordered array of URLs that return the public IPv4 address as plain text; the script tries each in sequence until one succeeds

* Provide either auth_token or both auth_email + auth_key.

Public IP URL fallback behavior

ip_check_urls is evaluated from top to bottom. If a URL is unreachable, times out, or returns an invalid value, the script automatically tries the next URL.

For backward compatibility, existing configs that still use ip_check_url (single string) continue to work. A warning is emitted so you can migrate to ip_check_urls in your next config update.

Authentication methods

Option A — API Token (recommended)

Create a scoped API Token at https://dash.cloudflare.com/profile/api-tokens with the following permissions:

  • Zone / DNS / Edit — scoped to the specific zone you want to update

A scoped token limits the blast radius if the credential is ever leaked; it can only edit DNS records in the zones you select.

{
  "auth_token": "your-api-token-here",
  "zone_name": "example.com",
  "record_name": "ddns.example.com",
  "ip_check_urls": [
    "https://ipv4.icanhazip.com",
    "https://api.ipify.org"
  ]
}

Option B — Legacy Global API Key

The Global API Key grants full access to your entire Cloudflare account. Use this only if you cannot use API Tokens. The script will print a deprecation warning each time it runs.

{
  "auth_email": "you@example.com",
  "auth_key": "your-global-api-key-here",
  "zone_name": "example.com",
  "record_name": "ddns.example.com",
  "ip_check_urls": [
    "https://ipv4.icanhazip.com",
    "https://api.ipify.org"
  ]
}

Command-Line Options

Usage: cloudflare-ddns [OPTIONS]

Options:
  -h, --help              Show this help message and exit.
  --version               Print the version number and exit.
  --config PATH           Path to the JSON configuration file.
                          Default: ~/.config/cloudflare-ddns/
                                   cloudflare-ddns.json
  --dry-run               Run the full check (load config, auth,
                          fetch public IP, read current DNS IP)
                          but do not apply any DNS changes.
                          Logs and prints what would have changed.
  --force                 Skip the DNS value check and update the
                          record immediately to the current public
                          IP. Log output clearly states the update
                          was forced. Compatible with --dry-run.
  --no-color, --cron-mode Disable coloured output. Colour is
                          also suppressed automatically when
                          not attached to a terminal or when
                          the NO_COLOR env var is set.

Examples

Use a custom config file:

./cloudflare-ddns --config /etc/cloudflare-ddns/config.json

Check what would happen without making any changes:

./cloudflare-ddns --dry-run

Force an immediate DNS update, skipping the current value check:

./cloudflare-ddns --force

Combine --force with --dry-run to see what a forced update would do without actually applying it:

./cloudflare-ddns --force --dry-run

Combine with a custom config and no colour:

./cloudflare-ddns --dry-run --config /etc/cloudflare-ddns/config.json --no-color

Disable colour output explicitly:

./cloudflare-ddns --no-color

Runtime Data

The script stores runtime data separately from the configuration:

Path Purpose
~/.local/state/cloudflare-ddns/cloudflare.ids Cached Cloudflare zone and record identifiers
~/.local/state/cloudflare-ddns/cloudflare.log Timestamped log of all INFO / WARN / ERROR events

The ~/.local/state/cloudflare-ddns/ directory is created automatically with permissions 700 on first run.

Identifier cache

On the first successful run the script fetches your zone and record identifiers from the Cloudflare API and stores them in cloudflare.ids. On subsequent runs these are loaded from the cache, avoiding an unnecessary API call. The cache is regenerated automatically if it is deleted or found to be incomplete.

Log file

All events are appended to cloudflare.log with UTC ISO-8601 timestamps, regardless of whether the terminal is a TTY. This makes the log useful when the script is invoked from cron. Example entries:

2024-06-01T08:00:01Z [INFO] ddns.example.com: 1.2.3.4 -> 5.6.7.8
2024-06-01T08:05:01Z [WARN] ID cache missing or incomplete; fetching from API.
2024-06-01T09:10:01Z [ERROR] Public IP check returned invalid value: ''.
2024-06-01T10:00:01Z [INFO] [FORCED] ddns.example.com: updated to 5.6.7.8 (DNS check skipped)

When --dry-run and --force are combined, the entry is prefixed with both labels:

2024-06-01T10:00:01Z [INFO] [DRY-RUN] [FORCED] Would update ddns.example.com to 5.6.7.8 (DNS check skipped)

Running from Cron

Add an entry to your crontab to run the script periodically. A check every five minutes is a reasonable starting point:

crontab -e
*/5 * * * * ~/.local/bin/cloudflare-ddns --cron-mode

The --cron-mode flag (equivalent to --no-color) prevents ANSI colour escape codes from appearing in cron mail. Colour is also disabled automatically when the script detects it is not attached to a terminal, so the flag is optional but recommended for clarity.

Note: cron does not expand ~ in command paths on all systems. Use the full path (/home/youruser/.local/bin/cloudflare-ddns) or the $HOME variable if ~ does not work in your crontab.

If you use a non-default config location, pass --config as well:

*/5 * * * * ~/.local/bin/cloudflare-ddns --cron-mode --config ~/.config/cloudflare-ddns/alt.json

Suppressing cron mail on no-change runs

The script produces no output when the IP address has not changed, so cron will not send mail on the majority of runs. Output (and therefore mail) is only generated when the record is updated, a warning is raised, or an error occurs.

Note: When --force is used, the DNS record is always updated and output is always produced, regardless of whether the IP has changed. Avoid using --force in cron unless you specifically want this behaviour.


Concurrency Protection

A per-user lock directory is created at /tmp/cloudflare-ddns-<uid>.lock before any network activity begins. This prevents two cron instances from running simultaneously if a previous invocation is still in progress (e.g. due to a slow network). The lock is removed automatically on exit, including on error.


Colour Output

Colour is enabled automatically when the script's output is connected to a terminal (TTY). It is suppressed in any of the following cases:

  • The NO_COLOR environment variable is set (any value) — see https://no-color.org/
  • The --no-color or --cron-mode flag is passed
  • stdout and stderr are both redirected (e.g. cron, pipes)

Security Notes

  • The config file and identifier cache are both created and maintained with permissions 600 (owner read/write only).
  • The config directory (~/.config/cloudflare-ddns/) and data directory (~/.local/state/cloudflare-ddns/) are maintained with permissions 700.
  • The config file is parsed as JSON via jq — it is never executed as shell code.
  • API credentials are never written to the log file.
  • Use an API Token (Option A) rather than the Global API Key: a token can be revoked independently and grants only the minimum required permission.

Troubleshooting

Symptom Likely cause
'jq' is required but not installed Install jq: sudo apt install jq
Config file contains invalid JSON Syntax error in your config file — validate with jq . ~/.config/cloudflare-ddns/cloudflare-ddns.json
No valid auth found auth_token (or auth_email/auth_key) still contains the placeholder value
Zone '…' not found zone_name does not match any zone in your Cloudflare account
Record '…' not found The A record does not exist in Cloudflare yet — create it manually first
Another instance is already running A previous run is still active; or the lock was left behind — remove /tmp/cloudflare-ddns-<uid>.lock
Failed to get public IP The ip_check_url is unreachable; try curl https://ipv4.icanhazip.com manually
DNS record keeps updating unexpectedly Run with --dry-run to inspect what public IP and DNS IP are being detected without making changes
Need to force an update regardless of current DNS value Use --force to skip the DNS value check and update immediately; combine with --dry-run to preview the action first

Credits

Thanks to teddysun and others whose scripts provided inspiration.

Written by Paul Git and Claude AI.


Disclaimer

No warranties are given for correct function. Use at your own risk.

About

Cloudflare as a Dynamic DNS Provider

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages