[RELEASE] Pi-hole HA — automatic DHCP failover, VIP, and config sync for a Pi-hole cluster

Pi-hole HA — automatic DHCP failover, VIP, and config sync for a Pi-hole cluster

If you run more than one Pi-hole for redundancy, you've probably hit the two annoying gaps: only one of them can serve DHCP at a time, and keeping their blocklists / settings in sync is a manual chore. Pi-hole HA solves both.

It turns two or more Pi-holes into a small cluster that talks to itself over HTTP (no SSH keys, no external services): if the active node dies, a standby takes over DHCP automatically, and every node's config stays in sync.

Repo: GitHub - RamSet/pihole-ha-cluster: Pi-hole DHCP high-availability cluster: automatic failover, VIP, and config sync · GitHub · Apache-2.0 · bare-metal (systemd) or Docker sidecar


"But I use my router for DHCP" — read this first

Not everyone lets Pi-hole run DHCP, so the installer auto-detects your setup and installs to match. It will never turn on Pi-hole DHCP behind your back.

  • DHCP-HA modePi-hole is your DHCP server. Full failover: a standby takes over DHCP if the primary dies, with an optional floating VIP.
  • DNS-only modeyour router or another box does DHCP. Pi-hole HA never touches DHCP; it just keeps your Pi-holes' config in sync and monitors peer health — redundant DNS without any DHCP failover.

At install it checks whether Pi-hole DHCP is active, probes the LAN for another DHCP server, inherits the mode from the cluster when joining one, and asks if it's ambiguous (defaulting to the safe DNS-only).


What it does

  • DHCP failover — if the primary goes down, a secondary takes over DHCP in ~40–80 s and yields back when the primary returns. Clients never lose their lease.
  • Virtual IP (VIP) — optional floating IP that follows the active DHCP node, so clients can use one stable DNS/DHCP address that survives failover.
  • Config sync — the primary bundles gravity DB (blocklists/adlists), DHCP static leases, custom DNS records, and FTL settings, and only rebuilds it when something actually changes. Standbys check the primary every ~15 min and pull + apply only when the config changed (plus an on-demand “Sync Now” button) — nothing transfers or restarts on unchanged data.
  • Manual master override — force any node to be the DHCP server with one click; the others yield.
  • HA kill-switch — disable all failover/VIP management cluster-wide with one toggle.
  • Web UI — a native panel inside the Pi-hole admin interface (Tools → HA Cluster), no separate app.
  • Join/leave — nodes register themselves on install; a "Leave Cluster" button removes them from all peers.
  • Optional Pushover alerts on failover events.
  • Mixed clusters — nodes can run on different Pi-hole web ports (e.g. bare-metal on :80 next to Docker on :8081).

How it works (so you know exactly what it touches)

Each node runs a small bash daemon plus a tiny socat-based JSON API on port 8887. Nodes health-check each other (ping / DNS / API / DHCP) and elect a DHCP master by priority order.

To be transparent, it uses Pi-hole's own interfaces — nothing forked or patched:

  • pihole-FTL --config … to read/set DHCP state and DNS options
  • files under /etc/dnsmasq.d/ (09-pihole-ha.conf for DHCP options, an optional new-device hook) — only in DHCP-HA mode
  • a Lua page injected into /var/www/html/admin for the Tools → HA Cluster tab (auto-reinjected after Pi-hole updates)

Everything lives under /etc/pihole-ha/ and /usr/local/…, and there's a clean uninstaller.


Quick start

Run on each node (primary first):

git clone https://github.com/RamSet/pihole-ha-cluster.git pihole-ha
cd pihole-ha
sudo ./setup.sh

setup.sh auto-detects bare-metal vs Docker and runs the right installer. Update in place with sudo ./install.sh --update; remove cleanly with sudo ./install.sh --uninstall.

Requirements: Pi-hole v6+, and either systemd (bare metal) or Docker + Compose.


Screenshots


