How to Set Up Unbound with Pi-hole (Your Own Recursive DNS, No Google or Cloudflare)

Pi-hole (tutorial 34) blocks ads, but it still asks somebody — Cloudflare, Google, your ISP — what each name means, and that somebody sees every site your house visits. Unbound is a recursive resolver: it asks the root servers and works down the chain itself, caches the answers, and validates them with DNSSEC. Pointed at Unbound, Pi-hole never sends your lookups to a third party at all.
Quick reference
| Task | What |
|---|---|
| Run | add an unbound service to Pi-hole's compose file |
| Pi-hole setting | Settings → DNS → Custom upstream: unbound#53, untick the providers |
| Test | docker compose exec pihole dig @unbound example.com |
| DNSSEC test | ... dig @unbound dnssec-failed.org should return SERVFAIL |
| Host install instead | sudo apt install unbound, listen on 127.0.0.1 port 5335 |
1. Add Unbound to the Pi-hole stack
Edit ~/docker/pihole/compose.yaml and add a second service:
unbound:
image: mvance/unbound:latest
container_name: unbound
volumes:
- ./unbound:/opt/unbound/etc/unbound
restart: unless-stopped
and give the Pi-hole service depends_on: [unbound]. No ports need publishing — only Pi-hole talks to it, over the compose network, by name.
docker compose up -d
docker compose logs unbound | tail
The image ships a sensible unbound.conf: recursion from the root servers, DNSSEC validation, caching, listening on port 53 inside the container.
2. Point Pi-hole at it
Pi-hole admin → Settings → DNS. Untick every upstream provider on the left. On the right, Custom 1 (IPv4): unbound#53 — the container name and its port. Save.
If you'd rather install Unbound on the host (sudo apt install unbound), put a config in /etc/unbound/unbound.conf.d/pi-hole.conf with port: 5335 and interface: 127.0.0.1, and the custom upstream becomes 127.0.0.1#5335.
3. Test it
From the server:
docker compose exec pihole dig @unbound example.com
docker compose exec pihole dig @unbound dnssec-failed.org
The first should answer with an IP and status: NOERROR. The second must return SERVFAIL — that's DNSSEC doing its job on a deliberately broken domain. Then browse normally from any device; Pi-hole's dashboard shows the upstream as unbound.
What changes
- Privacy: no single company sees your lookups; the root and authoritative servers each see a fragment.
- Speed: the first lookup of a name is slightly slower (walking the chain); repeats come from Unbound's cache and are faster than before.
- Reliability: no dependence on one provider being up.
Prefetch and cache tuning
In the Unbound config, these keep popular names warm:
prefetch: yes
cache-min-ttl: 300
cache-max-ttl: 86400
Restart the container after editing.
Troubleshooting
Everything resolves SERVFAIL. Unbound can't reach the internet, or the clock is wrong — DNSSEC needs correct time (timedatectl). docker compose logs unbound.
Pi-hole says "DNS not available" after the change. Wrong name or port in the custom upstream. Test with dig from inside the Pi-hole container as above.
Some domains fail that worked before. They have broken DNSSEC and your old provider ignored it. Rare; add domain-insecure: "example.com" for a specific one.
Lookups feel slow on first visit. Expected. Turn on prefetch and give it a day to build a cache.
Wrapping up
One extra service in the Pi-hole compose file, one upstream setting, two dig tests. Your house resolves its own names from the source, validated and cached, and the log of every site you visit stays at home.
