DIY Dynamic DNS with Cloudflare: No More 2 AM Remote Access Failures

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

The 2 AM Connection Timed Out

I was sitting in a hotel room, three time zones away from my home office, staring at a blinking cursor. I needed a configuration file from my dev server to patch a critical bug. I typed ssh dev.itfromzero.com, hit enter, and waited. Five seconds passed. Ten. Then the dreaded message appeared: ssh: connect to host dev.itfromzero.com port 22: Connection timed out.

I knew the culprit immediately. My residential ISP had rotated my public IP address again. The DNS record for dev.itfromzero.com was pointing to a dead address, while my server was sitting on a new one I didn’t know. I was locked out of my own gear. It wasn’t just annoying; it was a total showstopper for my remote workflow.

The IP Carousel: Why Home Connections Break

Residential ISPs are notorious for this. They manage a pool of addresses and assign them via DHCP, often with 24-hour lease cycles. When that lease expires—or if your modem reboots after a 10-second power flicker—your IP changes. For someone scrolling through Netflix, this is invisible. For an engineer running a VPN or a private cloud, it’s a breaking change.

The real killer is the gap between the change and the update. If your IP flips at 1:00 AM but your DNS record stays on the old one, your server is essentially a ghost. To fix this, we need a way to detect the change and tell our DNS provider to update the record in near real-time.

The DDNS Landscape: Why Most Options Suck

I looked at the standard solutions before writing my own code. Here is how the field usually looks:

  • Legacy Providers (No-IP, DynDNS): They work, but the free tiers feel like hostage situations. You often have to click a link in an email every 30 days just to keep your hostname alive, or they force you to use subdomains like myhome.ddns.net.
  • Router-based DDNS: Most consumer routers have built-in support, but they are often hardcoded to specific (paid) providers. Worse, they offer zero logging. When they fail, you’re left guessing why.
  • Cloudflare API + Bash: This is the engineer’s choice. If you already use Cloudflare, you have a powerful API at your fingertips. It’s free, it works with your own domain, and you can script it to behave exactly how you want.

I’ve been running this setup on my own gear for over two years. It hasn’t missed a beat. It bypasses the limitations of commercial services while giving you total control over the update logic and error reporting.

The 30-Line Solution: Building Your Own Client

To get this running, you only need three things: a Cloudflare API Token, a few lines of Bash, and a way to schedule it. Bash is the perfect tool here—it’s native to every Linux distro, lightning-fast, and has zero overhead.

Step 1: Getting Your Keys to the Kingdom

Safety first: Do not use your Global API Key. If that leaks, a hacker could delete your entire account. Instead, create a scoped token:

  1. Log into Cloudflare and go to My Profile > API Tokens.
  2. Create a token using the Edit zone DNS template.
  3. Scope it specifically to the domain you want to update.
  4. Copy the token and store it in a password manager.

You’ll also need your Zone ID, found on the Overview page of your domain dashboard.

Step 2: The Logic Core

The logic is straightforward. We grab the current public IP, compare it against a local cache file, and—only if they differ—send a request to Cloudflare. Here is the script. I’ve kept it lean so you can see exactly what’s happening under the hood.

#!/bin/bash

# Configuration
AUTH_TOKEN="your_api_token_here"
ZONE_ID="your_zone_id_here"
RECORD_NAME="dev.itfromzero.com"
IP_FILE="/tmp/current_ip.txt"

# Get current public IP using a reliable external service
CURRENT_IP=$(curl -s https://api.ipify.org)

# Check if we have a cached IP to compare
if [ -f "$IP_FILE" ]; then
    OLD_IP=$(cat "$IP_FILE")
else
    OLD_IP=""
fi

# IP mismatch? Update it. Otherwise? Sleep.
if [ "$CURRENT_IP" = "$OLD_IP" ]; then
    echo "IP has not changed ($CURRENT_IP). Skipping update."
    exit 0
fi

echo "IP changed from $OLD_IP to $CURRENT_IP. Updating Cloudflare..."

# Fetch the unique Record ID from Cloudflare
RECORD_ID=$(curl -s -X GET "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records?name=$RECORD_NAME" \
     -H "Authorization: Bearer $AUTH_TOKEN" \
     -H "Content-Type: application/json" | jq -r '.result[0].id')

# Push the update
UPDATE_RESULT=$(curl -s -X PUT "https://api.cloudflare.com/client/v4/zones/$ZONE_ID/dns_records/$RECORD_ID" \
     -H "Authorization: Bearer $AUTH_TOKEN" \
     -H "Content-Type: application/json" \
     --data "{\"type\":\"A\",\"name\":\"$RECORD_NAME\",\"content\":\"$CURRENT_IP\",\"ttl\":1,\"proxied\":false}")

if [[ $UPDATE_RESULT == *"\"success\":true"* ]]; then
    echo "Update successful."
    echo "$CURRENT_IP" > "$IP_FILE"
else
    echo "Update failed! Check your token permissions."
    exit 1
fi

Pro tip: You’ll need jq installed (sudo apt install jq) to parse the JSON response. It’s the standard tool for handling API data in the terminal.

Step 3: Why This Script is “API-Friendly”

First, we use a local file (/tmp/current_ip.txt) to cache the last known IP. This ensures we aren’t hammering Cloudflare’s servers every few minutes if nothing has changed. While Cloudflare allows 1,200 requests per 5 minutes, being a good API citizen is just good engineering practice.

Setting "proxied": false is also critical. If you’re using this for SSH or a VPN, you want the traffic to hit your IP directly. Cloudflare’s standard proxy only handles web traffic (HTTP/S), so unless you’re paying for their Spectrum service, the proxy will actually block your SSH connection.

Set it and Forget it: Automation

A script is useless if you have to run it manually. You could use a simple cron job to check every 5 minutes:

*/5 * * * * /home/user/scripts/cloudflare-ddns.sh >> /var/log/ddns.log 2>&1

However, I prefer a Systemd Timer. Why? Because Systemd handles dependencies properly, logs directly to journalctl, and waits for the network to be online before firing. If the script fails because your modem is still rebooting, Systemd can retry with exponential backoff.

The Service File (/etc/systemd/system/cf-ddns.service)

[Unit]
Description=Cloudflare DDNS Update
After=network-online.target

[Service]
Type=oneshot
ExecStart=/home/user/scripts/cloudflare-ddns.sh
User=user

The Timer File (/etc/systemd/system/cf-ddns.timer)

[Timer]
OnBootSec=1min
OnUnitActiveSec=5min

[Install]
WantedBy=timers.target

Enable it with systemctl enable --now cf-ddns.timer. You now have a professional-grade DDNS client integrated directly into your system’s service manager.

Stability and Peace of Mind

Since I implemented this, I haven’t been locked out once. But don’t stop here. A script is only as good as its monitoring. Consider adding a simple health check—if the update fails, have the script ping a Telegram bot or a Discord webhook. You want to know there’s a problem *before* you’re trying to SSH in at 2 AM.

By building this yourself, you’ve cut out a middleman and gained a deeper understanding of DNS automation. It’s a 15-minute project that pays off every time your ISP decides to play musical chairs with your IP address.

Share: