Contents

How to Set Up Pi-hole with Unbound for Private, Ad-Free DNS

Every DNS query your devices make tells a story. When your home network forwards those queries to Google (8.8.8.8), Cloudflare (1.1.1.1), or your ISP’s default resolver, that provider accumulates a detailed record of every domain every device visits - your phone, your laptop, your smart TV, your thermostat, all of it. You can fix this by running Pi-hole as a DNS sinkhole to block ads and trackers network-wide, and pair it with Unbound as a local recursive resolver so your queries go directly to the DNS root servers instead of a third-party middleman.

This guide walks through the full setup on a Raspberry Pi or any lightweight Linux box, from installation through hardening and ongoing maintenance.

Why Upstream DNS Providers See Too Much

Most people never think about DNS. Their router gets a DNS server address from the ISP via DHCP, and every device on the network uses it without question. Even tech-savvy users who switch to Google DNS or Cloudflare DNS are just picking a different company to hand their browsing data to.

The problem is simple: your DNS resolver sees every domain you query. Not just the websites you visit in a browser - every NTP sync, every smart TV phoning home, every IoT device checking in with its manufacturer, every app pinging its analytics endpoint. For a household of four with typical devices, that can be thousands of queries per day, and the upstream resolver gets all of them.

You might think DNS-over-HTTPS (DoH) or DNS-over-TLS (DoT) solves this. They encrypt the transport between your device and the resolver, which prevents your ISP from snooping on DNS traffic in transit. But the resolver itself still sees every query in plaintext on their end. You have moved the trust from your ISP to Cloudflare or Google. That is a lateral move, not an improvement.

A local recursive resolver takes a different approach. Instead of forwarding all queries to a single upstream provider, Unbound resolves domains by walking the DNS hierarchy itself. It starts at the root servers, asks them about .com, then asks the .com TLD servers about example.com, then asks example.com’s authoritative nameserver for the actual record. No single server in that chain sees your full query pattern. The root server only knows you asked about .com, not which .com domain. The TLD server knows you asked about example.com but not what you asked the root server before that.

QNAME minimization, defined in RFC 7816 and enabled by default in Unbound 1.22+, takes this further. Without it, Unbound would send the full domain name (say, mail.example.com) to every server in the chain. With QNAME minimization, each server only sees the minimum it needs: the root server sees a query for .com, the .com TLD server sees a query for example.com, and only the authoritative nameserver for example.com sees the full mail.example.com query.

Layer Pi-hole on top of this and you get network-wide ad blocking before queries even reach Unbound. Blocked domains return 0.0.0.0 immediately, saving bandwidth and eliminating tracking requests across every device on your network without installing anything on the devices themselves.

Architecture Overview

Understanding how the pieces connect makes configuration and troubleshooting much easier. Here is the full query flow:

  1. A device on your network (phone, laptop, smart TV) makes a DNS query
  2. Your router’s DHCP server has assigned the Pi-hole’s IP as the DNS server, so the query goes to Pi-hole
  3. Pi-hole checks the domain against its blocklists (the gravity database). If the domain is blocked, Pi-hole returns 0.0.0.0 and the query stops here
  4. If the domain is allowed, Pi-hole forwards the query to Unbound running on localhost port 5335
  5. Unbound starts recursive resolution: it consults its root hints file, queries the appropriate root server, follows the referral to the TLD server, then to the authoritative nameserver
  6. Unbound validates the response using DNSSEC, checking the cryptographic chain of trust from the root zone down
  7. The validated response flows back through Pi-hole to the requesting device

Caching happens at two layers. Pi-hole caches responses from Unbound based on the TTL (time to live) that the authoritative nameserver set. Unbound maintains its own cache and can prefetch popular domains before their TTL expires, so frequently visited sites resolve almost instantly.

The entire stack runs comfortably on a Raspberry Pi 4 or 5. Total RAM usage stays under 200 MB and CPU load is negligible. Any Debian-based Linux box with a static IP on your LAN will work just as well.

Step-by-Step Installation

