Ever tried hosting a server in your home network but annoyed at the fact that your public IP address keeps changing? Imagine for a moment that you're running a Nextcloud server located at nextcloud.example.com. All of a sudden, the public IP changes and your domain is still pointing to the old IP address. As a result, connectivity to your server gets disrupted. The change can happen at any time and without warning. It can take some time for you to find out. Once you do, you'll have to manually update the A-record of nextcloud.example.com to restore connectivity.

As you can see, spontaneous IP address changes are disruptive events; a static IP address or a DDNS service is essential for running public servers. Internet service providers usually offer static IP addresses only in their business plans. So this leaves out the vast majority of internet users. The other option, DDNS, is a feature supported by some routers. However, utilizing this feature typically involves having to sign-up for a hostname on a DDNS provider like No-IP. If you get one of No-IP's free accounts, then you must manually confirm hostnames every 30 days.

So, the options we have thus far require us to either:

  • Pay for a more expensive internet plan.
  • Utilize a router feature that requires creating a hostname on a third-party DDNS provider.

If only there was another way...

The Solution

The answer is to create our own DDNS. We can achieve a DDNS-like system with a shell script that runs every hour; scheduled via Cron. The script performs the following tasks everytime it runs:

  1. Obtain the current public IP address and the domain's A-record.
  2. Compare the public IP and the A-record's value.
  3. If they are not the same, it updates the A-record to the value of the public IP address.

This system provides some major advantages:

  • Can be deployed on any computer with a modern Linux installation.
  • Does not depend on a DDNS provider.
  • No need for a router with DDNS support.
  • The script is flexible and can be changed to use another domain registrar.

Requirements

  • Basic proficiency with Linux and its command line.

  • A computer with a Debian-based distribution installed.

    This computer will be referred to as the Host Computer. It will be online 24/7 to keep the scheduled script running.

  • Domain (or subdomain) with its DNS A-record set.

    Your domain registrar must have an API which provides the ability to read and update records. This guide will be using the GoDaddy API to update an A-record on GoDaddy.

  • Optional: Familiarity with JavaScript or TypeScript.

    Having some basic TypeScript knowledge allows you to modify or update the shell script.

Preparation

Host Computer

Perform these steps on the Host Computer.

Update the System

Open a terminal then enter the command below to update the system.

sudo apt -y update && sudo apt -y upgrade

Install Cron

Cron is included in most Linux distributions but we will install it just to be sure:

sudo apt install cron

Install Deno

Deno is a secure runtime environment for JavaScript and TypeScript. It is the environment for which the TypeScript-programmed shell script will be running under.

At the time of writing, Deno is not available in the APT repositories; we will be using the installation script instead.

Refer to the Deno documentation for additional installation options.

Run the following command to install Deno:

curl -fsSL https://deno.land/x/install/install.sh | sh

Confirm that Deno is properly installed by checking its version:

deno --version

An output like the one below indicates that Deno was successfully installed:

deno 1.38.3 (release, x86_64-unknown-linux-gnu)
v8 12.0.267.1
typescript 5.2.2

GoDaddy API Keys

Follow the GoDaddy API setup to create the API secret and key. Take note of the key and secret as they will be needed shortly.

Ensure that you get the API secret and key for the production environment; not the ones for the test environment.

The Script

The script sends requests to two endpoints from GoDaddy's Domains API:

Refer to the GoDaddy Domains API documentation for the complete list of endpoints.

The public IP address is obtained from an instance of IpMe at https://ipme.fosterhangdaan.com. Ipify can be used as an alternative.

Here is the content of the script:

#!/usr/bin/env -S deno run --allow-net

/**
 * Copyright (c) 2023 Foster Hangdaan <https://www.fosterhangdaan.com>
 * SPDX-License-Identifier: agpl-3.0-or-later
 *
 * A script which synchronizes a GoDaddy A-record with your public IP
 * address.
 */

// Change this value to your GoDaddy domain.
const domain = "example.com";

// Change this value to your GoDaddy API key.
const GODADDY_API_KEY = "key";

// Change this value to your GoDaddy API secret.
const GODADDY_API_SECRET = "secret";

// If you prefer to use Ipify instead, change this URL to:
// https://api.ipify.org
const publicIpUrl = "https://ipme.fosterhangdaan.com";

const domainParts = domain.split(".");
const domainRoot = domainParts.slice(-2).join(".");
const domainName = domainParts.slice(0,-2).join(".") || "@";

const goDaddyUrl = `https://api.godaddy.com/v1/domains/${domainRoot}/records/A/${domainName}`;

const goDaddyRequestHeaders = new Headers({
  Authorization: `sso-key ${GODADDY_API_KEY}:${GODADDY_API_SECRET}`,
});

const [ publicIpResponse, goDaddyResponse ] = await Promise.all([
  fetch(publicIpUrl),
  fetch(goDaddyUrl, { headers: goDaddyRequestHeaders }),
]);

if (!publicIpResponse.ok) {
  throw new Error(`Failed to fetch public IP address from ${publicIpUrl}.`);
} else if (!goDaddyResponse.ok) {
  throw new Error("Failed to fetch A-records from GoDaddy.");
}

const goDaddyJsonResponse = await goDaddyResponse.json();

if (goDaddyJsonResponse.length === 0) {
  throw new Error("No GoDaddy A-records found.");
}

const publicIP = await publicIpResponse.text();
const goDaddyIP = goDaddyJsonResponse[0]["data"];

if (publicIP !== goDaddyIP) {
  console.log(`The public IP address has changed. Public IP: ${publicIP}; Old IP: ${goDaddyIP}`);
  goDaddyRequestHeaders.append("Content-Type", "application/json");
  const response = await fetch(goDaddyUrl, {
    method: "PUT",
    headers: goDaddyRequestHeaders,
    body: JSON.stringify([
      {
        data: publicIP,
      }
    ]),
  });
  if (!response.ok) {
    throw new Error("Failed to update GoDaddy A-record.");
  } else {
    console.log(`GoDaddy A-record successfully updated to ${publicIP}.`);
  }
}

Schedule the Script to Run Every Hour with Cron

On the Host Computer, save the script as a file at /etc/cron.hourly/ddns.ts. Make sure to replace some necessary values:

  • Replace example.com in const domain = "example.com"; with your GoDaddy domain.
  • Replace key in const GODADDY_API_KEY = "key"; with your GoDaddy API key.
  • Replace secret in const GODADDY_API_SECRET = "secret"; with your GoDaddy API secret.

Grant read, write, and execute permissions only to the root user:

sudo chown root:root /etc/cron.hourly/ddns.ts
sudo chmod 700 /etc/cron.hourly/ddns.ts

Access to the script should be granted only to authorized users (such as root) since the script contains sensitive information: your GoDaddy API secret and key.

The script should now be scheduled to run every hour by Cron.

Conclusion

In this guide, we have setup an hourly Cron script that updates a GoDaddy domain's A-record if the public IP address changed. You can now rest assured that your hosted services will be protected from disruptions caused by public IP address changes.

Feel free to modify the script to your needs; especially if you are using a domain registrar other than GoDaddy.

As always, contact me for any comments or suggestions regarding this guide.