Dynamic DNS on an EdgeRouter ER‑X with Cloudflare

This post walks through using a Ubiquiti EdgeRouter ER‑X to update Cloudflare DNS records whenever your home IP changes. It features a small bash script which uses the Cloudflare API to update records whenever the router’s WAN IP changes.

networking
Author

Mitchell O’Hara-Wild

Published

March 18, 2026

Modified

March 18, 2026

To make my self-hosted services more reliable while I’m travelling, I’ve migrated some critical services to run directly on my router. By offloading these tasks from my server, I remove a point of failure (server outages) from affecting the network. I use a Ubiquiti EdgeRouter X (ER-X) at home, and was inspired to make this change by Wesley Brewer’s post about running AdGuard Home directly on the router. In addition to running AdGuard Home on the router, I also moved my dynamic DNS service from Home Assistant’s Cloudflare integration to a short bash script. Running these services directly on the router ensures that my network is internet accessible, even if my server goes down - unless of course the router also goes down, but then I wouldn’t have internet anyway!

The Cloudflare API

The script uses the Cloudflare v4 API, specifically the PUT endpoint for updating an existing DNS record:

https://api.cloudflare.com/client/v4/zones/<zone_id>/dns_records/<record_id>

To authenticate with this endpoint you’ll need a Cloudflare API token. For dynamic DNS you need a token with Zone:Zone:Read and Zone:DNS:Edit permissions. Cloudflare’s documentation covers the full token creation process at developers.cloudflare.com/fundamentals/api/get-started/create-token/.

For each DNS record you want to update, you’ll also need a “Zone ID” and “Record ID”. First find the zone ID on the Overview page for your domain (right-hand sidebar under API). The record ID can be retrieved via the API (it’s not shown in the dashboard). In the terminal, use this curl command (replacing <your_zone_id> and <your_api_token>) to list all of the records (and their IDs) for that zone.

curl -s -X GET \
  "https://api.cloudflare.com/client/v4/zones/<your_zone_id>/dns_records" \
  -H "Authorization: Bearer <your_api_token>" \
  -H "Content-Type: application/json"

With the API token, Zone ID(s), and Record ID(s) in hand, we can use the Cloudflare API to update DNS records.

curl -s -X PUT \
  "https://api.cloudflare.com/client/v4/zones/<your_zone_id>/dns_records/<your_record_id>" \
  -H "Authorization: Bearer <your_api_token>" \
  -H "Content-Type: application/json" \
  --data "<json_formatted_record_data>"

The script below uses this API to automatically update DNS records via the Cloudflare API whenever the router’s public IP changes.

Dynamic DNS records

The script works in three steps:

  1. Get the current public IP

    The IP is read directly from the router’s WAN interface using ip addr rather than querying an third-party IP-echo service (for both speed and reliability).

  2. Compare IP against the cached IP

    The current public IP is compared with the last IP that this script updated Cloudflare with. Storing the previous IP in a local cache file removes the need for API calls or DNS requests.

  3. Update Cloudflare IP

    If the IP has changed, the script uses the above PUT request to the Cloudflare API to update the outdated DNS records. The last IP local cache is only written once the API reports a success, so if the request fails it will be retried when the script is next run.

/config/scripts/update-cloudflare-ddns.sh [59 lines]
#!/bin/vbash
source /opt/vyatta/etc/functions/script-template

# --- USER CONFIG ---
CF_API_TOKEN="<your_api_token>"
WAN_IF="eth0"                      # WAN interface with your public IPv4
CF_TTL=120                         # TTL in seconds
CACHE_FILE="/config/scripts/cloudflare-ddns-ip.cache"

# One entry per record:
# "ZONE_ID,RECORD_ID,NAME,TYPE,PROXIED"
# TYPE is usually A (IPv4) or AAAA (IPv6)
# PROXIED is true or false
RECORDS=(
  "<your_zone_id>,<your_record_id>,<your_record_name>,A,false"
)
# -------------------

# Get current IPv4 from WAN interface
current_ip=$(ip -4 addr show "$WAN_IF" | awk '/inet / {print $2}' | cut -d/ -f1)
[ -z "$current_ip" ] && exit 0

# Compare with cached value
if [ -f "$CACHE_FILE" ]; then
    cached_ip=$(cat "$CACHE_FILE")
else
    cached_ip=""
fi

# If unchanged, nothing to do
if [ "$current_ip" = "$cached_ip" ]; then
    exit 0
fi

# Update all records
all_ok=1
for rec in "${RECORDS[@]}"; do
    IFS=',' read -r zone_id rec_id rec_name rec_type rec_proxied <<< "$rec"

    # Only handle IPv4 A records here
    if [[ "$rec_type" != "A" ]]; then
        continue
    fi

    update_response=$(curl -s -X PUT \
      "https://api.cloudflare.com/client/v4/zones/$zone_id/dns_records/$rec_id" \
      -H "Authorization: Bearer $CF_API_TOKEN" \
      -H "Content-Type: application/json" \
      --data "{\"type\":\"$rec_type\",\"name\":\"$rec_name\",\"content\":\"$current_ip\",\"ttl\":$CF_TTL,\"proxied\":$rec_proxied}")

    if ! echo "$update_response" | grep -q '"success":true'; then
        all_ok=0
    fi
done

# Only write cache if all updates succeeded
if [ "$all_ok" -eq 1 ]; then
    echo "$current_ip" > "$CACHE_FILE"
fi

To use this script, replace <your_api_token> with your API token and enter the records to keep updated in the RECORDS array. This script allows multiple records across several zones to be updated, simply add an entry for each record into the RECORDS array. Finally, we’ll also need to make this script executable.

chmod +x /config/scripts/update-cloudflare-ddns.sh

The task scheduler on the ER-X can be used to regularly run this script - I’ve set the script to run every couple minutes. Since the IP rarely changes, the vast majority of runs will quickly hit the cache check and exit immediately (requiring no API calls, no network traffic).

configure
set system task-scheduler task cloudflare-ddns executable path /config/scripts/update-cloudflare-ddns.sh
set system task-scheduler task cloudflare-ddns interval 2m
commit
save
exit

Citation

BibTeX citation:
@online{oharawild2026erxdns,
  author = {O’Hara-Wild, Mitchell},
  title = {Dynamic {DNS} on an {EdgeRouter} {ER‑X} with {Cloudflare}},
  date = {2026-03-18},
  url = {https://mitchelloharawild.com/blog/erx-cloudflare-dns/},
  langid = {en}
}
For attribution, please cite this work as:
O’Hara-Wild, Mitchell. 2026. “Dynamic DNS on an EdgeRouter ER‑X with Cloudflare.” March 18, 2026. https://mitchelloharawild.com/blog/erx-cloudflare-dns/.