When Your DNS Goes Down, the Internet Dies
You know the drill. It’s Sunday afternoon, someone’s watching a show, someone else is mid-game, and then — nothing. Every device in the house stops working. Phones can’t load apps. The TV just stares at you. Your spouse gives you that look.
The culprit? DNS is down. Your single AdGuard Home instance decided it was done for the day. Maybe the host rebooted. Maybe Docker got weird. Doesn’t matter — the damage is done and now you’re explaining to a non-technical person why “the internet is broken” when the actual internet is fine.
Single-node DNS is a single point of failure. It’s the one service in your home lab that absolutely cannot go down without everyone noticing immediately. You can lose Jellyfin for an hour and nobody cares. Lose DNS for 90 seconds and you’re getting texts from the living room.
The fix is running two AdGuard Home instances that stay in sync — same blocklists, same custom DNS rewrites, same allowed/blocked clients — and handing out both IPs (or a virtual failover IP) via DHCP. Here’s how to do it without losing your mind.
The Three Patterns (and Which One to Use)
Before diving into configs, let’s be honest about the tradeoffs.
Option A: DHCP Hands Out Two DNS Servers
Your router’s DHCP server can hand out two DNS IPs — primary and secondary. Clients try the primary first, fall back to the secondary automatically if it doesn’t respond.
Pros: Dead simple. Zero extra tooling. Works with any DNS server.
Cons: No config sync. Your blocklists, custom rewrites, and client rules live on two separate instances that will drift apart the moment you touch one of them. You’ll end up with mystery behavior where one device gets a different DNS rewrite than another depending on which instance answered. Good luck debugging that at 11 PM.
Use this only if you want redundancy without caring that the two instances are identical. Which you should care about, because you’re reading this.
Option B: Keepalived VRRP — Active-Passive Failover
Keepalived implements VRRP (Virtual Router Redundancy Protocol), which lets two hosts share a virtual IP. One host holds the VIP and answers DNS; if it dies, the other grabs the VIP and takes over. Your DHCP hands out a single DNS IP (the VIP) and clients never know anything happened.
Pros: Completely transparent to clients. One DNS IP, clean failover in 1-3 seconds.
Cons: Still doesn’t sync config. You need to combine this with something else for true parity.
Option C: adguardhome-sync — Config Replication
bakito/adguardhome-sync is the tool for this job. It talks to the AdGuard Home API on both your primary and replica instances, pulls config from the primary, and pushes it to the replica on a schedule (or via webhook). Blocklists, custom filtering rules, DNS rewrites, client configurations — all of it.
Pros: Actual config parity. Whatever you configure on primary shows up on replica.
Cons: Config sync is eventually consistent — there’s a window (however small) where they differ. Not a problem in practice.
The right answer: Combine B and C. Use adguardhome-sync to keep configs in sync, and Keepalived to handle IP failover. Active-passive HA with actual config replication.
Setting Up adguardhome-sync
Assume you’ve already got two AdGuard Home instances running somewhere:
- Primary:
192.168.1.51(AdGuard Home 0.107.x) - Replica:
192.168.1.52(AdGuard Home 0.107.x)
adguardhome-sync runs as a sidecar container alongside either instance (or on a third host — doesn’t matter). It hits the AdGuard Home REST API with basic auth.
Docker Compose
version: "3.8"
services: adguardhome-sync: image: ghcr.io/bakito/adguardhome-sync:v0.7.3 container_name: adguardhome-sync restart: unless-stopped environment: - ORIGIN_URL=http://192.168.1.51:3000 - ORIGIN_USERNAME=admin - ORIGIN_PASSWORD=your_primary_password - REPLICA1_URL=http://192.168.1.52:3000 - REPLICA1_USERNAME=admin - REPLICA1_PASSWORD=your_replica_password - CRON=*/5 * * * * - FEATURES_GENERAL_SETTINGS=true - FEATURES_QUERY_LOG_CONFIG=true - FEATURES_STATS_CONFIG=true - FEATURES_CLIENT_SETTINGS=true - FEATURES_SERVICES=true - FEATURES_FILTERS=true - FEATURES_DNS_SERVER_CONFIG=true - FEATURES_DNS_ACCESS_LISTS=true - FEATURES_DNS_REWRITES=true - FEATURES_DHCP_SERVER_CONFIG=false - FEATURES_DHCP_STATIC_LEASES=false - RUN_ON_START=true networks: - adguard_sync
networks: adguard_sync: driver: bridgeA few things worth calling out here:
CRON=*/5 * * * * — syncs every 5 minutes. That’s fine for home lab use. If you’re paranoid, go to */1 (every minute), but honestly 5 minutes is more than adequate. You’re not running a bank.
FEATURES_DHCP_SERVER_CONFIG=false and FEATURES_DHCP_STATIC_LEASES=false — this is important. Do NOT sync DHCP config between instances. If both think they’re the DHCP server handing out leases, you’ll get IP conflicts and the network chaos you were trying to avoid. One instance handles DHCP, the other is DNS-only.
RUN_ON_START=true — does an immediate sync when the container starts, instead of waiting for the first cron tick.
Verify It’s Working
# Check sync logsdocker logs adguardhome-sync --tail 50
# You should see something like:# INFO Synchronizing [origin] http://192.168.1.51:3000 => [replica] http://192.168.1.52:3000# INFO Synchronization doneLog into the replica’s web UI and confirm your primary’s blocklists, custom rules, and DNS rewrites are all there. If they’re missing, check that the API credentials are correct — AdGuard Home 0.107.x uses the same basic auth for the API as the web UI.
Setting Up Keepalived for IP Failover
Now for the network-level failover. Both hosts need Keepalived installed. The primary holds the VIP (say, 192.168.1.53) under normal operation. If the primary’s DNS stops responding, Keepalived promotes the replica.
Install Keepalived
# Debian/Ubuntusudo apt install keepalived
# RHEL/Rocky/Almasudo dnf install keepalivedPrimary Node Config (/etc/keepalived/keepalived.conf)
global_defs { router_id DNS_PRIMARY}
vrrp_script check_dns { script "/usr/bin/nc -z 127.0.0.1 53" interval 2 weight -30 fall 2 rise 2}
vrrp_instance VI_DNS { state MASTER interface eth0 virtual_router_id 53 priority 100 advert_int 1 authentication { auth_type PASS auth_pass your_shared_vrrp_secret } virtual_ipaddress { 192.168.1.53/24 } track_script { check_dns }}Replica Node Config (/etc/keepalived/keepalived.conf)
global_defs { router_id DNS_REPLICA}
vrrp_script check_dns { script "/usr/bin/nc -z 127.0.0.1 53" interval 2 weight -30 fall 2 rise 2}
vrrp_instance VI_DNS { state BACKUP interface eth0 virtual_router_id 53 priority 90 advert_int 1 authentication { auth_type PASS auth_pass your_shared_vrrp_secret } virtual_ipaddress { 192.168.1.53/24 } track_script { check_dns }}The key differences: state MASTER vs state BACKUP, and priority 100 vs priority 90. The node with the higher priority holds the VIP. The vrrp_script checks that port 53 is actually listening locally — if it’s not (container crashed, service died, whatever), the weight drops the effective priority by 30, causing failover.
Replace eth0 with your actual interface name (ip addr will tell you). Keep virtual_router_id the same on both nodes.
# Start and enable on both nodessudo systemctl enable --now keepalived
# Check statussudo systemctl status keepalived
# Check which node holds the VIPip addr show eth0 | grep 192.168.1.53Router DHCP: One DNS IP to Rule Them All
Now that you’ve got a VIP, point your router’s DHCP at it as the only DNS server. One IP, no secondary, clients never know about the two physical hosts behind the curtain.
Most routers: DHCP settings → DNS server → 192.168.1.53 (the VIP).
If your router is also running AdGuard Home (some setups do this for the router itself), that’s a different conversation. For most home labs, the router is dumb and just hands out DHCP.
Testing the Failover
This is the fun part. Actually kill the primary and make sure things work.
# From any client on the network, query through the VIPdig @192.168.1.53 google.com
# Should return answers. Note the query time.
# Now on the primary host, kill AdGuard Homesudo docker stop adguardhome # or whatever your container is named
# Wait 4-6 seconds for VRRP to detect failure and promote replica
# Query againdig @192.168.1.53 google.com
# Still returns answers — now served by the replicaCheck which node has the VIP now:
# On replica nodeip addr show eth0 | grep 192.168.1.53# Should show the IP — it's been promoted
# On primary node (with AdGuard stopped)ip addr show eth0 | grep 192.168.1.53# Should show nothingBring the primary back:
sudo docker start adguardhome
# Wait a few seconds# Primary reclaims the VIP (it has higher priority)ip addr show eth0 | grep 192.168.1.53# Back on primaryIf the failover doesn’t happen, check Keepalived logs:
sudo journalctl -u keepalived -fCommon issues: wrong interface name, firewall blocking VRRP protocol (protocol 112), or nc not installed for the health check script. Use apt install netcat-openbsd if nc is missing.
The DHCP Gotcha (Read This Twice)
Here’s where people mess up. adguardhome-sync will happily sync your DHCP configuration to the replica if you let it. If both instances then try to hand out leases on the same subnet, you get:
- Two DHCP servers racing to respond to discovery packets
- Clients getting IP addresses from whichever server wins the race
- Lease table inconsistencies
- Weird connectivity issues that are nearly impossible to debug
The rule: Only one AdGuard Home instance should have DHCP enabled. If you’re using AdGuard Home as your DHCP server (uncommon but valid), designate the primary as DHCP + DNS, and the replica as DNS-only. Set both FEATURES_DHCP_SERVER_CONFIG=false and FEATURES_DHCP_STATIC_LEASES=false in adguardhome-sync.
If you’re not using AdGuard Home for DHCP at all (your router handles it), this isn’t a concern — but still leave those features disabled just to be safe.
A Note on Client Identification
AdGuard Home tracks clients by IP address. If you’ve set per-client rules (“block all ads for the kids’ tablet, allow YouTube for the smart TV”), those rules live in the config and sync fine between instances.
What doesn’t sync perfectly: if your two AdGuard instances are on different subnets or VLANs, the source IP they see for each client might differ. On a flat home LAN where both instances are in the same subnet, this isn’t an issue — both see the same client IPs. If you’re running VLANs, test client identification on the replica before trusting it fully.
What About Pi-hole Users?
If you’re on Pi-hole, the equivalent tool was gravity-sync. Past tense intentional — Pi-hole 6 changed the gravity database format and internal APIs enough that gravity-sync broke and was effectively deprecated.
The current alternatives are orbital-sync and nebula-sync, both of which aim to fill the gap. As of mid-2026, orbital-sync has the most active development. The setup concept is identical to adguardhome-sync: point it at your primary and replica, let it handle the API calls.
For Keepalived, the config is identical — Pi-hole uses port 53 same as anything else, so the VRRP health check works the same way.
Honestly, if you’re starting fresh and want this to just work, AdGuard Home 0.107.x + adguardhome-sync is currently the path of least resistance. The Pi-hole 6 ecosystem is still catching up.
The Full Picture: What Gets Synced
With the setup above, adguardhome-sync propagates:
- Block lists and filter subscriptions
- Custom filtering rules (allowlist/blocklist entries)
- DNS rewrites (your
*.home.lanentries, internal service names, etc.) - Client configurations and groups
- General settings (safe search, parental controls, etc.)
- Query log and stats settings
- DNS server upstream configuration
What it does NOT sync (and shouldn’t):
- DHCP leases and server config (disabled explicitly)
- Per-instance network interface settings
- TLS certificate configuration (manage those separately per host)
Should You Bother?
If you’ve got one server and run AdGuard Home as a single Docker container, probably not yet. Redundancy adds operational complexity and you need a second host to make it worthwhile.
If you’ve got two servers (a Pi 5 and an old mini PC, two VMs in Proxmox, whatever) and DNS going down causes actual household conflict — yes, absolutely. The adguardhome-sync container is essentially zero maintenance once it’s running. Keepalived is a systemctl enable and forget situation.
The sweet spot for this setup: two nodes already exist for other reasons (Proxmox cluster, Home Assistant + NAS, two Pis), and you want DNS HA without buying dedicated hardware or running it all on the router.
Two containers, one config file, one Keepalived setup. Your Sunday afternoon stays uninterrupted. Your spouse stops giving you the look. Worth it.