Fixing my DNS once and for all
I run a few services on my homelab, a small computer in my apartment. This means that I am subject to the whims of my residential internet service provider (ISP), who may change the IP address assigned to my router at any time.
I've been getting around this issue for years by using the integrated dynamic DNS service provided by my router's manufacturer. Many router manufacturers provide this service, letting you register some subdomain name which will be set to whatever IP address the router happens to be assigned, and update periodically.
My router happens to be made by TPLink, so my dynamic DNS records are of the form something.tplinkdns.com. Here's what my dynamic DNS settings look like in the router settings:

With dynamic DNS configured, I can then create a CNAME record for my apartment which points to the dynamic DNS record, which lets me have a nice memorable name in my own DNS space. Then, I can create other CNAME records for particular services (such as this blog) which point to that record, and everything resolves nicely.
Everything fine, until it wasn't
As mentioned above, I've been running a homelab for years this way, with no issue. When I move apartments, I plug the router back in, it gets a new IP, updates the dynamic DNS, and everything Just Works™.
The primary service in my homelab is Nextcloud, through which I self-host file syncing, contacts, calendar, notes, tasks, podcasts, and various Nextcloud apps. I'm rather reliant on these services, especially contacts and calendar, which sync to all my devices. My desktop and laptops all use the excellent GNOME Online Accounts integration as well as Thunderbird. My phone syncs via the DAVx5 app, which then populates the system contacts and calendar databases for other apps to use.
I use the Nextcloud snap, which means since I initially installed it and set it up years ago, it just automatically updates itself in the background and I basically never need to touch it. Twice I've noticed that it had been a while since updates, logged in, and saw that recent auto-refresh attempts had failed. After poking around a bit, I figured out that the bundled redis service was unhappy, so I restarted it, and the refresh proceeded as expected — a year or so later, the same issue happened again, so the fix was easy. All this is to say, I'm quite used to everything running as usual in the background, without issue.
Then a few days ago, I got a notification on my phone from AntennaPod failing to authenticate to the gpodder sync from my Nextcloud podcasts service. And when I booted up my work laptop on Monday, I got a warning from Thunderbird about expired security certificates for my nextcloud server, and after allowing the exception for the known good cert which happened to have expired, I received an endless series of pop-ups to confirm the exception. The pop-ups are always drawn overtop the usual Thunderbird window, which made catching up on emails an excercise in window management. Not ideal.
In short, my SSL certificate for my nextcloud server had expired, and it was getting in my way. So it was time to figure out why the certificate had stopped being autorenewed.
What happened?
I use certbot to manage all my SSL certificates, along with an nginx reverse proxy. When I want to set up a new service, I create the new nginx config file, set up the DNS record, and then run sudo certbot --nginx and select the domain and enable auto-renew. After that, certbot runs in the background, renewing certificates before they expire.
So why did my certs just expire?
I shelled into the machine which runs the reverse proxy and checked the certbot logs:
Aug 29 12:37:12 forum certbot.renew[480332]: All renewals failed. The following certificates could not be renewed:
Aug 29 12:37:12 forum certbot.renew[480332]: /etc/letsencrypt/live/mysubdomain.mydomain.com/fullchain.pem (failure)
Aug 29 12:37:12 forum certbot.renew[480332]: /etc/letsencrypt/live/another.mydomain.com/fullchain.pem (failure)
Aug 29 12:37:12 forum certbot.renew[480332]: /etc/letsencrypt/live/differentdomain.com/fullchain.pem (failure)
Aug 29 12:37:12 forum certbot.renew[480332]: /etc/letsencrypt/live/yetanotherdomain.com/fullchain.pem (failure)
Aug 29 12:37:12 forum certbot.renew[480332]: 4 renew failure(s), 0 parse failure(s)
Aug 29 12:37:12 forum systemd[1]: snap.certbot.renew.service: Main process exited, code=exited, status=1/FAILURE
Aug 29 12:37:12 forum systemd[1]: snap.certbot.renew.service: Failed with result 'exit-code'.
Aug 29 12:37:12 forum systemd[1]: Failed to start snap.certbot.renew.service - Service for snap application certbot.renew.
Aug 29 12:37:12 forum systemd[1]: snap.certbot.renew.service: Consumed 2.106s CPU time.
Hmm, not good, but unclear why. Let's try running sudo certbot --nginx interactively:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Processing /etc/letsencrypt/renewal/mysubdomain.mydomain.com.conf
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Renewing an existing certificate for mysubdomain.mydomain.com
Certbot failed to authenticate some domains (authenticator: nginx). The Certificate Authority reported these problems:
Identifier: mysubdomain.mydomain.com
Type: dns
Detail: DNS problem: SERVFAIL looking up A for mysubdomain.mydomain.com - the domain's nameservers may be malfunctioning; DNS problem: NXDOMAIN looking up AAAA for mysubdomain.mydomain.com - check that a DNS record exists for this domain
Hint: The Certificate Authority failed to verify the temporary nginx configuration changes made by Certbot. Ensure the listed domains point to this nginx server and that it is accessible from the internet.
Failed to renew certificate mysubdomain.mydomain.com with error: Some challenges have failed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
All renewals failed. The following certificates could not be renewed:
/etc/letsencrypt/live/mysubdomain.mydomain.com/fullchain.pem (failure)
/etc/letsencrypt/live/another.mydomain.com/fullchain.pem (failure)
/etc/letsencrypt/live/differentdomain.com/fullchain.pem (failure)
/etc/letsencrypt/live/yetanotherdomain.com/fullchain.pem (failure)
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
4 renew failure(s), 0 parse failure(s)
Ask for help or search for solutions at https://community.letsencrypt.org. See the logfile /var/log/letsencrypt/letsencrypt.log or re-run Certbot with -v for more details.
Okay, that's more interesting. NXDOMAIN? That stands for nonexistent domain. But strange, I can still connect to the server from outside my home network — I can ping it, ssh into it, even view the corresponding webpages if I force http instead of https. So the domain must exist.
The tool to look up DNS records is nslookup. Running this tool on the nextcloud subdomain reveals:
nslookup mysubdomain.mydomain.com
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
mysubdomain.mydomain.com canonical name = landing.mydomain.com
landing.mydomain.com canonical name = something.tplinkdns.com.
Name: something.tplinkdns.com
Address: <my real IP>
** server can't find something.tplinkdns.com: NXDOMAIN
What? It clearly resolved to my real (redacted) IP address, how can it not find the domain? I tried this on another domain that I own which is hosted on a VPS outside my network, and it worked, with the only notable difference being the lack of the last “server can't find ...: NXDOMAIN” line.
So that means something must be wrong with dynamic DNS provided by TPLink: it's able to resolve to the correct IP address, but it reports NXDOMAIN. Perhaps it's time to stop relying on my router's free dynamic DNS service.
What to do?
After some searching on DuckDuckGo for “self-host dynamic DNS”, I found that there are lots of options. Various sites provide hosted dynamic DNS sites, and separately there are various clients for interacting with them and other DNS providers.
Generally, there are two approaches:
- Sites which provide services similar to TPLink's dynamic DNS, where you register some custom name, then the service creates a subdomain for you, which you periodically poll to update.
- Sites which to which you grant direct access over your DNS records, either for your whole domain or a particular slice of subdomains.
Neither of these is particularly appealing to me. Relying on a free dynamic DNS service is exactly how I got into this problem in the first place, so I'd rather not do that again. And I already manage DNS records well through my domain registrar, I don't really want to split out DNS management between two services.
But it looks like my domain registrar (which happens to be Cloudflare) does provide API access for clients to manage dynamic DNS. So I just need a compatible client.
I ran snap search ddns, and one of the first results was a package called inadyn, which I noticed was packaged by Snapcrafters, which is an organization I trust with members I know personally. I checked out the source for the package, then found my way to the upstream inadyn project. Unfortunately, the project was archived a year ago, but fortunately, it seems to be well-known, feature complete, robust, and have a provider for my DNS provider.
The latest/edge channel of the inadyn snap has the most recent changes, though the default latest/stable seems to have been neglected for some years before that. snap info inadyn shows:
name: inadyn
summary: Internet Automated Dynamic DNS Client
publisher: Snapcrafters✪
store-url: https://snapcraft.io/inadyn
contact: https://github.com/snapcrafters/inadyn/issues
license: GPL-2.0+
description: |
Inadyn is a small and simple Dynamic DNS, DDNS, client with HTTPS support.
Commonly available in many GNU/Linux distributions, used in off the shelf
routers and Internet gateways to automate the task of keeping your Internet
name in sync with your public¹ IP address. It can also be used in installations
with redundant (backup) connections to the Internet.
This snap is maintained by the Snapcrafters community, and is not necessarily endorsed or
officially maintained by the upstream developers.
snap-id: HsYllvdShtMn4CWCWIuL9nksNo2UM1te
channels:
latest/stable: 2.7 2020-04-14 (335) 2.63MB -
latest/candidate: ↑
latest/beta: ↑
latest/edge: 2.13.0 2025-10-25 (793) 2.02MB -
That's fine, I'll use latest/edge, and ask the Snapcrafters to promote it to stable. Now to get it configured with access to my DNS provider...
Setting up DNS access
As mentioned, my DNS provider happens to be Cloudflare, for historical reasons. It seemed better than domains.google.com at the time, and checks notes I was correct. These days I would obviously look elsewhere, but migration is a pain, so I'm still using Cloudflare.
That aside, I see in the inadyn documentation that there's a provider for Cloudflare, and the configuration looks something like this:
# Create a unique custom API token with the following permissions:
# -> Zone.Zone - Read, Zone.DNS - Edit.
provider cloudflare.com {
username = zone.name
password = api_token_important_read_comment
hostname = hostname.zone.name
ttl = 1 # optional, value of 1 is 'automatic'.
proxied = false # optional.
}
So I'll need an API token. Some searching around led me to this page, which has no useful information on how to set up dynamic DNS access, though it did link to Cloudflare's DNS API docs with one important snippet of information for later:
A/AAAA records cannot exist on the same name as CNAME records.
I then found this other page, which was similarly useless. Finally, I found the page to actually manage Cloudflare API tokens. It looks something like this, which I assume is similar for other dynamic DNS providers:

