DNS failover makes your domain answer with a backup server's IP address when the primary server stops responding. Instead of a cron script that rewrites zone files, you can let the authoritative DNS server run the health checks itself. PowerDNS Authoritative Server supports LUA records, whose answers are computed at query time: the ifurlup() function checks a URL on each candidate address and returns only the healthy ones. In this tutorial you will install PowerDNS on Ubuntu 24.04, serve a zone with a health-checked failover record and test that it switches when the primary fails.
How DNS failover works (and its limits)
With the configuration in this guide, PowerDNS checks https://www.your_domain/healthz on the primary and backup servers every few seconds. While the primary is healthy, queries for www.your_domain return only the primary IP. When the primary fails its checks, the same query returns the backup IP.
Keep these limits in mind:
- Resolvers cache answers for the record's TTL. With a 30-second TTL, most clients move to the backup within a minute, but some resolvers and applications cache longer than the TTL.
- DNS failover redirects new connections only. Open connections to the failed server are lost.
- The backup must be able to serve the site: files, database and TLS certificates must already be there.
- Your nameservers must be highly available themselves. Run at least two, in different locations.
If you need sub-second failover within one location, a floating IP or a load balancer is a better fit; DNS failover shines for failover between sites.
Prerequisites
To follow this guide you need:
- Two web servers serving the same site: a primary (
primary_ip) and a backup (backup_ip). Both must respond onhttps://www.your_domain/healthzwith HTTP 200 and the bodyok, using a valid certificate forwww.your_domain. - Two servers running Ubuntu 24.04 LTS for the nameservers, each with a non-root sudo user and a public IP (
ns1_ipandns2_ip). Small CubePath VPS instances in different locations work well. This guide shows the setup onns1; repeat it identically onns2. - A registered domain (
your_domain) whose registrar lets you set custom nameservers and glue records. - TCP and UDP port 53 open to the internet on both nameservers.
Step 1 - Creating the health check endpoint
The health check should confirm that the application really works, not just that Nginx is running. A good endpoint checks the database connection and returns ok. For a quick start, a static endpoint in Nginx is enough. On both web servers, add this block to the site's server block:
location = /healthz {
access_log off;
default_type text/plain;
return 200 "ok";
}
Reload Nginx and test from any machine:
curl -s --resolve www.your_domain:443:primary_ip https://www.your_domain/healthz
curl -s --resolve www.your_domain:443:backup_ip https://www.your_domain/healthz
okok
Both servers must answer. If the backup fails here, failover would send traffic to a broken server.
Step 2 - Installing PowerDNS
Ubuntu 24.04 ships PowerDNS Authoritative Server 4.8 with the BIND zone file backend as a separate package. Install both on ns1:
sudo apt update
sudo apt install pdns-server pdns-backend-bind
The package starts the service right away. Ubuntu's systemd-resolved already listens on 127.0.0.53:53, so PowerDNS must bind only to the public IP. You will set that in the next step.
Step 3 - Configuring PowerDNS
Keep a copy of the packaged configuration:
sudo cp /etc/powerdns/pdns.conf /etc/powerdns/pdns.conf.orig
Open the main configuration file:
sudo nano /etc/powerdns/pdns.conf
Replace its contents with the following. Replace ns1_ip with the server's public IPv4 address:
# Serve zones from BIND-style zone files
launch=bind
bind-config=/etc/powerdns/named.conf
# Listen only on the public address (systemd-resolved uses 127.0.0.53)
local-address=ns1_ip
local-port=53
# Enable LUA records and their health checks
enable-lua-records=yes
lua-health-checks-interval=5
Because this file no longer contains an include-dir line, the drop-in files under /etc/powerdns/pdns.d/ are not loaded, and this file is the complete configuration.
Create the file that lists the zones:
sudo nano /etc/powerdns/named.conf
zone "your_domain" {
type master;
file "/etc/powerdns/zones/your_domain.zone";
};
Step 4 - Writing the zone with a failover record
Create the zone directory:
sudo install -d -m 0755 /etc/powerdns/zones
Open the zone file:
sudo nano /etc/powerdns/zones/your_domain.zone
Add the following zone. The www record is a LUA record that returns an A answer:
$ORIGIN your_domain.
$TTL 3600
@ IN SOA ns1.your_domain. hostmaster.your_domain. (
2026092501 ; serial
3600 ; refresh
600 ; retry
1209600 ; expire
300 ) ; negative caching TTL
@ IN NS ns1.your_domain.
@ IN NS ns2.your_domain.
ns1 IN A ns1_ip
ns2 IN A ns2_ip
www 30 IN LUA A "ifurlup('https://www.your_domain/healthz', {{'primary_ip'}, {'backup_ip'}}, {stringmatch='ok'})"
How the www record works:
ifurlup()requests the URL on each address, sending the host name from the URL, and considers an address up when the request succeeds within the timeout.{stringmatch='ok'}also requires the body to containok, so an error page served with status 200 counts as down.- The addresses are given as a list of groups:
{{'primary_ip'}, {'backup_ip'}}. PowerDNS answers with the healthy addresses of the first group, and only moves to the next group when every address in the first one is down. That is active/passive failover. A single flat list such as{'primary_ip', 'backup_ip'}would instead return one of the healthy addresses at random (active/active). - The TTL of 30 seconds keeps resolver caches short.
To fail over the apex domain too, add the same LUA record with @ as the name. LUA records work at the apex, where a CNAME is not allowed.
Validate the zone:
sudo pdnsutil check-zone your_domain
Checked 6 records of 'your_domain', 0 errors, 0 warnings.
Restart PowerDNS and check that it is running:
sudo systemctl restart pdns
sudo systemctl status pdns --no-pager
● pdns.service - PowerDNS Authoritative Server
Loaded: loaded (/usr/lib/systemd/system/pdns.service; enabled; preset: enabled)
Active: active (running) since Thu 2026-09-25 11:04:12 UTC; 3s ago
If the service fails, read the log with sudo journalctl -u pdns -n 50.
Open port 53 for TCP and UDP:
sudo ufw allow 53
Step 5 - Querying the failover record
Query the server directly:
dig @ns1_ip www.your_domain A +noall +answer
www.your_domain. 30 IN A primary_ip
Right after startup, PowerDNS may answer before the first health check has completed. Wait a few seconds and query again; from then on the answer reflects the check results.
Repeat Steps 2 to 5 on ns2 with its own IP in local-address. Keep the zone file identical on both servers: copy it with scp or manage it with a configuration management tool, and increase the serial every time you change it.
NoteUse two PowerDNS servers that each evaluate the LUA records, rather than a classic primary/secondary pair with zone transfers. A secondary that is not PowerDNS with LUA records enabled cannot evaluate the
LUArecord type.
Step 6 - Delegating the domain
At your registrar, set the nameservers of your_domain to ns1.your_domain and ns2.your_domain, and create glue records pointing ns1.your_domain to ns1_ip and ns2.your_domain to ns2_ip. Glue records are required because the nameservers live inside the domain they serve.
Delegation can take up to a day to propagate because the parent zone has long TTLs. Check it with a trace:
dig +trace www.your_domain A
The last section of the output should come from ns1.your_domain or ns2.your_domain and contain the primary IP.
Step 7 - Testing the failover
Simulate an outage on the primary web server by stopping Nginx:
sudo systemctl stop nginx
Within a few check intervals (the interval is 5 seconds), the answer changes. Query both nameservers:
dig @ns1_ip www.your_domain A +short
dig @ns2_ip www.your_domain A +short
backup_ip
backup_ip
Start Nginx on the primary again:
sudo systemctl start nginx
A few seconds later, both nameservers return primary_ip again. Note that failback is automatic: as soon as the primary passes its checks it receives traffic again. If the backup has accepted writes that the primary does not have, handle data synchronization before bringing the primary back.
To see the failover from a client's point of view, query a public resolver in a loop, pausing between queries:
for i in $(seq 1 10); do dig @1.1.1.1 www.your_domain A +short; sleep 10; done
Troubleshooting
PowerDNS fails with "Unable to bind UDP socket to '0.0.0.0:53': Address already in use". local-address is missing or wrong. Set it to the server's public IP so it does not collide with systemd-resolved.
The record always returns the backup. The check to the primary fails. Run curl -sv --resolve www.your_domain:443:primary_ip https://www.your_domain/healthz from the nameserver: firewall rules, an invalid certificate or a body that does not contain ok all mark the address as down.
pdnsutil check-zone reports an error on the LUA record. Check that enable-lua-records=yes is set and that the record content is wrapped in double quotes, with single quotes inside the Lua code.
Clients keep reaching the failed server. Some resolvers enforce a minimum TTL, and browsers and applications keep their own DNS caches. This is a limit of DNS failover, not of the configuration.
Conclusion
You now have two PowerDNS nameservers that health-check your web servers and answer with the backup IP when the primary fails, then fail back automatically when it recovers. The same ifurlup() pattern works for AAAA records, and ifportup() checks a TCP port for services that do not speak HTTP. As next steps, alert on failovers by monitoring the answer of your nameservers, and make sure the backup server's data is kept current with replication or regular syncs so a failover never serves stale content.
