Skip to content
Go back

AdGuard DNS Sync Across Two Instances

By SumGuy 11 min read
AdGuard DNS Sync Across Two Instances

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:

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

docker-compose.yml
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: bridge

A 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

Terminal window
# Check sync logs
docker 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 done

Log 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

Terminal window
# Debian/Ubuntu
sudo apt install keepalived
# RHEL/Rocky/Alma
sudo dnf install keepalived

Primary 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.

Terminal window
# Start and enable on both nodes
sudo systemctl enable --now keepalived
# Check status
sudo systemctl status keepalived
# Check which node holds the VIP
ip addr show eth0 | grep 192.168.1.53

Router 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.

Terminal window
# From any client on the network, query through the VIP
dig @192.168.1.53 google.com
# Should return answers. Note the query time.
# Now on the primary host, kill AdGuard Home
sudo docker stop adguardhome # or whatever your container is named
# Wait 4-6 seconds for VRRP to detect failure and promote replica
# Query again
dig @192.168.1.53 google.com
# Still returns answers — now served by the replica

Check which node has the VIP now:

Terminal window
# On replica node
ip 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 nothing

Bring the primary back:

Terminal window
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 primary

If the failover doesn’t happen, check Keepalived logs:

Terminal window
sudo journalctl -u keepalived -f

Common 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:

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:

What it does NOT sync (and shouldn’t):


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.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Next Post
iperf3 + nload: Network Diagnosis

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts