Why pair them
Pi-hole on its own forwards unblocked queries to whatever upstream is configured — usually a public resolver. That's fine, but the upstream sees every domain every client on the LAN visits. Unbound is a recursive resolver: it walks the DNS hierarchy itself, asking .com's nameservers about example.com, and caches the result. After warm-up, nothing is forwarded; nothing is logged by any third party. Latency is comparable for cached queries and slightly worse for cold cache misses (one round-trip to root, one to TLD, one to authoritative) — usually not noticeable.
Install Pi-hole on Debian/Ubuntu
On a clean Debian/Ubuntu box:
curl -sSL https://install.pi-hole.net | bash
The installer is interactive: it asks for upstream DNS (pick anything — you'll replace it with Unbound below), which interface to listen on, whether to install the web admin, and whether to enable query logging. After it finishes:
pihole status
sudo pihole -a -p # set the web admin password
# Web UI: http://<box-ip>/admin
Point a client device's DNS server at the box's IP and queries start showing up in the Pi-hole dashboard. Don't change every client yet — point one device first, confirm it resolves, then propagate.
Install Unbound
sudo apt install unbound
sudo systemctl status unbound
The Pi-hole project ships a recommended Unbound configuration tuned for recursive use behind Pi-hole. Drop it in:
cat <<'EOF' | sudo tee /etc/unbound/unbound.conf.d/pi-hole.conf
server:
verbosity: 0
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: no
prefer-ip6: no
# Trust glue only if it is within the server's authority.
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
# Reduce EDNS reassembly buffer to mitigate IP fragmentation.
edns-buffer-size: 1232
# Pre-fetch records that are about to expire.
prefetch: yes
prefetch-key: yes
# Cache sizing.
num-threads: 1
msg-cache-size: 50m
rrset-cache-size: 100m
cache-min-ttl: 300
cache-max-ttl: 86400
# Ratelimit recursive replies.
ratelimit: 1000
# Treat RFC1918 / private ranges as private (don't expose them).
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
private-address: 169.254.0.0/16
private-address: fd00::/8
private-address: fe80::/10
EOF
sudo systemctl restart unbound
dig @127.0.0.1 -p 5335 cloudflare.com +dnssec | head
If the dig returns an A record with the ad flag set in the response, Unbound is resolving and validating DNSSEC.
Point Pi-hole at Unbound
In the Pi-hole admin UI: Settings → DNS. Uncheck every "Upstream DNS Servers" preset. In the "Custom 1 (IPv4)" box, enter:
127.0.0.1#5335
Save. From now on, Pi-hole's upstream is the local Unbound — which talks straight to the root.
Block-list management
Pi-hole's default list (StevenBlack consolidated hosts) blocks a few hundred thousand ad/tracker domains. Add more via Group Management → Adlists. Reasonable additions:
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts— default, already therehttps://raw.githubusercontent.com/hagezi/dns-blocklists/main/wildcard/multi.txt— a maintained, well-tuned set with explicit allow-list curationhttps://big.oisd.nl/— aggregated blocklist with low false-positive rate
After adding, run pihole -g (or click "Update gravity" in the UI) to compile the lists into Pi-hole's working database.
More lists is not more better. Heavily overlapping lists waste memory and increase false positives. Two well-curated lists usually beat ten random ones. If something legitimate is being blocked, check Query Log — Pi-hole shows exactly which list matched.
Per-device policies via Groups
For "the work laptop should not have ads stripped from a specific marketing platform" or "the kids' tablet has stricter blocking":
- Group Management → Groups → create groups (e.g.
kids,work,default). - Group Management → Clients → pin specific client IPs/MACs to the right group.
- Group Management → Adlists / Domains → assign each list/exception to a subset of groups.
The default group applies to everyone not explicitly grouped, so it's the right baseline.
Encrypted DNS to clients (DNS-over-TLS)
To let off-LAN clients use this resolver over the public internet, terminate DoT in front of Pi-hole. stunnel or nginx stream blocks both work; Unbound itself can also do DoT with a separate config snippet. The simplest tool is dns-over-https/dnsproxy which serves both DoH and DoT in one daemon and forwards to local Pi-hole on 53.
Whichever option, point the client's "Private DNS" (Android) / "Encrypted DNS" (iOS/macOS) at the public hostname; the server presents a Let's Encrypt cert, terminates the DoT, and forwards the plain query to Pi-hole locally.
Backup and migrate
From the admin UI, Settings → Teleporter → Backup downloads a tarball with all Pi-hole state (lists, groups, clients, allow/deny, settings). The same UI restores from one. Combined with the Unbound config under /etc/unbound/unbound.conf.d/, that's everything needed to rebuild the box.
For automated nightly backups, snapshot /etc/pihole/, /etc/dnsmasq.d/, and /etc/unbound/ with restic.
Performance numbers worth knowing
- Cache hits return in well under 1 ms.
- Cache misses against root + TLD + authoritative typically resolve in 30–120 ms — comparable to a public resolver from most home connections, sometimes faster, occasionally slower for obscure domains.
- After a few days of warm-up, hit rate on a normal household tends to be ~85–95%.
- RAM usage on a Pi: Pi-hole + Unbound ≈ 60–100 MB combined.