Wi-Fi & Network
Pi-hole + Unbound recursive DNS setup
Pairing Pi-hole with a local Unbound resolver means your lookups go straight to the root and authoritative servers instead of through Google/Cloudflare. This guide sets it up the way the Pi-hole docs do, with the gotchas operators actually hit.
Who this is for
Pi-hole users who want to stop forwarding to a third-party resolver (Google/Cloudflare) and instead run their own recursive resolver with Unbound — for privacy and independence — and who are comfortable editing a config file and a sysctl on the Pi-hole host.
Outcome
A working Pi-hole → Unbound chain where Unbound listens on 127.0.0.1#5335, recurses from the root servers, and validates DNSSEC; Pi-hole forwards only to it (all other upstreams unticked); the common so-rcvbuf and port-53 gotchas are handled; and you understand the cold-cache latency trade-off you accepted.
Required inputs
- A working Pi-hole (Core v6.x) on a host where you have shell/root access to install a package and edit config.
- The official Unbound config from the Pi-hole docs (listen on 5335, edns-buffer-size 1232, DNSSEC root trust anchor) — don't hand-roll it.
- Ability to set a kernel sysctl (net.core.rmem_max) on bare metal, or to lower so-rcvbuf if running in Docker/LXC.
- On Debian/Ubuntu hosts: awareness of systemd-resolved's :53 stub listener and the resolvconf hijack, which must be disabled.
Step-by-step procedure
Install Unbound and drop in the documented config
Do: Install the unbound package, then place the Pi-hole docs' config at /etc/unbound/unbound.conf.d/pi-hole.conf (on some Red Hat-based distros the path is /etc/unbound/conf.d/pi-hole.conf). It listens on port 5335, sets edns-buffer-size 1232, and references the root hints + auto-trust-anchor-file for DNSSEC. Restart Unbound.
Expected result: Unbound starts cleanly and is listening on 127.0.0.1:5335.
If not: If it won't start, read the Unbound log for a config or path error — usually the trust-anchor/root-hints path or a stray duplicate listen directive.
Test Unbound directly before touching Pi-hole
Do: Run dig pi-hole.net @127.0.0.1 -p 5335 and confirm an answer. Then test DNSSEC: dig sigfail.verteiltesysteme.net @127.0.0.1 -p 5335 should return SERVFAIL, and dig sigok.verteiltesysteme.net @127.0.0.1 -p 5335 should return NOERROR with an IP.
Expected result: Normal lookups resolve; the bogus DNSSEC domain is rejected (SERVFAIL) and the valid one succeeds.
If not: If the bogus domain resolves instead of SERVFAIL, DNSSEC validation isn't active — re-check the trust-anchor config before pointing Pi-hole at it.
Point Pi-hole at Unbound and remove other upstreams
Do: In Pi-hole Settings > DNS, set the Custom (upstream) DNS to 127.0.0.1#5335 and untick every other upstream (Google/Cloudflare/etc.). Save.
Expected result: Pi-hole forwards exclusively to Unbound; clients still resolve normally.
If not: If resolution breaks, confirm Unbound is up (previous step) and that no firewall blocks localhost:5335; don't leave a public upstream ticked 'as backup' — it defeats the point.
Fix the so-rcvbuf / kernel-buffer warning
Do: If the Unbound log shows 'so-rcvbuf … was not granted', raise net.core.rmem_max (and wmem_max) via /etc/sysctl.d and apply with sysctl --system on bare metal. In Docker/LXC where you can't change host sysctls, lower so-rcvbuf in the config instead.
Expected result: The warning clears (bare metal) or is rendered harmless (container falls back to the granted size).
If not: If the warning persists on bare metal, confirm the sysctl actually applied (sysctl net.core.rmem_max) and that you restarted Unbound.
Clear port-53 conflicts and the resolvconf hijack
Do: On systemd hosts, set DNSStubListener=no in /etc/systemd/resolved.conf and restart systemd-resolved so it doesn't squat on :53 (Pi-hole/FTL needs it; Unbound is on 5335). On Debian Bullseye+, disable the resolvconf entry for Unbound so it doesn't rewrite /etc/resolv.conf into a loop. Leave Pi-hole's own DNSSEC toggle OFF — let Unbound validate.
Expected result: FTL owns :53, Unbound owns 5335, and there's no resolv.conf loop or double DNSSEC validation.
If not: If the host itself can't resolve during a Pi-hole outage, remember the host uses Pi-hole as upstream — a temporary public resolver restores it while you repair.
Verify end to end and warm the cache
Do: From a client, confirm normal browsing and that blocked domains are still blocked; watch the Pi-hole query log show replies coming from 127.0.0.1#5335. Expect the first lookup of a new TLD to be slower; repeats are sub-0.1s.
Expected result: Clients resolve through Pi-hole → Unbound, DNSSEC is validated by Unbound, and performance settles after the cache warms.
If not: If lookups feel persistently slow, it's usually cold cache or a DNSSEC-broken zone, not a misconfig — confirm with dig timing against 5335.
Commands and settings paths
Unbound resolves + validates DNSSEC
dig pi-hole.net @127.0.0.1 -p 5335 ; dig sigfail.verteiltesysteme.net @127.0.0.1 -p 5335
Where: On the Pi-hole/Unbound host.
Expected: pi-hole.net returns an answer; sigfail returns SERVFAIL (and sigok returns NOERROR + IP).
Failure means: If sigfail resolves instead of SERVFAIL, DNSSEC validation isn't active.
Safe next step: Re-check the auto-trust-anchor-file/root-hints config before pointing Pi-hole at Unbound.
Who's on port 53 vs 5335?
sudo ss -lntup | grep -E ':53 |:5335 '
Where: On the host.
Expected: pihole-FTL listens on :53 and unbound on :5335 — no systemd-resolved stub on :53.
Failure means: systemd-resolved squatting on :53 will collide with FTL.
Safe next step: Set DNSStubListener=no and restart systemd-resolved.
Confirm the so-rcvbuf fix
sysctl net.core.rmem_max ; sudo journalctl -u unbound | grep -i so-rcvbuf
Where: On the host (bare metal).
Expected: rmem_max is raised to match the config and the 'not granted' warning no longer appears after restart.
Failure means: A still-low rmem_max means the sysctl didn't apply.
Safe next step: Re-apply via /etc/sysctl.d + sysctl --system and restart Unbound (or lower so-rcvbuf in containers).
Evidence to record
- The Unbound config path and that it listens on 5335 with edns-buffer-size 1232 and a DNSSEC trust anchor.
- dig results: a normal lookup against :5335, plus the sigfail (SERVFAIL) / sigok (NOERROR) DNSSEC pair.
- Whether you raised net.core.rmem_max (bare metal) or lowered so-rcvbuf (container).
- That Pi-hole's only upstream is 127.0.0.1#5335 with all others unticked, and Pi-hole's own DNSSEC toggle is off.
Common mistakes
- Leaving a public upstream (Cloudflare/Google) ticked 'as backup' in Pi-hole — it defeats the privacy point and Pi-hole will use it.
- Enabling Pi-hole's own DNSSEC on top of Unbound's — double validation that just bloats the query database.
- Ignoring the so-rcvbuf warning's root cause on bare metal (raise rmem_max) vs container (lower so-rcvbuf) — applying the wrong fix.
- Forgetting systemd-resolved's :53 stub or the resolvconf hijack, so FTL can't bind :53 or resolv.conf loops.
Stop points
- Stop and restore a temporary public resolver if the Pi-hole host itself loses DNS mid-setup (it uses Pi-hole as upstream) — you can't repair offline.
- Stop before assuming Unbound is broken if lookups are merely slow on first hit — that's expected cold-cache behavior, not a fault.
Last reviewed
2026-06-02
Source-backed checks
HomeTechOps turns official docs and conservative safety rules into a shorter runbook. These links are the source trail for the page direction.
Did this solve your problem?
Your answer helps us improve this page.
No account or personal details. We store the page, answer, optional reason and day with a 90-day retention period. A random reference in this browser prevents repeat votes. Feedback privacy
Get the deal & firmware alerts
Home Stack Field Notes: NAS deals, firmware changes worth acting on, restore-test reminders, and new decision guides — plus the capacity & backup sizing cheatsheets from our calculators. Unsubscribe anytime.
Subscribe on beehiiv