Namecheap Dynamic DNS on Linux: Bash Script and Cron

by Fahim

Residential ISPs quietly drop or reassign your public IP whenever your DHCP lease rolls over or your router restarts. If you host WireGuard, Nextcloud, or an SSH jump box at home, you’re suddenly locked out. We’re going to fix that with a small, bulletproof Bash script that detects external IP changes, hits Namecheap’s Dynamic DNS API, and runs cleanly from cron every 15 minutes.

Most tutorials push ddclient for this. I ran ddclient for years on Debian and Ubuntu until an upstream package update silently choked on Namecheap’s XML response, locking me out of my VPN while I was traveling. Swapping a bloated Perl daemon for a 45-line Bash script and standard system utilities was a no-brainer: zero third-party dependencies, runs in under 400ms, and writes dead-simple log lines you can actually debug.

Namecheap Dynamic DNS on Linux: Bash Script and Cron
Namecheap Dynamic DNS on Linux: Bash Script and Cron

Enable Dynamic DNS in Namecheap

Before writing any Bash, you need an API update password from Namecheap. This is completely separate from your main account password.

  1. Log into your Namecheap dashboard and open Domain List.
  2. Click Manage next to the domain you’re targeting.
  3. Switch to the Advanced DNS tab.
  4. Scroll down to Dynamic DNS and toggle it ON.
  5. Copy the alphanumeric string in the Dynamic DNS Password field.

Now head up to the Host Records table on that same page. You need a record ready for the script to touch. Hit Add New Record and pick A + Dynamic DNS Record from the dropdown.

Set Host to whatever subdomain you want (like home or vpn), or use @ for the apex domain. Fill Value with a dummy IP like 127.0.0.1 and set TTL to Automatic (or 5 minutes). If you’re organizing more complex DNS setups, check out our guide on how to point a Namecheap apex domain to a hostname target.

The Anatomy of Namecheap’s DDNS API

Namecheap doesn’t use a complicated JSON REST payload here. It’s just a raw HTTP GET endpoint that takes query parameters and returns an XML string.

The endpoint looks like this:

https://dynamicdns.park-your-domain.com/update?host=[HOST]&domain=[DOMAIN]&password=[PASSWORD]&ip=[IP]

If you leave out the &ip= parameter, Namecheap tries to grab your IP from the incoming HTTP connection. Don’t do that. If you’re behind an ISP proxy, dual-stack connection, or CGNAT, Namecheap will often latch onto the wrong upstream gateway. Explicitly detecting your IPv4 address first and passing it in the query string avoids weird surprises.

When the update goes through, Namecheap returns this XML payload:

 SETDNSHOST eng 0 0 true 

If your credentials fail or the target host record doesn’t exist yet, ErrCount jumps above 0 and the XML will show nested tags. You can check the full parameter breakdown in the Namecheap Dynamic DNS HTTP documentation.

Step 1: Storing Credentials Securely

Don’t hardcode passwords directly inside scripts. If you ever push your home-lab configs or dotfiles to Git, that credential is gone. Keep your configuration in a root-owned directory.

Create the config folder in /etc:

sudo mkdir -p /etc/namecheap-ddns
sudo chmod 700 /etc/namecheap-ddns

Create /etc/namecheap-ddns/ddns.conf:

# Namecheap Dynamic DNS Configuration
DDNS_DOMAIN="example.com"
DDNS_HOST="home"
DDNS_PASSWORD="48a1c92d54e84b7285a8efb7a0f3d991"
CACHE_FILE="/var/cache/namecheap-ddns/current_ip.txt"
LOG_FILE="/var/log/namecheap-ddns.log"

Lock down permissions so unprivileged users can’t read the file:

sudo chmod 600 /etc/namecheap-ddns/ddns.conf
sudo mkdir -p /var/cache/namecheap-ddns
sudo chmod 755 /var/cache/namecheap-ddns

Step 2: Writing the Update Script

A reliable DDNS script needs three things: local IP caching so you don’t spam Namecheap on every tick, sensible timeouts so an unresponsive API doesn’t stall cron jobs, and basic XML response checking.

Save this script to /usr/local/bin/namecheap-ddns.sh:

