Pi-hole + Unbound on Raspberry Pi 4 (Homelab Guide)
Network-wide ad blocking with a validating, recursive, caching DNS resolver.
Introduction
This document walks through step-by-step installation of Pi-hole (DNS sinkhole and ad blocker) on a Raspberry Pi 4, together with Unbound as a local recursive and validating DNS resolver. This combination ensures privacy, speed, and reliability – without relying on external DNS providers.
Required Hardware
- Raspberry Pi 4 (2–4 GB RAM recommended) + power supply
- microSD (≥ 16 GB, A1/A2 class recommended)
- Ethernet cable (more stable than Wi-Fi for DNS server)
- Case + cooling (optional)
Software Installation Steps
- Flash Raspberry Pi OS Lite (64‑bit) using Raspberry Pi Imager. Enable SSH and set a hostname.
- First login and system update:
sudo apt update && sudo apt full-upgrade -y sudo reboot
- Set static IP address (example in
dhcpcd.conf
):sudo nano /etc/dhcpcd.conf # Example (adjust to your network): interface eth0 static ip_address=192.168.1.10/24 static routers=192.168.1.1 static domain_name_servers=127.0.0.1
Note: For LAN DNS clients, the DHCP gateway should advertise this Pi-hole server's IP.
- Install Pi-hole (official installer):
curl -sSL https://install.pi-hole.net | bash
During installation, select the interface (
eth0
), static IP, and temporary upstream DNS (e.g., Quad9). We will later switch to Unbound. - Install Unbound:
sudo apt install -y unbound
- Download root hints (optional cron for monthly refresh):
sudo mkdir -p /var/lib/unbound sudo wget -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root # (Optional) monthly auto-refresh: echo '0 3 1 * * root wget -qO /var/lib/unbound/root.hints https://www.internic.net/domain/named.root' | sudo tee /etc/cron.d/unbound-root-hints
Configuration Steps
- Unbound configuration – create dedicated file:
sudo nano /etc/unbound/unbound.conf.d/pi-hole.conf
server: verbosity: 0 logfile: "/var/log/unbound/unbound.log" interface: 127.0.0.1 port: 5335 do-daemonize: no use-caps-for-id: no edns-buffer-size: 1232 prefetch: yes qname-minimisation: yes harden-dnssec-stripped: yes harden-glue: yes harden-referral-path: yes aggressive-nsec: yes root-hints: "/var/lib/unbound/root.hints" cache-min-ttl: 3600 cache-max-ttl: 86400 msg-cache-size: 128m rrset-cache-size: 256m so-rcvbuf: 1m so-sndbuf: 1m access-control: 127.0.0.0/8 allow
Check config and start service:
sudo unbound-checkconf sudo systemctl enable --now unbound
- Pi-hole → Upstream DNS = Unbound:
In http://<IP-of-Pi>/admin → Settings → DNS enable Custom 1 (IPv4) and set:
127.0.0.1#5335
Disable all other external upstream options so queries go only through Unbound.
- DNSSEC: With Unbound as validator, you usually do not enable DNSSEC inside Pi-hole (Unbound already validates).
- Disable conflicting services on port 53 (if any):
sudo systemctl disable --now systemd-resolved
Verification / Tests
# Query Unbound directly
dig @127.0.0.1 -p 5335 example.com +dnssec +multi
# Query via Pi-hole (port 53)
dig @127.0.0.1 example.com +dnssec +multi
# Test DNSSEC validation
dig @127.0.0.1 -p 5335 dnssec-failed.org A +dnssec
# Expect SERVFAIL (validation works)
Troubleshooting Tips
- Port 53 in use? Check:
sudo ss -tulpn | grep :53
. Stop or reconfigure the conflicting service. - Unbound not starting? Check logs:
sudo journalctl -u unbound -b
, and runsudo unbound-checkconf
. - Slow resolution? Ensure
edns-buffer-size
≤ 1232 and MTU settings are correct (try 1500/1492). - Pi-hole not blocking? Update gravity:
raspberrypi -g
and confirm LAN clients use the Pi-hole IP as DNS. - DNSSEC problems? Test with
dnssec-failed.org
. If all queries fail, temporarily disable DNSSEC in Pi-hole (keep Unbound validating). - Outdated root hints? Refresh manually or via cron job as shown above.
Reference
Based on tutorial: Crosstalk Solutions — The World’s Greatest Pi-hole + Unbound Tutorial (2023) .
© 2025 Kubernix — Homelab Documentation. Prepared for Raspberry Pi 4 and Pi-hole + Unbound setup with focus on privacy and performance.