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

Unbound: resolve DNS yourself — Pi-hole stops the ads. Unbound stops the snooping.

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

TaskWhat
Runadd an unbound service to Pi-hole's compose file
Pi-hole settingSettings → DNS → Custom upstream: unbound#53, untick the providers
Testdocker compose exec pihole dig @unbound example.com
DNSSEC test... dig @unbound dnssec-failed.org should return SERVFAIL
Host install insteadsudo 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

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.

Call us