Local DNS Sinkhole on Raspberry Pi
A Raspberry Pi Zero 2 W running Pi-hole for local DNS filtering, backed by a small Python maintenance workflow for safer updates, health checks, and local reporting.
A home-network DNS sinkhole built on a Raspberry Pi Zero 2 W. Every device on my network resolves DNS through the Pi, which blocks ads, trackers, and known-bad domains before they ever load, and gives me a single place to see what my network is actually talking to.
Why I built this
I wanted network-wide ad and tracker blocking without installing anything on individual devices. A DNS sinkhole does that at the source: point the network’s DNS at the Pi, and the filtering applies to phones, laptops, and smart-home gear automatically.
But the real motivation was the part that comes after the install. A Pi-hole is only useful if it stays healthy, and a Raspberry Pi Zero 2 W is a small machine that needs occasional care: OS updates, Pi-hole updates, package cleanup. I didn’t want to rediscover those steps from memory every couple of weeks, and I didn’t want to blindly automate something that touches DNS for my whole house. So the project is really two things: the sinkhole itself, and a small, transparent maintenance workflow I can trust and actually understand.
What it is
- Hardware: Raspberry Pi Zero 2 W, running Raspberry Pi OS (Debian-based).
- Software: Pi-hole installed natively on the OS (not in Docker), acting as the DNS server for the whole network.
- Maintenance tooling: a small Python script I run from my Mac that connects to the Pi over SSH and walks through maintenance in reviewable phases, appending the results to a single local report file.
The design goal throughout was simple, local, and inspectable: no cron jobs, no remote orchestration, nothing that runs unattended against my DNS without me watching.
Accessing the dashboard
Pi-hole ships with an admin web interface. On my network I reach it at the Pi’s local address followed by /admin and log in with the admin password. From there the useful views are:
- Query log: what every device is resolving in real time, which is genuinely eye-opening the first time you see how chatty modern apps are.
- Dashboard: total queries, percentage blocked, and the top blocked domains.
- Blocking toggle: temporarily disable filtering when something legitimate gets caught.
- Teleporter: export a full backup of the configuration. This one matters: I always export a Teleporter backup before updating Pi-hole, so a bad upgrade is a quick restore instead of a rebuild.
The maintenance routine
I run maintenance roughly every two weeks, and after any major change or troubleshooting session. Instead of one big automated run, the workflow is broken into phases I step through deliberately:
# 1. Pre-check: confirm the Pi and Pi-hole are healthy before touching anything
python3 pi-hole.py --phase precheck
# 2. Export a Teleporter backup from the web UI (before any update)
# 3. Reviewed package updates over SSH, read each summary before confirming
sudo apt update
sudo apt full-upgrade
sudo apt autoremove
sudo apt autoclean
# 4. Update Pi-hole itself (Core, Web, FTL)
pihole -up
# 5. Post-check: confirm everything still resolves and no services failed
python3 pi-hole.py --phase postcheck
A few decisions in the script are there on purpose:
- No stored secrets. Passwords are entered at runtime with Python’s
getpass, so nothing is hardcoded or echoed to the terminal. - SSH host key verification. The script checks the Pi’s host key so I’m not silently connecting to the wrong machine.
- Updates are never blind. It deliberately does not run
pihole -uporapt full-upgrade -yautomatically. Pi-hole gets backed up first, and I review each upgrade summary before pressingY. - One running report. Every run appends command output, status, and errors to a single report file, so I have a maintenance journal to check when something breaks later.
What I keep an eye on
The post-check exists to answer one question: is the network still healthy? The things I actually watch for:
pihole statusshows Pi-hole active and blocking enabled.dig google.com @127.0.0.1returns a valid answer on the Pi, and resolution works from a client on the network.systemctl --failedshows no failed services.pihole -g(gravity / blocklist update) completes cleanly.- Kernel and firmware upgrades can look frozen on a Pi Zero 2 W running off an SD card. That’s normal. I wait it out rather than interrupting a package install mid-write.
Not every warning is a fire. A non-blocking message I’ve seen after a Pi-hole update is local: FTL_PID_FILE: readonly variable. If status is healthy and DNS resolves, I log it and move on instead of panicking.
Pros and cons
What works well
- Network-wide blocking with zero config on individual devices.
- Real visibility into what my network is doing via the query log.
- A maintenance process that’s repeatable, reviewable, and easy to fix because I understand every step.
- Backups before updates mean low-risk upgrades.
The trade-offs
- It’s a single point of DNS for the network. If the Pi goes down, resolution needs a fallback.
- A Pi Zero 2 W is modest hardware; large kernel upgrades are slow.
- Maintenance is still manual by design. That’s a deliberate choice for trust and transparency, but it does mean I have to actually run it.
Where it goes next
The workflow is intentionally not “finished.” Things I may add when there’s a real reason: SSH keys instead of password auth, a single prompt when the SSH and sudo passwords match, clearer flagging of failed steps, and report rotation once the log grows large. The guiding rule is the one I wrote at the top of the repo for future me: keep Pi-hole healthy, keep the process simple, and don’t overcomplicate a small home task just because a fancier option exists.