Clicking “Create Token”, I was then led to a page to select a template for the token:

Lastly, after selecting “Edit zone DNS”, I got to the page where I could actually configure the token details as instructed in the inadyn README. The important pieces:
- Grant “Read” permission for “Zone”
- Grant “Edit” permission for “DNS”
Confusingly, there's also a dropdown for “Account”, “Zone”, or “User”. “Zone” there refers to scoping the access to a particular domain, rather than all domains associated with an account or user. The “Zone Resources” section similarly lets you restrict the token to accessing a particular “zone” (domain).
So when filled out fully, it should look something like this:

Finally, continue to summary, create the token, and keep the page open while I set up inadyn.
Setting up inadyn
Now that I had the access token ready, it's time to install and configure inadyn. On my server, that's just a matter of running:
sudo snap install --channel=latest/edge inadyn
(Again, I'm hoping the edge build will be promoted to stable shortly, as the project is archived and this build corresponds to its finished state.)
Now to write the config file. Running snap info inadyn reveals that it has two commands:
commands:
- inadyn.configpath
- inadyn
Great, seems I should write the config to the path given by inadyn.configpath. Except:
[I] oac@forum ~> inadyn.configpath
/var/snap/snapcraft/15950/inadyn.conf
Hmm, that certainly doesn't look right.
Earlier, when I first found the inadyn snap, I was looking through its snapcraft.yaml and saw something fishy:
echo "#!/bin/bash" > $SNAPCRAFT_PART_INSTALL/bin/configpath
echo "echo $SNAP_DATA/inadyn.conf" >> $SNAPCRAFT_PART_INSTALL/bin/configpath
Seems I was right, and there is indeed a bug in the snapcraft.yaml definition. The $SNAP_DATA environment variable is being evaluated by snapcraft at build time, rather than by inadyn at runtime. No matter, the fix is simple enough.
The path ought to be /var/snap/inadyn/current/inadyn.conf. So I opened that path via sudo vim /var/snap/inadyn/current/inadyn.conf, and put the following there:
# Create a unique custom API token with the following permissions:
# -> Zone.Zone - Read, Zone.DNS - Edit.
provider cloudflare.com {
username = mydomain.com
password = <my api token>
hostname = landing.mydomain.com
ttl = 1 # optional, value of 1 is 'automatic'.
proxied = false # optional.
}
After saving the file, I then ran sudo snap restart inadyn to restart the service. However, I checked the logs to see if it succeeded, and saw some errors:
[I] oac@forum ~> sudo snap logs inadyn
2026-09-03T01:47:39Z inadyn[555295]: HTTP 400: Cloudflare says our request was invalid. Possibly a malformed API token.
2026-09-03T01:47:39Z inadyn[555295]: Fatal error in DDNS server response: DDNS server response not OK
2026-09-03T01:47:39Z inadyn[555295]: Error response from DDNS server, exiting!
2026-09-03T01:47:39Z inadyn[555295]: Error code 48: DDNS server response not OK
2026-09-03T01:47:39Z systemd[1]: snap.inadyn.daemon.service: Main process exited, code=exited, status=48/n/a
2026-09-03T01:47:39Z systemd[1]: snap.inadyn.daemon.service: Failed with result 'exit-code'.
2026-09-03T01:47:39Z systemd[1]: snap.inadyn.daemon.service: Scheduled restart job, restart counter is at 5.
2026-09-03T01:47:39Z systemd[1]: snap.inadyn.daemon.service: Start request repeated too quickly.
2026-09-03T01:47:39Z systemd[1]: snap.inadyn.daemon.service: Failed with result 'exit-code'.
2026-09-03T01:47:39Z systemd[1]: Failed to start snap.inadyn.daemon.service - Service for snap application inadyn.daemon.
Cloudflare is returning an error code 400.
I checked my API token and the username and hostname, and all are correct according to the example. But then I remembered that note from the Cloudflare API docs:
A/AAAA records cannot exist on the same name as CNAME records.
For ease of management, my DNS records previously looked something like this:
CNAME mysubdomain.mydomain.com -> landing.mydomain.com
CNAME otherservice.mydomain.com -> landing.mydomain.com
CNAME thirdservice.mydomain.com -> landing.mydomain.com
CNAME landing.mydomain.com -> something.tplinkdns.com
All services hosted on my homelab had CNAME records pointing to the same subdomain (something like landing.mydomain.com), which in turn I could point at whatever provides my real IP address, such as TPLink's dynamic DNS service. If that changes, it's easy for me to change the single record to point wherever it needs to.
However, I hadn't yet adjusted my DNS records since trying to set up inadyn, so that CNAME record still existed, which violates the note from the Cloudflare docs. I deleted the DNS record for landing.mydomain.com from Cloudflare DNS, and then restarted inadyn via sudo snap restart inadyn. This time, everything worked as expected:
[I] oac@forum ~> sudo snap logs inadyn
2026-09-03T01:49:23Z systemd[1]: Started snap.inadyn.daemon.service - Service for snap application inadyn.daemon.
2026-09-03T01:49:23Z inadyn.daemon[555511]: ERROR: ld.so: object '/snap/inadyn/793/lib/libsnapcraft-preload.so' from LD_PRELOAD cannot be preloaded (cannot open shared object file): ignored.
2026-09-03T01:49:23Z inadyn[555511]: In-a-dyn version 2.13.0 -- Dynamic DNS update client.
2026-09-03T01:49:23Z inadyn[555511]: Guessing DDNS plugin '[email protected]' from 'cloudflare.com'
2026-09-03T01:49:23Z inadyn[555511]: Update forced for alias landing.mydomain.com, new IP# <my real IP>
2026-09-03T01:49:23Z inadyn[555511]: Updating IPv4 cache for landing.mydomain.com
(I'll ignore that ld.so error for now, probably another minor snapcraft.yaml packaging issue.)
Let's confirm that nslookup is actually happy now:
[I] oac@forum ~> nslookup mysubdomain.mydomain.com
Server: 127.0.0.53
Address: 127.0.0.53#53
Non-authoritative answer:
mysubdomain.mydomain.com canonical name = landing.mydomain.com
Name: landing.mydomain.com
Address: <my real IP>
Excellent! So now all that's left is to finally renew my certificates.
Renewing SSL certificates via certbot
Certbot will auto-renew certificates on a timer, but the nslookup is finally working, I want to renew certs immediately and be done swatting away security pop-ups.
Since I already have previous certificates configured for the services running on my homelab, all I need to do is run:
sudo certbot renew
After waiting rather a long time for certbot to perform the required challenges to confirm that each of the domains to renew has DNS records pointing to this machine, it finally succeeds.
The pop-ups stay gone, and I can visit my Nextcloud domain in a browser via https. All is well. And now, I'm not reliant on a free dynamic DNS service which may cease to work in the future. I control my own DNS, and I know how to change it in the future, if need be.
And now, so do you.