This section assumes a fresh Debian 12, Ubuntu 24.04 LTS, or Raspberry Pi OS installation with a static IP address already configured. Replace <pihole-ip> with the actual IP of your machine throughout.

Install Pi-hole

Run the official installer:

curl -sSL https://install.pi-hole.net | bash

The interactive installer walks through network configuration and upstream DNS selection. When it asks you to select an upstream DNS provider, choose “Custom” and leave it blank for now - you will point it at Unbound after that is configured. Note the admin password shown at the end of installation, or set a new one with:

pihole -a -p

Install and Configure Unbound

Install Unbound from the default repos:

sudo apt install unbound

Current Debian and Ubuntu repos include Unbound 1.22+, which has QNAME minimization enabled by default. Next, download the root hints file that tells Unbound where the DNS root servers are:

sudo wget -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root

Now create the configuration file that tells Unbound to listen on localhost for Pi-hole’s queries. Create /etc/unbound/unbound.conf.d/pi-hole.conf with the following content:

server:
    verbosity: 0

    interface: 127.0.0.1
    port: 5335
    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    do-ip6: no

    # Security hardening
    harden-glue: yes
    harden-dnssec-stripped: yes
    harden-referral-path: no
    harden-algo-downgrade: no

    # 0x20 encoding for query name randomization
    use-caps-for-id: yes

    # Cache tuning
    edns-buffer-size: 1232
    prefetch: yes
    prefetch-key: yes

    # Serve stale cache on upstream failure
    serve-expired: yes
    serve-expired-ttl: 86400

    num-threads: 1
    so-rcvbuf: 1m

    # DNSSEC trust anchor
    auto-trust-anchor-file: /var/lib/unbound/root.key

    # Root hints
    root-hints: /var/lib/unbound/root.hints

    # Prevent queries to localhost (except Pi-hole)
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8
    private-address: fd00::/8
    private-address: fe80::/10

Restart Unbound and verify it is running:

sudo systemctl restart unbound
sudo systemctl status unbound

Verify DNSSEC Validation

Test that DNSSEC validation works correctly:

dig sigfail.verteiltesysteme.net @127.0.0.1 -p 5335

This should return SERVFAIL because the domain has intentionally broken DNSSEC records.

dig sigok.verteiltesysteme.net @127.0.0.1 -p 5335

This should return NOERROR with a valid A record. If both tests pass, DNSSEC validation is working.

Connect Pi-hole to Unbound

Open the Pi-hole web admin interface at http://<pihole-ip>/admin, go to Settings, then DNS. Remove all upstream DNS servers and add a single custom entry:

127.0.0.1#5335

Save the settings. Pi-hole now forwards all non-blocked queries to your local Unbound instance.

Test the Full Chain

From any machine on your network, test resolution through Pi-hole:

dig example.com @<pihole-ip>

You should get a valid response. Run it a second time and notice the query time drops dramatically as the result is served from cache. You can also watch queries in real time:

pihole -t

This shows a live tail of the query log, including which domains are blocked and which are forwarded to Unbound.

Hardening and Blocklist Management

The default setup works, but tuning the blocklists and security settings makes a real difference in day-to-day use.

Expanding Blocklists

Pi-hole ships with Steven Black’s unified hosts list, which blocks roughly 170,000 domains. That is a decent baseline but leaves a lot of tracking domains untouched. Consider adding supplementary lists:

BlocklistDomains BlockedNotes
OISD Full400,000+Broad coverage, community-maintained
Hagezi Light~80,000Minimal false positives
Hagezi Pro~300,000Good balance of coverage and usability
Hagezi Ultimate~700,000Aggressive, may need whitelist entries

Add lists through the Pi-hole admin interface under Adlists, then update gravity:

pihole -g

Group-Based Blocking

Pi-hole’s group management lets you assign different blocklist policies to different devices. This is useful when a smart TV needs aggressive blocking (Samsung TVs are notorious for analytics traffic) but a work laptop needs access to certain analytics domains for testing. Create groups in the admin interface, assign clients to groups by MAC address or IP, and assign adlists to specific groups.

