Why Pi-hole is the first service I bring up on any network
A few months back I rebuilt my home lab from scratch. New VLANs, fresh DHCP scopes, cleaner firewall rules. Same old problem: my devices were still leaking requests to ad/telemetry domains, my smart TV was aggressively phoning home, and my gaming PC’s browser felt like it was dragging a caravan of trackers behind it.
Dropping Pi-hole back into the mix did two things immediately:
- Pages felt snappier because repeat lookups were coming from a local cache instead of a faraway resolver.
- The noise disappeared. DNS-level blocking kneecaps a ridiculous amount of junk before it even reaches your browser or apps.
And because I’m a developer who constantly flips between staging domains, local services, and random cloud endpoints, Pi-hole became more than an ad blocker. It’s my central DNS control plane for the house.
This post is the guide I wish I had the first time:
- Practical hardware picks (including a Raspberry Pi Zero + USB‑Ethernet build for low latency) and “use whatever you have” alternatives
- A clean install path on Raspberry Pi OS (Bookworm) or Docker
- The Unbound recursive resolver setup I recommend for privacy and independence
- The real‑world DNS issues you’ll hit (systemd‑resolved, IPv6 bypass, conditional forwarding, hairpin NAT, mDNS/.local collisions) and how to fix them
- Sensible blocklists, per‑client policies, and firewall rules that stop clever devices from bypassing your Pi-hole
- Why developers should treat Pi-hole as essential local tooling
Hardware: go wired, stay tiny, be boring
My favourite: Raspberry Pi Zero with a USB‑to‑Ethernet adapter via OTG, powered from a decent 5V supply. Stick it behind the router/switch and forget about it.
Why the Zero? Because Pi-hole barely sips CPU/RAM. DNS is lightweight and benefits more from low latency than raw compute. Wi‑Fi works, but wired Ethernet gives you stability, low jitter, and fewer weird edge cases. If you only take one opinionated sentence from me, take this: run your DNS server on wired Ethernet.
Alternatives that work fine: any Pi with Ethernet (Pi 3/4), a cheap x86 mini‑PC, an old NUC, a VM or LXC on Proxmox, or a Docker container on your NAS. Pi-hole doesn’t care as long as it can bind to port 53 and keep its clock right.
Storage: an 8–16 GB microSD card is plenty for Pi-hole logs and the Unbound package. If you like durability, use a small USB SSD or enable log2ram, but it’s not required.
Network placement: plug into your core switch or the router LAN port. If you’re virtualising, give the container/VM a first‑class IP on your LAN (macvlan or host networking) so clients can actually reach it on port 53.
Install paths
Pick one. I’ll show both.
A) Bare‑metal Raspberry Pi OS (Bookworm) on a Pi Zero or any Pi
-
Image the OS
- Use Raspberry Pi Imager, choose “Raspberry Pi OS Lite (64‑bit)” for Pi 3/4/5 or “Lite (32‑bit)” for Zero/Zero 2.
- Hit the gear icon and preconfigure: hostname, username, SSH, timezone, optional Wi‑Fi (we won’t use it for the DNS box), and set a static IP or at least a DHCP reservation later.
-
First boot and updates
sudo apt update && sudo apt full-upgrade -y sudo reboot
-
Install Pi-hole
curl -sSL https://install.pi-hole.net | bash
During the installer:
- Upstream DNS: choose Custom for now (we’ll point to Unbound later), or pick a provider temporarily.
- Privacy level: your call; I start with default and later switch to Anonymous mode once everything’s verified.
-
Give the Pi a stable address
- Either set a DHCP reservation on your router, or configure a static IP on the Pi. A reservation is simpler and avoids fights with NetworkManager/dhcpcd.
B) Docker (on any Linux box or NAS)
If you can run it on bare metal, do that. If not, here’s a solid Docker Compose that also works great on a Pi 4/5 or an x86 mini‑PC.
services:
pihole:
image: pihole/pihole:latest
container_name: pihole
hostname: pihole
network_mode: host # or use a macvlan network to give it a LAN IP
environment:
TZ: "Europe/Bucharest"
WEBPASSWORD: "change-me"
DNSMASQ_LISTENING: "all" # if not using host networking
FTLCONF_LOCAL_IPV4: "192.168.1.10" # replace with your IP
# Optional: change web port when using host mode
# WEB_PORT: "8080"
volumes:
- ./etc-pihole:/etc/pihole
- ./etc-dnsmasq.d:/etc/dnsmasq.d
cap_add:
- NET_ADMIN
restart: unless-stopped
Notes
network_mode: host
is simplest on Linux hosts. If you can’t use host mode, create a macvlan network so the container gets a real LAN IP.- On bridge networking, set Interface listening behaviour in the Pi-hole UI to “Listen on all interfaces, permit all origins,” otherwise cross‑subnet clients won’t resolve.
Make it private: run your own recursive resolver with Unbound
Forwarding DNS to a public resolver is easy but hands them your metadata. Running Unbound locally keeps resolution and caching under your control.
Install and configure:
sudo apt install -y unbound
# /etc/unbound/unbound.conf.d/pi-hole.conf
sudo tee /etc/unbound/unbound.conf.d/pi-hole.conf >/dev/null <<'EOF'
server:
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
edns-buffer-size: 1232
prefetch: yes
num-threads: 1
so-rcvbuf: 1m
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
EOF
sudo systemctl restart unbound
Test Unbound directly:
# First lookup may be slow, subsequent ones will be cached
dig @127.0.0.1 -p 5335 pi-hole.net +short
Then in Pi-hole → Settings → DNS, untick any public upstreams, and set Custom 1 (IPv4) to 127.0.0.1#5335
. Do the same for IPv6 if you like: ::1#5335
.
If you want DNSSEC validation, keep it enabled in Pi-hole and Unbound will handle the heavy lifting.
Why recursive? Aside from privacy, a warm local cache is fast, and you’re not beholden to a third‑party resolver’s filtering/outages/policies. For local dev, it’s also nice to own every part of resolution.
Fixing the usual DNS gremlins
You will hit at least one of these. Save yourself the rage‑Google and deal with them early.
1) Port 53 is already in use
On Ubuntu/Debian, systemd-resolved
can grab port 53. Either disable its stub listener or stop the service entirely.
# Option A: keep systemd-resolved, disable the stub listener
sudo sed -i 's/^#\?DNSStubListener=.*/DNSStubListener=no/' /etc/systemd/resolved.conf
sudo systemctl restart systemd-resolved
# Option B: disable systemd-resolved and point resolv.conf elsewhere
sudo systemctl disable --now systemd-resolved
sudo rm -f /etc/resolv.conf
printf 'nameserver 127.0.0.1\noptions edns0 trust-ad\n' | sudo tee /etc/resolv.conf
If you also run Unbound, ensure Unbound listens on 5335 and Pi-hole on 53 to avoid collisions.
2) Clients aren’t using Pi-hole
- Your router must hand out Pi-hole’s IP as DNS via DHCP.
- If you enable Pi-hole’s DHCP server, disable the router’s DHCP to avoid double leases. Move scopes carefully then reboot clients.
- Some phones, TVs and consoles hardcode public DNS. Intercept or block that traffic at the firewall (more on that below).
3) Local hostnames don’t resolve
There are three approaches. Pick one:
- Conditional forwarding: in Pi-hole → Settings → DNS, set your router’s IP as the conditional forwarder and configure the local domain (e.g.
home.arpa
). Your router must actually resolve its DHCP leases. - Pi-hole as DHCP: Pi-hole knows hostnames it handed out and will resolve them. Clean and self‑contained.
- Static local records: in Pi-hole → Local DNS → DNS Records, add A/AAAA for always‑on gear like NAS, controllers, and homelab hosts.
Avoid using .local
for your LAN domain. That suffix is reserved by mDNS and causes hair‑pulling. Prefer home.arpa
or a subdomain you own.
4) Hairpin NAT and split‑DNS
If you self‑host services on a public domain and try to reach them from inside your LAN, some routers lack hairpin NAT. Fix with split-horizon DNS: create a local A/AAAA record in Pi-hole for app.example.com
pointing to the internal IP. Externally it still resolves to your public IP.
5) IPv6 bypasses your Pi-hole
If your ISP hands out IPv6 and your router advertises IPv6 DNS via RA or DHCPv6, clients may skip your Pi-hole entirely. Options:
- Advertise Pi-hole’s IPv6 address as DNS (ULA is ideal if your router supports it), or
- Don’t advertise any IPv6 DNS server and force IPv4 DNS only, or
- As a last resort, turn off IPv6 on the LAN.
6) Docker networking weirdness
If you’re running Pi-hole in Docker and clients are shown as the Docker bridge, use host networking on Linux or give the container a macvlan LAN IP. On bridge networking, set Interface listening behaviour to “permit all origins.”
Blocklists: start small, stay maintainable
Pi-hole ships with a default list that does a decent job. Resist the urge to install 50 mega-lists and then spend weekends fixing false positives. My baseline:
- StevenBlack’s unified hosts (good general coverage)
- One tracking/telemetry list you trust
- Optional: a malware/phishing list
I use per‑client groups heavily. TVs and guest devices get stricter lists. Developer machines get lighter blocking so npm/pip/docker don’t explode mid‑install.
YouTube and a few in‑app ad platforms are hard at the DNS layer because ads resolve from the same domains as content. Manage expectations and consider browser extensions for YouTube on desktops.
Pi-hole refreshes adlists weekly by default. You can always force an update with pihole -g
.
Per‑client policies and developer‑friendly tricks
This is where Pi-hole becomes a tool rather than just an appliance.
- Per‑client groups: put noisy devices in a strict group, relax developer boxes, and give consoles their own policy.
- Local overrides: map
staging.example.com
to a private IP during testing without touching system hosts files. Flip it back instantly. - Regex blocking: block telemetry patterns you see in your logs. Keep regex lists short and measured.
- Anonymous mode when guests visit. Flip back later if you want full stats again.
Stop devices from bypassing Pi-hole
If your router/firewall allows it, add these outbound rules on LAN/VLANs:
-
Block or redirect all outbound DNS:
- Drop TCP/UDP 53 and 853 to the internet for all clients except the Pi-hole host
- Optionally block known DoH endpoints using IP/domain lists if your gear supports it
-
Allow the Pi-hole host to talk out on 53/853/443 for recursion or updates
On some platforms you can also DNAT 53 to your Pi-hole so hardcoded resolvers get rewritten to your local box.
Sensible privacy defaults
- In Pi-hole → Settings → Privacy, choose the level you’re comfortable with. I normally go with Anonymous mode once I’m done verifying.
- Set a strong admin password, don’t expose the dashboard to the internet, and if you want extra belt‑and‑braces, put the UI behind your reverse proxy with auth.
The Unbound config I ship everywhere
You saw the minimal config above. Here’s the full variant I keep in dotfiles, tuned for home networks:
# /etc/unbound/unbound.conf.d/pi-hole.conf
server:
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-ip6: yes
do-udp: yes
do-tcp: yes
# Security & privacy
harden-glue: yes
harden-dnssec-stripped: yes
qname-minimisation: yes
prefetch: yes
use-caps-for-id: no
edns-buffer-size: 1232
# Threads & buffers
num-threads: 1
so-rcvbuf: 1m
# Keep internal ranges private
private-address: 10.0.0.0/8
private-address: 172.16.0.0/12
private-address: 192.168.0.0/16
private-address: fd00::/8
private-address: fe80::/10
Restart Unbound and point Pi-hole at 127.0.0.1#5335
.
Picking a local domain that won’t fight you
- Don’t use
.local
. That suffix belongs to mDNS, and you’ll collide with Bonjour/Avahi devices. - Do use
home.arpa
or a delegated subdomain of a name you own, likelan.yourdomain.tld
.
If you must have pretty names for everything, add static A/AAAA records for the key boxes (NAS, controllers, printer, etc.). For everything else, conditional forwarding or Pi-hole DHCP works fine.
Troubleshooting playbook
- “Ignoring query from non-local network” in logs: set Interface listening to “listen on all interfaces, permit all origins” for multi‑subnet environments, or bind Pi-hole to the correct interface.
- Slow first lookups: that’s recursion. Warm the cache or keep Unbound running. DNSSEC adds a bit of latency; keep it if you care about validation.
- Port 53 conflicts: free it from systemd‑resolved. Keep Pi-hole on 53 and Unbound on 5335.
- IPv6 addresses keep changing: that’s privacy extensions. If you care about per‑client policy, stick to IPv4 for DNS or assign static v6 where possible.
- Docker can’t hear LAN: switch to host networking or macvlan so Pi-hole gets a real LAN IP.
Why every developer should run Pi-hole at home
- Repeatable environments: local DNS overrides for staging, feature previews, or blue/green tests without touching everyone’s hosts files.
- Telemetry sanity: see who’s talking to where and silence the vendors that insist on spamming your network.
- Fewer distractions: ad‑heavy docs and forums stop being painful. Small quality‑of‑life win that compounds.
- Debugging superpower: when your app fails DNS, you see it instantly. The Pi-hole query log beats guessing.
Optional add‑ons I actually use
- Metrics: expose Pi-hole stats to Netdata or Prometheus so you can graph queries over time and spot noisy clients.
- High‑availability: a second Pi-hole with Gravity Sync keeps lists and groups in sync. Point DHCP to both resolvers and sleep better.
A minimal firewall recipe (UFW example)
# Default stance
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow DNS and web UI from LAN only
sudo ufw allow from 192.168.1.0/24 to any port 53 proto udp
sudo ufw allow from 192.168.1.0/24 to any port 53 proto tcp
sudo ufw allow from 192.168.1.0/24 to any port 80 proto tcp
# Optional: allow SSH from LAN
sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp
sudo ufw enable
Block or redirect outbound DNS on your router for all clients except the Pi-hole host.
The Pi Zero + USB‑Ethernet build, step by step
- Flash Raspberry Pi OS Lite and preconfigure via Imager.
- Plug a USB‑OTG to Ethernet adapter into the Zero’s data micro‑USB port. Confirm link lights.
- SSH in, update packages, run the Pi-hole installer.
- Install Unbound, test it, point Pi-hole at
127.0.0.1#5335
. - In your router, set DHCP to advertise your Pi-hole’s IP as the only DNS server. If you’re brave, use Pi-hole’s DHCP and disable the router’s.
- Add a couple of local A records for core services. Enable conditional forwarding if you want to see DHCP hostnames from the router.
- Add one or two quality blocklists, create per‑client groups, and don’t go wild.
- Add firewall rules to prevent DNS bypass.
- Enjoy the silence.
Takeaways
Pi-hole changed the feel of my network more than any other service, and it cost about the price of a takeaway. Run it wired, pair it with Unbound, fix the handful of sharp edges, and it’ll quietly make everything nicer. For developers, it doubles as a local DNS control plane that saves time and prevents dumb mistakes.
If you implement one thing today, make it Pi-hole + Unbound on a wired box. Then take the 10 minutes to block outbound DNS on your firewall. You’ll never go back.
Quick reference
- Pi-hole admin:
http://pi.hole/admin
orhttp://<pi-ip>/admin
- Force adlist refresh:
pihole -g
- Tail live queries:
pihole -t
- Restart DNS:
pihole restartdns
- Export/import settings: Tools → Teleporter