Pi-hole + Unbound: Self-Hosted Recursive DNS

The most surprising thing about running your own recursive DNS resolver is how much less you’re actually doing, not more.

Let’s see it in action. Imagine your browser wants to resolve example.com.

  1. Browser: "Hey, what’s the IP for example.com?"
  2. Pi-hole: "Okay, I’ll ask my upstream resolver."
  3. Unbound (running on the same machine as Pi-hole): "Hmm, I don’t have that cached. I’ll ask the root servers."
  4. Unbound to . (root) server: "Where can I find .com?"
  5. Root server to Unbound: "Here’s the IP of a .com name server."
  6. Unbound to .com name server: "Where can I find example.com?"
  7. .com server to Unbound: "Here’s the IP of example.com’s authoritative name server."
  8. Unbound to example.com’s authoritative server: "What’s the IP for example.com?"
  9. Authoritative server to Unbound: "It’s 93.184.216.34."
  10. Unbound caches 93.184.216.34 for example.com and returns it to Pi-hole.
  11. Pi-hole caches it and returns it to your browser.

This is the "recursive" part: Unbound does all the legwork, following the chain of delegation from the root servers down to the authoritative server for the domain. Pi-hole’s job is simply to block ads by checking its blocklists before it even asks Unbound to resolve anything. If it’s blocked, Pi-hole returns 0.0.0.0 immediately. If not, it forwards the request to Unbound.

This setup solves a few problems. First, privacy: you’re not sending your DNS queries to Google (8.8.8.8) or Cloudflare (1.1.1.1), which can log and analyze your browsing habits. Second, security: Unbound can be configured to use DNSSEC for authenticated denial of existence and validated responses, ensuring you’re talking to the real server. Third, speed (potentially): by caching frequently accessed domains locally, Unbound can serve them faster than fetching them from external resolvers every time.

The core configuration for Unbound typically lives in /etc/unbound/unbound.conf. Here’s a minimal example for running it alongside Pi-hole, assuming both are on the same server (e.g., 192.168.1.10):

server:
  verbosity: 1
  interface: 127.0.0.1     # Listen on localhost for Pi-hole
  interface: 192.168.1.10  # Listen on the server's LAN IP for other devices
  port: 53                 # Standard DNS port
  access-control: 127.0.0.0/8 allow # Allow localhost
  access-control: 192.168.1.0/24 allow # Allow your LAN subnet
  do-ip4: yes
  do-ip6: no               # Disable IPv6 if not used
  do-udp: yes
  do-tcp: yes
  hide-identity: yes
  hide-version: yes
  harden-dnssec-stripped: yes
  use-caps-for-id: yes
  cache-min-ttl: 3600      # Keep records for at least 1 hour
  cache-max-ttl: 86400     # Keep records for at most 24 hours
  prefetch: yes            # Proactively refresh expired records
  prefetch-key: yes        # Proactively refresh DNSSEC keys
  num-threads: 4           # Adjust based on your CPU cores
  so-rcvbuf: 4m            # Increase buffer size
  so-sndbuf: 4m            # Increase buffer size
  logfile: "/var/log/unbound/unbound.log" # Ensure this directory exists and unbound has write permissions

remote-control:
  control-enable: yes
  control-interface: 127.0.0.1
  control-port: 8953
  control-key-file: "/etc/unbound/unbound.key"
  control-cert-file: "/etc/unbound/unbound.pem"

Make sure to create the log directory (sudo mkdir /var/log/unbound) and set ownership (sudo chown unbound:unbound /var/log/unbound). You’ll also need to generate SSL certificates for remote-control if you plan to use it.

In Pi-hole’s web interface, go to Settings -> DNS and uncheck all public DNS servers. Then, under "Upstream DNS Servers," enter the IP address of your Unbound server (e.g., 192.168.1.10#53 or 127.0.0.1#53 if Pi-hole and Unbound are on the same machine) and check the "Listen on all interfaces" box if Unbound is configured to listen on its LAN IP.

The prefetch and prefetch-key options are crucial for performance. When a DNS record is about to expire from the cache, Unbound will proactively fetch a fresh copy before it’s actually needed. This means the next time Pi-hole asks for that record, Unbound can serve it instantly from its cache, avoiding the round trip to the upstream servers. This is especially effective for domains with short TTLs (Time To Live) or during periods of high network activity.

The so-rcvbuf and so-sndbuf settings are often overlooked. Increasing the socket receive and send buffer sizes allows Unbound to handle more concurrent queries and responses more efficiently, preventing dropped packets and improving throughput, especially under load. Setting them to 4m (4 megabytes) is a common recommendation for home servers.

Most people don’t realize that Unbound, when configured correctly for recursion, doesn’t actually need to know about the entire DNS hierarchy. It only needs to know the IP addresses of the root DNS servers. From there, it dynamically discovers the IP addresses of the TLD servers (like .com, .org) and then the authoritative servers for the specific domain being queried. This dynamic discovery is what makes the recursive process work without needing a massive, pre-populated database of the entire internet.

Once you have this working, the next logical step is exploring DNS over TLS (DoT) or DNS over HTTPS (DoH) for encrypting your own DNS traffic from your Pi-hole/Unbound server to external DNS providers, or even setting up Unbound to act as a DoT/DoH server for your clients.

Want structured learning?

Take the full Pihole course →