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 there
  • https://raw.githubusercontent.com/hagezi/dns-blocklists/main/wildcard/multi.txt — a maintained, well-tuned set with explicit allow-list curation
  • https://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.

Don't stack too many lists

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

  1. Group Management → Groups → create groups (e.g. kids, work, default).
  2. Group Management → Clients → pin specific client IPs/MACs to the right group.
  3. 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.