Reading time: ~13 minutes
Audience: Privacy-focused homelab users who want to stop leaking DNS queries to Cloudflare or Google
Last tested: Pi-hole FTL v6.0, Unbound 1.22, June 2026


Why Add Unbound to Pi-hole?

The Privacy Gap in Standard Pi-hole

By default, Pi-hole forwards filtered queries to upstream resolvers like Cloudflare (1.1.1.1), Quad9 (9.9.9.9), or Google (8.8.8.8). While these are fast and reliable, they see every domain you visit. Even with DNS-over-HTTPS (DoH), you are trusting a third party with your browsing metadata.

Unbound is a validating, recursive, caching DNS resolver. Instead of asking Google “what is example.com?”, Unbound asks the root DNS servers, then the .com TLD servers, then the authoritative nameservers for example.com. No third party ever sees your full query log.

Key Benefits

Feature Pi-hole Only Pi-hole + Unbound
Ad blocking
Tracker blocking
Recursive resolution
Third-party DNS logs Yes (Cloudflare, etc.) No
DNSSEC validation Partial Full end-to-end
Local caching Minimal Aggressive

Prerequisites

Hardware Requirements

Unbound requires slightly more RAM than forwarding to Cloudflare because it maintains a cache of DNS records:

Metric Minimum Recommended
RAM 512 MB 1 GB
CPU 1 core 2 cores (for initial root server queries)
Storage 8 GB 16 GB SSD (for Unbound cache and logs)

Software Requirements

  • Existing Pi-hole Docker deployment or willingness to deploy both together
  • Docker Engine and Docker Compose plugin
  • A static local IP for the DNS stack

Step 1: Deploy Unbound as a Separate Container

Objective

Create an Unbound container that Pi-hole will use as its sole upstream resolver.

Step-by-Step Instructions

  1. If you already have Pi-hole running, create a new directory for Unbound:
mkdir -p ~/unbound && cd ~/unbound

If you are starting fresh, create a combined stack:

mkdir -p ~/pihole-unbound && cd ~/pihole-unbound
  1. Create unbound.conf:
cat > unbound.conf << 'EOF'
server:
    verbosity: 0
    num-threads: 2
    interface: 0.0.0.0
    port: 5335
    do-ip4: yes
    do-ip6: yes
    do-udp: yes
    do-tcp: yes
    so-rcvbuf: 1m
    so-sndbuf: 1m

    harden-glue: yes
    harden-dnssec-stripped: yes
    harden-referral-path: yes
    unwanted-reply-threshold: 10000
    val-clean-additional: yes
    edns-buffer-size: 1232
    prefetch: yes
    prefetch-key: yes
    cache-min-ttl: 300
    cache-max-ttl: 86400
    msg-cache-slabs: 2
    rrset-cache-slabs: 2
    infra-cache-slabs: 2
    key-cache-slabs: 2
    rrset-cache-size: 64m
    msg-cache-size: 32m
    soa-cache-size: 128k
    neg-cache-size: 128k

    # Performance tuning
    qname-minimisation: yes
    qname-minimisation-strict: no
    aggressive-nsec: yes
    delay-close: 10000

    # Private ranges (do not forward these)
    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

    # Root hints are built into the image; no extra config needed
    auto-trust-anchor-file: "/var/lib/unbound/root.key"
EOF
  1. Create docker-compose.yml for the combined stack:
version: "3.8"

services:
  unbound:
    container_name: unbound
    image: mvance/unbound-rpi:latest  # Use mvance/unbound:latest for x86_64
    restart: unless-stopped
    volumes:
      - ./unbound.conf:/etc/unbound/unbound.conf.d/custom.conf:ro
    ports:
      - "5335:5335/tcp"
      - "5335:5335/udp"
    healthcheck:
      test: ["CMD", "dig", "@127.0.0.1", "-p", "5335", "dnssec-failed.org"]
      interval: 30s
      timeout: 10s
      retries: 3

  pihole:
    container_name: pihole
    image: pihole/pihole:2025.07.0
    restart: unless-stopped
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "8080:80/tcp"
    environment:
      - TZ=Europe/Berlin
      - WEBPASSWORD=changeme
      - FTLCONF_LOCAL_IPV4=192.168.1.10
      - PIHOLE_DNS_=unbound#5335  # Point to Unbound container
    volumes:
      - ./etc-pihole:/etc/pihole
      - ./etc-dnsmasq.d:/etc/dnsmasq.d
    depends_on:
      unbound:
        condition: service_healthy
    cap_add:
      - NET_ADMIN
    dns:
      - 127.0.0.1

Image selection: Use mvance/unbound:latest on Intel/AMD. Use mvance/unbound-rpi:latest on Raspberry Pi (ARM64). If those tags are unavailable, alpinelabs/unbound is a solid alternative.

  1. Create directories and start:
mkdir -p etc-pihole etc-dnsmasq.d
docker compose up -d

Step 2: Verify Recursive Resolution

Objective