#!/usr/bin/env bash
set -euo pipefail
CONFIG_FILE="/etc/namecheap-ddns/ddns.conf"
if [[ ! -f "$CONFIG_FILE" ]];
then
echo "[$(date '+%Y-%m-%d %H:%M:%S')] Config file not found: $CONFIG_FILE" >&2
exit 1
fi
source "$CONFIG_FILE"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
}
# 1. Fetch current public IPv4 address with a 5-second timeout
CURRENT_IP=$(curl -4 -s --max-time 5 https://dynamicdns.park-your-domain.com/getip || true)
# Fallback if Namecheap getip is unreachable
if [[ -z "$CURRENT_IP" || ! "$CURRENT_IP" =~ ^[0-9]+.[0-9]+.[0-9]+.[0-9]+$ ]];
then
CURRENT_IP=$(curl -4 -s --max-time 5 https://api.ipify.org || true)
fi
# Validate IP format
if [[ ! "$CURRENT_IP" =~ ^[0-9]+.[0-9]+.[0-9]+.[0-9]+$ ]];
then
log "ERROR: Failed to retrieve a valid IPv4 address."
exit 1
fi
# 2. Compare against cached IP
CACHED_IP=""
if [[ -f "$CACHE_FILE" ]];
then
CACHED_IP=$(cat "$CACHE_FILE")
fi
if [[ "$CURRENT_IP" == "$CACHED_IP" ]];
then
# IP has not changed;
exit silently to respect API limits
exit 0
fi
log "IP change detected. Old: '${CACHED_IP:-NONE}', New: '${CURRENT_IP}'. Updating Namecheap..."
# 3. Call Namecheap Update API
UPDATE_URL="https://dynamicdns.park-your-domain.com/update?host=${DDNS_HOST}&domain=${DDNS_DOMAIN}&password=${DDNS_PASSWORD}&ip=${CURRENT_IP}"
RESPONSE=$(curl -s --max-time 10 "$UPDATE_URL")
# 4. Parse the XML response for errors
if echo "$RESPONSE" | grep -q '0';
then
echo "$CURRENT_IP" > "$CACHE_FILE"
log "SUCCESS: DNS record for ${DDNS_HOST}.${DDNS_DOMAIN} updated to ${CURRENT_IP}."
else
ERR_MSG=$(echo "$RESPONSE" | grep -oP '(?<=).*?(?=)' || echo "Unknown API response")
log "ERROR: Namecheap API rejected update. Reason: ${ERR_MSG}"
exit 1
fi

The key part is the local cache file. Namecheap will throttle or temporarily block clients that send frequent redundant updates. By caching the current IP in /var/cache/namecheap-ddns/current_ip.txt, the script bails out immediately if your IP hasn’t shifted.

Mark the script executable and restrict it to root:

sudo chown root:root /usr/local/bin/namecheap-ddns.sh
sudo chmod 750 /usr/local/bin/namecheap-ddns.sh

Step 3: Test Run and Verifying the Output

Always test the script manually once before letting cron take over so you can verify directory permissions and log writes.

Run it as root:

sudo /usr/local/bin/namecheap-ddns.sh

Check the log file:

sudo cat /var/log/namecheap-ddns.log

You should see an IP change recorded alongside a successful response:

[2024-04-18 10:14:02] IP change detected. Old: 'NONE', New: '198.51.100.42'. Updating Namecheap...
[2024-04-18 10:14:03] SUCCESS: DNS record for home.example.com updated to 198.51.100.42.

Run the script a second time right away. Check the log again. You shouldn’t see any new lines—the detected IP matches the cache file, so it exited early without hitting the API.

To verify that Namecheap actually updated their nameservers, query them directly with dig or host:

dig +short home.example.com @dns1.registrar-servers.com

If that command returns your current external IP, your record is active. If you’re building out subdomains on this domain later, take a look at how to handle wildcard subdomains with Certbot DNS challenge.

Step 4: Scheduling Execution with Cron

Now we automate it every 15 minutes. Make sure you add this to root’s crontab—running it under a standard user account will fail when writing to /var/log or /var/cache.

Open root’s crontab:

sudo crontab -e

Append the schedule line at the end:

# Namecheap Dynamic DNS sync every 15 minutes
*/15 * * * * /usr/local/bin/namecheap-ddns.sh > /dev/null 2>&1

Why 15 minutes? Most residential IPs only rotate every few weeks or months. Checking every 15 minutes means you’ll recover from an unexpected ISP reset within a quarter of an hour without putting needless load on external APIs. If you’re running time-sensitive inbound webhooks, you can safely drop it to */5 * * * * because our local cache guard prevents duplicate API calls. For more on setting up reliable cron jobs, see our guide on how to disable WP-Cron and set up a real cron job.

Step 5: Setting Up Logrotate

Even though this script only logs updates and errors, running it indefinitely on a small server or Raspberry Pi will slowly inflate /var/log/namecheap-ddns.log. Standard rotation keeps things tidy.

Drop a logrotate config into /etc/logrotate.d/namecheap-ddns:

/var/log/namecheap-ddns.log { monthly rotate 6 missingok notifempty compress delaycompress create 0640 root root
}

Do a dry run to make sure logrotate parses the file cleanly:

sudo logrotate -d /etc/logrotate.d/namecheap-ddns

If logrotate runs through without complaints, you won’t have to worry about runaway logs eating disk space.

Break It: Common Failures and Edge Cases

Dynamic DNS problems usually fail silently. These are the three main bugs I’ve run into across different machines and networks.

1. The “Domain name not found” Error

If your log spits out:

ERROR: Namecheap API rejected update. Reason: Domain name not found

This means your host and domain split is wrong. Open /etc/namecheap-ddns/ddns.conf and double-check. If your full URL is vpn.example.com, DDNS_DOMAIN must be example.com and DDNS_HOST must be vpn. Don’t put vpn.example.com into DDNS_DOMAIN. Also make sure the A + Dynamic DNS Record actually exists in the Namecheap UI—the API won’t create a non-existent record out of thin air.

2. Dual-Stack IPv6 Interference

If your ISP hands your router both IPv4 and IPv6 subnets, running a basic curl https://api.ipify.org might spit back an IPv6 address (like 2600:...). Namecheap’s Dynamic DNS API strictly demands a 32-bit IPv4 address for dynamic A records.

That’s why the script uses the -4 flag:

curl -4 -s --max-time 5 https://dynamicdns.park-your-domain.com/getip

The -4 flag forces curl to resolve and connect over IPv4 only. Check the curl documentation if you need to route requests through specific local interfaces or proxies.

3. The ISP Carrier-Grade NAT (CGNAT) Trap

If the script succeeds and updates your DNS, but you still can’t connect from the outside, you’re probably behind CGNAT. With CGNAT, your ISP doesn’t give you a dedicated public IPv4 address—your router shares an external gateway IP with hundreds of other households.

To confirm, open your router’s status page and check the WAN/Internet IP. If that address falls in the 100.64.0.0/10 range (anything from 100.64.x.x to 100.127.x.x), you’re behind CGNAT. Dynamic DNS cannot fix this because inbound traffic cannot be forwarded to your router. In that case, you’ll need to ask your ISP for a static IP or use a tunnel like Cloudflare Tunnels, Tailscale, or a VPS reverse proxy—similar to what we cover in our guide to deploying Docker Compose on a VPS.

Frequently Asked Questions

Can I use this script to update multiple subdomains at once?

Yes, but Namecheap needs an individual HTTP request for each record. You can adapt the script to loop over an array like DDNS_HOSTS=("vpn" "home" "@") and issue the curl command sequentially using the same resolved IP.

Does Namecheap charge extra for Dynamic DNS?

No, it’s free with any domain using Namecheap’s default BasicDNS nameservers. If you switch to custom nameservers (like Cloudflare or AWS Route 53), Namecheap’s DDNS service stops handling queries for that zone.

Why not use ddclient instead of writing a custom script?

ddclient drags in Perl and a chain of library dependencies. On minimal servers or embedded SBCs, it’s unnecessary overhead. A small Bash script relying purely on curl and standard coreutils is faster, uses fewer resources, and won’t break on distribution upgrades.

How fast does the DNS update propagate after the script runs?

Namecheap sets the TTL on dynamic DNS records to 300 seconds (5 minutes). Once the script makes a successful call, upstream resolvers will pick up your new IP as soon as their cached 5-minute window expires.

Once your script is running reliably, make sure your router forwards the necessary ports to your internal server. If you’re hosting web services, route them through a reverse proxy that automates SSL renewals.

all_in_one_marketing_tool