Unbound Security Settings

The configuration file above already includes the important hardening options. A few additional settings are useful:

  • use-caps-for-id: yes enables 0x20 encoding, which randomizes the capitalization in query names as an additional defense against cache poisoning attacks
  • ip-ratelimit: 1000 can be added to prevent DNS amplification if the resolver is accidentally exposed to the internet (though binding to 127.0.0.1 already prevents this)
  • serve-expired: yes with serve-expired-ttl: 86400 means Unbound will serve stale cache entries for up to 24 hours if upstream resolution temporarily fails, keeping your network functional during brief connectivity issues

Keeping Things Updated

The root hints file changes occasionally as root server IP addresses are updated. Download a fresh copy monthly. Blocklist gravity should be updated more frequently. A weekly cron job or systemd timer handles both:

# /etc/cron.d/dns-maintenance
0 4 * * 0 root pihole -g
0 4 1 * * root wget -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root && systemctl restart unbound

Monitoring, Troubleshooting, and Maintenance

A DNS resolver that silently fails is worse than not having one at all. You need visibility into what is happening.

Pi-hole Dashboard

The web interface at http://<pihole-ip>/admin provides real-time data: total queries, percentage blocked, top blocked domains, top allowed domains, and per-client breakdowns. This is the fastest way to spot anomalies - if a device is making thousands of queries to odd domains, you will see it here.

Unbound Statistics

Check Unbound’s performance with:

sudo unbound-control stats_noreset

Key metrics to watch:

  • Cache hit ratio is the most telling number. A healthy setup shows 60-80% cache hits after a day or two of operation. Lower than that might mean your cache size is too small or prefetching is not working.
  • Query time averages tell you about resolution performance. First queries for uncached domains typically take 50-150ms because Unbound is walking the full DNS hierarchy. Cached responses should be under 1ms.
  • Error counts are worth tracking too. Occasional SERVFAIL responses are normal (broken DNSSEC on remote domains), but a sudden spike means something is wrong.

Common Issues and Fixes

systemd-resolved conflicts: On Ubuntu systems, systemd-resolved often binds to port 53 and can overwrite /etc/resolv.conf. Either disable it entirely or configure it to forward to Pi-hole:

sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf

Alternatively, if you want to keep systemd-resolved running, edit /etc/systemd/resolved.conf and set DNSStubListener=no.

DNSSEC failures for specific domains: Some domains have misconfigured DNSSEC records. Check with:

dig +dnssec problematic-domain.com @127.0.0.1 -p 5335

If the authoritative nameserver has broken DNSSEC, the domain will fail validation in Unbound. You can add the domain to Pi-hole’s whitelist, but the real fix is on the domain operator’s side.

Slow first resolution: The first query after Unbound starts (or for any uncached domain) is slower because Unbound walks the full hierarchy. This is normal. Once the cache warms up after a few hours of regular use, most queries resolve from cache. The prefetch: yes setting helps by refreshing popular entries before they expire.

Backup and Restore

Back up your Pi-hole configuration regularly:

pihole -a -t

This creates a teleporter archive containing your settings, blocklists, whitelist, blacklist, local DNS records, and CNAME records. Store it somewhere safe. Restoring is a single click in the admin interface.

High Availability

For a setup that survives a Pi-hole reboot without taking down DNS for the entire household, run a second Pi-hole + Unbound instance on another machine. Configure your DHCP server to advertise both IPs as DNS servers. The gravity-sync tool keeps blocklists synchronized between the two instances so you only manage adlists in one place.

Wrapping Up

This setup takes about 30 minutes to get running and delivers two concrete improvements: network-wide ad blocking without installing anything on individual devices, and DNS resolution that does not hand your complete browsing history to a third party. Pi-hole handles the filtering, Unbound handles private recursive resolution with DNSSEC validation, and the whole thing runs on hardware you probably already own.

The ongoing maintenance is minimal - update blocklists weekly, refresh root hints monthly, and occasionally check the dashboard for anything unusual. Once it is running, you mostly forget about it, which is the best thing you can say about any piece of infrastructure.