Confirm that Unbound is resolving and Pi-hole is using it.

Step-by-Step Instructions

  1. Test Unbound directly:
dig @127.0.0.1 -p 5335 wordforge.example.com

You should see NOERROR and an IP address in the ANSWER SECTION.

  1. Test DNSSEC validation (Unbound should return SERVFAIL for broken domains):
dig @127.0.0.1 -p 5335 dnssec-failed.org

Expected: status: SERVFAIL — this confirms DNSSEC validation is working.

  1. Test through Pi-hole:
dig @127.0.0.1 -p 53 wordforge.example.com
  1. Check Pi-hole dashboard → Settings → DNS.
  2. Custom upstream should show unbound#5335
  3. No other upstreams should be listed

  4. Confirm no external resolvers are used:

docker logs pihole | grep "forwarded"

You should see to unbound#5335.


Step 3: Tune Unbound Performance

Objective

Optimize cache sizes and thread counts for your hardware.

Step-by-Step Instructions

For low-power hosts (Raspberry Pi, N100):

Keep the default config above. It is already tuned for modest hardware.

For dedicated servers with 4+ cores and 4+ GB RAM:

Increase cache sizes in unbound.conf:

num-threads: 4
rrset-cache-size: 256m
msg-cache-size: 128m
so-rcvbuf: 4m
so-sndbuf: 4m

Restart Unbound:

docker compose restart unbound

Monitor cache hit rate:

docker exec unbound unbound-control stats_noreset | grep cache

A hit rate above 70% is excellent for a home network.


Step 4: Maintain and Update

Objective

Keep both services updated without losing configuration.

Step-by-Step Instructions

Update both containers:

cd ~/pihole-unbound
docker compose pull
docker compose up -d

Clear Unbound cache if needed:

docker exec unbound unbound-control flush

Backup your configuration:

tar czf pihole-unbound-backup-$(date +%Y%m%d).tar.gz etc-pihole etc-dnsmasq.d unbound.conf docker-compose.yml .env

Pro Tips

Tip 1: Add DNS-over-TLS for External Queries

If you occasionally need to query an external resolver (e.g., for GeoCDN accuracy), configure Unbound to forward specific zones over TLS:

forward-zone:
    name: "."
    forward-addr: 1.1.1.1@853#cloudflare-dns.com
    forward-ssl-upstream: yes

This hybrid mode uses Unbound for recursion but encrypts the final hop.

Tip 2: Split-Horizon DNS for Homelab

Unbound can serve local records without Pi-hole’s involvement:

local-zone: "lan." static
local-data: "proxmox.lan. IN A 192.168.1.5"
local-data: "nas.lan. IN A 192.168.1.20"

Restart Unbound and query: dig @192.168.1.10 -p 5335 proxmox.lan

Tip 3: Reduce TTL for Faster Blocklist Updates

When you add a new blocklist to Pi-hole, old cached records may persist. Lower Unbound’s cache-min-ttl to 60 seconds temporarily, then raise it back to 300 after Gravity updates.

Tip 4: Run on a Separate VLAN

For network segmentation, place Pi-hole + Unbound on a dedicated management VLAN (e.g., VLAN 10). Ensure your router allows UDP/TCP 53 from all client VLANs to the DNS VLAN.


Troubleshooting Common Issues

connection refused” on Port 5335

  • Unbound container may not be fully started. Check logs: bash docker compose logs unbound
  • Ensure no other service is using port 5335 on the host.

Slow Initial Resolution

  • Unbound must populate its cache from root servers. The first query to any domain is slower (~200–500 ms) than Cloudflare (~20 ms). Subsequent queries are cached and typically faster than external resolvers.
  • This is normal and improves dramatically after 24 hours of use.

Server Failed” for All Queries

  • Check that auto-trust-anchor-file is generating correctly: bash docker exec unbound ls -la /var/lib/unbound/
  • If root.key is missing, generate it manually: bash docker exec unbound unbound-anchor -a /var/lib/unbound/root.key docker compose restart unbound

Conclusion

Summary

You now have a privacy-first DNS stack: Pi-hole filters ads and trackers, while Unbound performs recursive resolution without leaking your browsing history to upstream providers. Your network enjoys faster cached DNS, end-to-end DNSSEC validation, and complete query autonomy.

Next Steps


Affiliate Opportunities

  • Raspberry Pi: Pi 4/5 with PoE HAT for always-on DNS
  • SSD: Small 32 GB SSD for Pi-hole + Unbound persistent storage
  • UPS: APC unit to maintain DNS during outages

Internal Linking Strategy

  • what-is/homelab-ad-blocking-pihole for readers starting from scratch
  • router/homelab-networking-basics for VLAN and subnet guidance
  • conclusion/adguard-home-docker for readers comparing DNS filter options

CTA

Are you running recursive DNS at home? Share your block rate and cache hit percentage in the comments!

Subscribe to the WordForge newsletter for weekly homelab privacy and networking deep dives.