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
- 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
- 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
- Create
docker-compose.ymlfor 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:lateston Intel/AMD. Usemvance/unbound-rpi:lateston Raspberry Pi (ARM64). If those tags are unavailable,alpinelabs/unboundis a solid alternative.
- 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
- 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.
- Test DNSSEC validation (Unbound should return
SERVFAILfor broken domains):
dig @127.0.0.1 -p 5335 dnssec-failed.org
Expected: status: SERVFAIL — this confirms DNSSEC validation is working.
- Test through Pi-hole:
dig @127.0.0.1 -p 53 wordforge.example.com
- Check Pi-hole dashboard → Settings → DNS.
- Custom upstream should show
unbound#5335 -
No other upstreams should be listed
-
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-fileis generating correctly:bash docker exec unbound ls -la /var/lib/unbound/ - If
root.keyis 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
- Compare Pi-hole + Unbound vs AdGuard Home
- Add DNS-over-HTTPS for encrypted client queries
- Set up a secondary Pi-hole for redundancy
- Monitor DNS query patterns with Grafana
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-piholefor readers starting from scratchrouter→/homelab-networking-basicsfor VLAN and subnet guidanceconclusion→/adguard-home-dockerfor 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.