Notes

  • The bare-metal (systemd) path is the primary, well-tested deployment. The Docker sidecar is newer and less battle-tested (image builds and logic verified; the full two-container runtime hasn't been exercised as broadly) — feedback welcome.
  • It's a third-party tool, not affiliated with the Pi-hole project. It uses supported pihole-FTL --config interfaces rather than modifying Pi-hole itself, but as always: try it on a node you can afford to reinstall first.
  • Issues / PRs / ideas: please open them on GitHub.

Is there an option to sync only when something has been changed in the configuration and not sync every 15 minutes to avoid unnecessary writes to the microSDXC card ?

Also : Manual 'Force Sync' button ?

The sync check happens every 15 minutes. There is no sync happening at all if the data stays the same. It only syncs when it needs to or manually, on demand when Sync Now is pressed.

(I corrected the wording - Thank you)

On the DNS not resolving after failover:

  • cd pihole-ha
  • git pull && sudo ./install.sh --update*
  • sudo pihole-ha-debug > ha.txt # then paste ha.txt here (secrets are auto-redacted)*

In the meantime, in Pi-hole → Settings → DNS → Interface settings, set it to “Listen on all interfaces” (or pihole-FTL --config dns.listeningMode ALL), then re-test failover. That’s the difference between my working setup and, I suspect, yours.

On “Could not register with any existing nodes”: that fires when the new node can’t reach the existing node’s cluster API (:8887) to register — most often because the primary has a Pi-hole admin password set, so the join needs authentication. Since your primary already shows both nodes, membership sorted itself out, so it’s likely harmless — but if the secondary’s health table shows the primary as “Locked,” enter the primary’s Pi-hole password there so the two can talk fully. The pihole-ha-debug output will show whether the peers can reach each other (/api/status -> 200).

Are you able to configure which parts of the config sync and which don't like in nebula sync?

Awesome looking project. Thanks for sharing!

I’m not familiar with nebula sync… but yes. It does allow selecting:

BUT, in a HA deployment though, the nodes should be identical …

PERFECT! :grimacing::+1::+1:

Hi RamSet,

I've been looking for a native way to replace nebula-sync for some time, and this project looks incredibly promising! I am curious if it will support my specific dual-platform layout.

My topology consists of:

  • Primary node: A Proxmox LXC container (originally deployed using community helper scripts).
  • Secondary node: A Synology NAS running inside Docker via Container Manager.

Both environments are also running Unbound. Getting everything to cooperate on the Synology side was quite complex because of Synology’s default container isolation. I ended up implementing a macvlan driver network, mapping an interface bridge, and relying on a custom startup script at NAS boot time just so the host and containers could route traffic to one another properly.

Before I begin tearing things down and unwinding my old configurations to test this out, I had a couple of quick questions:

  1. Deployment footprint: For the Synology/Docker node, does the HA component run as a sidecar container sharing the same volume paths/mounts as the official Pi-hole container?
  2. Macvlan compatibility: Does the HA cluster heartbeat mechanism or Virtual IP (VIP) configuration face any known routing issues when a node is bound to a macvlan network interface?

I appreciate your work on this; it would be amazing to finally move away from cron-based syncing scripts!

The Docker path is only tested with network_mode: host on both containers, and macvlan fights with that lol. But you're coming from nebula-sync so you want config sync, not DHCP failover — and that part's totally doable.

1. Footprint on the Synology node

It's a sidecar, doesn't touch/replace your Pi-hole container. It just rides alongside it:

  • Shares the same /etc/pihole and /etc/dnsmasq.d volumes as the official container (they're external: true in my compose, so it hooks into your existing Pi-hole volumes, doesn't make new ones).
  • Shares Pi-hole's PID namespace (pid: "service:pihole") so it can poke pihole-FTL to hot-reload config without bouncing it.
  • Everything goes through pihole-FTL --config — supported interface, no fork, no custom image.

So on the NAS you'd just have your Pi-hole container + a little pihole-ha sidecar next to it. Its own state lives in separate volumes.

2. macvlan + heartbeat/VIP

Host mode and macvlan can't coexist, so your macvlan node isn't a copy-paste. Two separate things though:

Heartbeat / sync (:8887) — that's literally just HTTP between nodes, doesn't need host mode. And the macvlan "host can't reach its own container" thing that made you do the bridge + boot-script dance? Doesn't bite here, because your two nodes are different physical boxes — a macvlan container talking to another host on the LAN is fine. You'd just run the sidecar inside Pi-hole's netns instead of host:

pihole-ha:
  network_mode: "service:pihole"   # ride Pi-hole's macvlan IP
  pid: "service:pihole"

Then the dash binds on Pi-hole’s macvlan IP:8887 and the Proxmox node reaches it normally. I haven’t actually run this exact combo, so you’d be the guinea pig lol. Sound in theory.

VIP — this is the part I’d just skip on macvlan. It floats the IP with an alias + gratuitous ARP on the interface, and on a macvlan sub-iface (on Synology especially) that’s flaky and I haven’t validated it. Just point clients at both node IPs and move on.

The actual good news

You want sync, not failover. Install DNS-only mode (router does DHCP, you’ve got Unbound already) with the VIP off. In that mode it never touches DHCP and needs no broadcast/VIP — which is the entire reason host networking is “required.”

Kill that and you’re left with config sync over :8887, which is the macvlan-friendly bit. Change-gated, HMAC-signed, no cron, no SSH keys.

So: LXC primary is a normal LAN host, just works. Synology/macvlan node should be fine for DNS-only sync with that service:pihole tweak and no VIP — untested by me, so if you try it I actually want to hear how it goes and I’ll help you debug. Just don’t count on VIP/DHCP failover on the macvlan side.

Long story short, you should be able to give this a test run, just make sure that you disable nebula-sync.

This project is additive, the sidecar rides alongside Pi-hole.

One thing to be aware of:

Once sync is on, the node you pick as primary becomes source of truth and the other node’s gravity.db / custom DNS / DHCP-hosts / FTL settings get overwritten to match on the first pull. There’s no dry-run mode.

See if it flows properly, errors out, hiccups and let me know so that we can tackle it properly. I’m a QNAP guy but i know a lot of users use Synology.

Hi there,

I just wanted to say thankyou for this amazing project. Everything has been working really well!.

Can you point me to change log updates? Or can you provide me with what was changed this update? It would be nice to know if there are functional changes / updates.

Thankyou!

This was a 6 month old project that initially was private.

I have not posted change history into the now public repo. I’ll put something together from the commit messages i have in the private repos. I’ll let you know ow when the development history is available.

Oh! Thankyou for bringing it public. You dont have to, but just nice to know what happens in updates. Thankyou so much for this project.

Here you go.

Change log now available

In DNS only mode
can you describe the advantage over a solution with :

  • pihole
  • nebula-sync
  • keepalived

Thanks in advance

Sure.

  1. It is via Pi-hole
  2. Never used nebula sync but i’m assuming better memory management? Less impact?
  3. Keepalived from my own experience was flaky to be honest and unstable (behind the scenes). It was flapping a lot causing a lot if false positives.

well i try test
and get
2026-07-13T13:50:34 [PRIMARY] [INFO] event=dhcp_activate reason="Primary enforcing DHCP"

2026-07-13T13:50:35 [PRIMARY] [ERROR] event=dhcp_activate status=failed
2026-07-13T13:50:45 [PRIMARY] [INFO] event=dhcp_activate reason="Primary enforcing DHCP"
2026-07-13T13:50:47 [PRIMARY] [ERROR] event=dhcp_activate status=failed

I do not user pihole DHCP
seems it do not deetect that

Edit : logged an issue in github

Looks like you're "saving" me from migrating to Technitium with this :victory_hand:

Does is also transfer the cache from unbound over to the nodes?

Nope, it doesn’t! Setting up Unbound direction is just an optional part of using Pi-Hole overall.

I actually do use Unbound, but I don’t migrate it—honestly, it’s not worth the hassle since it’s temporary, stored in memory, and limited by TTL. Kind of fleeting…

The main perk of migrating it is just a slightly “warmer” cache which has no perceivable “speed”

Totally fair​:ok_hand: