Skip to content

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's unbound package 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@5335 is 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:

sudo timeout 6 unbound -d -vv

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

can't bind socket: Address already in use for ::1 port 53

(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 interface reports 127.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.com should 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 returns SERVFAIL, 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 (dig a domain you haven't queried recently, check Query time) — full recursion is slower than forwarding to a public resolver's warm cache, but should still be well under a second.