Featured image for Homebrew Dynamic DNS

Homebrew Dynamic DNS

Explainer

A little history

I run a GitLab server from home. It's cost-effective, the hardware requirements are pretty low, and I can add storage as and when I need it. It's also a lot cheaper than renting a VPS elsewhere (a VPS being a virtual private server, basically a rented slice of someone else's machine).

The snag is reaching it from outside the house. My work takes me all over the country, and that means getting at the repos remotely.

What I used to do was grab the server's public IP and either use it directly or update the DNS record by hand. DNS is the system that turns a domain name like classicniall.co.uk into the IP address a machine actually lives at; the A record is the specific entry that points a name at an IPv4 address.

Using DigitalOcean, I set up an A record pointing a subdomain at my home IP, so I could reach the server at something like repo.classicniall.co.uk. Handy, but the record needs updating every time my home IP changes, and it does change (a static IP from the ISP costs a fair bit more, so I don't have one).

So I wrote a little bash script to run whenever I lost access off the back of the IP changing.

The script

#!/bin/bash
IP=$(curl -s https://ipinfo.io/ip)

curl -X PUT -H "Content-Type: application/json" -H "Authorization: Bearer [API KEY]" -d '{"data":"'"${IP}"'"}' "https://api.digitalocean.com/v2/domains/[DOMAIN]/records/[RECORD ID]"

That's the whole thing to start with. Copy it into a new .sh file (I've called mine DynDNS.sh), make a few changes, and set the permissions to 744 so it's executable.

You need three values for it to work: your API key, your domain, and the record ID. Swap the placeholders out (so "[DOMAIN]" becomes "classicniall.co.uk").

Getting the API key

Straightforward. Go to your DigitalOcean API tokens page and generate a new token. Save it somewhere secure, copy it, and replace [API KEY] with it.

Getting the domain

Also straightforward: it's the domain the subdomain sits under. If you want repo.domain.com, you put domain.com here.

Getting the record ID

This one's less obvious. Run:

curl -X GET -H 'Content-Type: application/json' -H 'Authorization: Bearer [API KEY]' "https://api.digitalocean.com/v2/domains/[DOMAIN]/records"

That spits out every record for the domain. Find the A record for your subdomain and copy the id field (yes, lowercase). I ran this in Git Bash so the output came back unformatted, but it was the only way I knew to get the ID at the time; if you want it readable, pipe it through jq.

If you don't have an A record for the subdomain yet, create one manually first, then run the command again.

Running it

With the script created, edited, and set to 744, you run it by calling the file: ./DynDNS.sh. It's quick, about a second, and every time I've tested it it's grabbed the right public IP and updated the record.

A quick alias

To save typing the path out, add an alias. Open your .bash_aliases file and add:

alias DynDNS="./DynDNS.sh"

alias marks it as an alias, DynDNS is the word you type, and the bit in quotes is what actually runs. Save it, log out and back in, and DynDNS works as a command.

Making it update itself

Now, the original version of this post ended here with me admitting I didn't know how to automate it. Well, I do now, so let's finish the job.

The problem I flagged back then was efficiency. You could stick the script on a cron job (cron being the Linux scheduler that runs a command at set intervals) and have it fire every few minutes, but the script above calls the DigitalOcean API every single time, whether the IP has changed or not. That's a lot of pointless requests for something that only changes now and then.

The fix is to have the script remember the last IP it saw, and only talk to DigitalOcean when the current IP is different. You write the IP to a small file, and next run you compare against it.

#!/bin/bash

API_KEY="[API KEY]"
DOMAIN="classicniall.co.uk"
RECORD_ID="[RECORD ID]"
IP_CACHE="$HOME/.dyndns_last_ip"

# Current public IP
IP=$(curl -s https://ipinfo.io/ip)

# Last IP we recorded, if there is one
LAST_IP=""
[ -f "$IP_CACHE" ] && LAST_IP=$(cat "$IP_CACHE")

# Only bother DigitalOcean if it's actually changed
if [ "$IP" != "$LAST_IP" ]; then
    curl -s -X PUT \
        -H "Content-Type: application/json" \
        -H "Authorization: Bearer ${API_KEY}" \
        -d '{"data":"'"${IP}"'"}' \
        "https://api.digitalocean.com/v2/domains/${DOMAIN}/records/${RECORD_ID}"

    echo "$IP" > "$IP_CACHE"
    echo "$(date): IP changed to ${IP}, record updated."
fi

Now you can run it as often as you like and it'll keep quiet until there's a genuine change. Drop it on a cron job to check every five minutes:

*/5 * * * * $HOME/DynDNS.sh >> $HOME/dyndns.log 2>&1

Run crontab -e to edit your cron jobs, paste that line in, and save. The */5 means "every 5 minutes"; the rest sends anything the script prints to a log file, so you can see when it last did something. Five minutes is just what I use; make it one minute or fifteen, it barely matters now the API only gets hit on a real change.

And that's the part I couldn't figure out in 2019. It runs on a schedule but only calls the API when the IP has genuinely changed, so it's automated without being wasteful, which is what I was after in the first place.

dynamic dns · bash · DigitalOcean · DNS · self-hosting · cron · SysAdmin · GitLab