Running Unbound and Pi-hole Together¶
Self-Hosted Recursive DNS with unbound covers running Unbound as the resolver on a Fedora mail server. This article covers the different, common case of running Unbound alongside Pi-hole on the same Debian/Ubuntu box — Pi-hole keeps port 53 and its ad-blocking role, and Unbound becomes Pi-hole's own upstream, doing real recursive resolution instead of forwarding to a third party (Google, Cloudflare, your ISP).
Why this needs its own setup, not just the mail-server pattern¶
- Different package manager. The mail-server article assumes Fedora/
dnf. Debian/Ubuntu'sunboundpackage ships its own install helper (/usr/libexec/unbound-helper) that handles chroot setup/teardown and root-trust-anchor updates — don't hand-roll what the packaged installer already does. - Different port. Pi-hole needs port 53. Unbound has to bind somewhere else —
127.0.0.1@5335is the conventional choice — and Pi-hole's upstream DNS setting points at that instead of a public resolver. - Coexistence, not replacement. The goal is a minimal Unbound config that does one job (recurse, validate DNSSEC, cache) and gets out of the way. A large mail-server-style config with mail-specific tuning is the wrong template here.
Minimal working config¶
/etc/unbound/unbound.conf.d/pi-hole.conf:
server:
interface: 127.0.0.1@5335
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: no
prefer-ip4: yes
access-control: 127.0.0.0/8 allow
access-control: 0.0.0.0/0 refuse
Then point Pi-hole's upstream at 127.0.0.1#5335 (Pi-hole v6: pihole-FTL --config dns.upstreams — see below) instead of any public resolver, and make sure whatever installs Pi-hole runs after Unbound in your provisioning order, since Pi-hole's config references Unbound's port immediately.
Pi-hole v6 config: use pihole-FTL --config, not manual TOML edits¶
Pi-hole v6 moved its config into /etc/pihole/pihole.toml. Editing it by hand risks fighting FTL's own config reload logic. The safe read/write path is:
pihole-FTL --config dns.upstreams # read current value
pihole-FTL --config dns.upstreams '["127.0.0.1#5335"]' # set it
This applies to both dns.hosts (local DNS overrides) and dns.upstreams (what Pi-hole forwards to) — same mechanism, same safety guarantee.
The gotcha that will cost you the most debugging time: orphaned debug processes squat on the port¶
If you ever interactively debug Unbound with something like:
...over an SSH session that then disconnects or the tool you're running it from times out, the process can survive the disconnect and keep holding 127.0.0.1:5335 open. The systemd-managed unbound.service will then fail to start with:
(note: the error may reference the wrong port/address in its message — don't trust it literally). This looks exactly like a config problem, and unbound-checkconf -o interface will confirm your config is correct, which is actively misleading. It is not a config problem.
Diagnosis: temporarily disable your config drop-in entirely and try starting the service with vanilla defaults. If the identical bind error still happens, your config is exonerated — something else already holds the port.
Fix:
sudo lsof -i :5335 # or: ss -tulpn | grep 5335
sudo kill <pid> # the orphaned debug process
sudo systemctl reset-failed unbound
sudo systemctl start unbound
Verification checklist¶
unbound-checkconf -o interfacereports127.0.0.1@5335— confirms config is loaded, not that the daemon is actually bound (see gotcha above).ss -tulpn | grep 5335— confirms Unbound is actually listening, not just configured to.- Query Unbound directly, bypassing Pi-hole:
dig @127.0.0.1 -p 5335 example.comshould return a real answer. - DNSSEC actually validates, not just "is enabled": query a domain with intentionally broken DNSSEC (e.g.
dig @127.0.0.1 -p 5335 dnssec-failed.org) and confirm it returnsSERVFAIL, not a normal answer. A resolver that returns real answers for good domains but never actually checks signatures will pass a naive test and fail this one. - Genuine recursion, not just resolution: the open-resolver / DNSBL test used for the mail-server pattern applies here too — confirm Unbound isn't accidentally forwarding to something else upstream.
- Pi-hole's ad-blocking still works end-to-end after the upstream change (query a known-blocked domain through Pi-hole itself, not directly against Unbound).
- Latency is sane for a cold cache (
diga domain you haven't queried recently, checkQuery time) — full recursion is slower than forwarding to a public resolver's warm cache, but should still be well under a second.
Related¶
- Self-Hosted Recursive DNS with unbound — the standalone (non-Pi-hole) pattern, mail-server motivated.
- pihole-v6-adlist-management, pihole-v6-group-management — other
pihole-FTL --config/ v6-specific management patterns.