My private internet: Pi-hole, Unbound & Tailscale
I’d been meaning to do this for a while. A browser extension handles ads on my laptop fine, but it does nothing for my phone, my smart TV, or the rest of the devices on my network that quietly phone home whenever they feel like it. DNS-level blocking solves all of that in one place — before any device even makes the connection.
The other thing worth mentioning: this isn’t just for your own devices. Once Pi-hole is the DNS server for your home network, every device in the house benefits — your partner’s phone, the kids’ tablets, the guest WiFi, all of it. Nobody has to install anything. It just works for everyone on the network, including people who’ve never heard of the setup and don’t need to.
The stack I landed on: Pi-hole to filter out ad and tracker domains, Unbound as a recursive resolver so queries don’t pass through a third-party DNS provider, and Tailscale to make the whole thing work on every device I own, wherever I am. Runs on a small Linux box behind the router, took an afternoon to set up, and I haven’t needed to touch it since.
The three components look similar on the surface but solve very different problems. It helps to know what each one actually does before you start, or the Tailscale configuration steps won’t make much sense.
Pi-hole sits at the front and acts as a filter. It doesn’t resolve anything itself — it checks every DNS query against its blocklists and kills the bad ones before they go anywhere. Ad domain? The request returns nothing and dies there.
Unbound does the actual DNS resolution, but recursively — starting from the root DNS servers and following the chain all the way to the answer. No Cloudflare, no Google, no third party in between.
Tailscale wraps the whole thing in a private WireGuard mesh and makes Pi-hole reachable from every device you own, no matter where you are. It then tells all your devices to use your Pi-hole as their DNS server — automatically, without configuring each one individually.
Three components, one job each, no overlap.
A Linux host. Any always-on machine works: a Raspberry Pi (any model with 512 MB RAM or more), a spare mini-PC sitting in a drawer, a VM on a home server, or a cheap VPS. DNS is not a heavy workload — even a single-core ARM chip handles a full household without breaking a sweat. I use a Raspberry Pi and it sits at maybe 3% CPU on a busy day.
Debian or Ubuntu. All commands below assume this. Raspberry Pi OS is Debian-based, so it fits perfectly.
A static LAN IP. Either set it on the device itself or create a DHCP reservation on your router. A DNS server with a changing address is quietly catastrophic — things will just randomly stop working and you’ll spend 20 minutes confused before you figure out why.
A Tailscale account. Free for personal use, up to 100 devices. No credit card required.
curl -sSL https://install.pi-hole.net | bashThe interactive installer handles most of the configuration. A few things to pay attention to:
eth0 or wlan0). You can revisit
this after Tailscale is installed.The installer will print a web interface password at the end. Note it down. If you miss it:
pihole setpasswordOnce installed, check that the admin panel loads at http://<your-host-ip>/
(Pi-hole v6 serves the panel at the root; /admin still redirects there).
If it does, Pi-hole is running.
This is the step most tutorials skip, and it’s the one I find most satisfying. With Unbound in place, your DNS queries don’t go anywhere externally at all. Unbound walks the DNS tree itself — root servers, TLD servers, authoritative servers — and hands the answer back to Pi-hole. Your ISP and every DNS provider out there are completely out of the picture.
The trade-off is that the very first query to a domain you’ve never visited can be slightly slower, since Unbound has to traverse the full chain from scratch. Subsequent queries are cached and fast. In practice I’ve never noticed the difference.
sudo apt install unbound -yRoot hints are a list of the DNS root servers. Unbound ships with built-in defaults, but keeping a fresh copy is good practice.
wget -O root.hints https://www.internic.net/domain/named.root
sudo mv root.hints /var/lib/unbound/While you’re at it, add a monthly cron job so it stays current:
sudo crontab -e
# add this line:
0 3 1 * * wget -qO /var/lib/unbound/root.hints https://www.internic.net/domain/named.root && systemctl restart unboundCreate the config file:
sudo nano /etc/unbound/unbound.conf.d/pi-hole.confPaste this in:
server:
verbosity: 0
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: no
prefer-ip6: no
root-hints: "/var/lib/unbound/root.hints"
# DNSSEC hardening
harden-glue: yes
harden-dnssec-stripped: yes
harden-large-queries: yes
use-caps-for-id: no # leave off — known to cause DNSSEC issues
edns-buffer-size: 1232
# Caching
cache-min-ttl: 300
cache-max-ttl: 86400
prefetch: yes
prefetch-key: yes
# Performance — single thread is plenty for home use
num-threads: 1
msg-cache-size: 50m
rrset-cache-size: 100m
so-reuseport: yes
so-rcvbuf: 4m
so-sndbuf: 4m
# Never leak private address ranges to public DNS
private-address: 192.168.0.0/16
private-address: 172.16.0.0/12
private-address: 10.0.0.0/8
private-address: 169.254.0.0/16If you’re on Bullseye or newer (which includes recent Raspberry Pi OS), the
system auto-installs openresolv with a configuration that quietly conflicts
with Unbound. Disable it before restarting:
sudo systemctl disable --now unbound-resolvconf.service
sudo systemctl restart unboundSkip this step and things will seem fine until they mysteriously aren’t.
# Should return a valid IP address
dig github.com @127.0.0.1 -p 5335
# DNSSEC check — should return SERVFAIL (bad signature correctly rejected)
dig sigfail.verteiltesysteme.net @127.0.0.1 -p 5335
# DNSSEC check — should return NOERROR with 'ad' in the flags
dig sigok.verteiltesysteme.net @127.0.0.1 -p 5335That ad flag in the last response stands for “Authentic Data” — DNSSEC
validation is working. If both DNSSEC checks pass, Unbound is doing exactly
what it should.
Now wire the two together. Open Pi-hole admin → Settings → DNS.
127.0.0.1#5335.Load a few websites from a device on your LAN. Pi-hole’s query log should start filling up. You’re now running a fully self-contained DNS stack.
Pi-hole on its own only works at home. Tailscale is what makes it global. It creates a private WireGuard mesh between all your devices, and once you tell it to use your Pi-hole as the DNS server, every device on that mesh gets ad-blocking everywhere — at the office, in a café, on mobile data in another country.
curl -fsSL https://tailscale.com/install.sh | sh
# --accept-dns=false is important here.
# This machine IS the DNS server — you don't want Tailscale
# overwriting its own DNS configuration.
sudo tailscale up --accept-dns=falseAuthenticate via the URL printed in the terminal. Your host should appear in
the Tailscale admin console.
Take note of the Tailscale IP assigned to it — a 100.x.x.x address.
You need that in the next step, not the LAN IP.
By default, Pi-hole only responds to DNS queries from interfaces it considers
“local”. Tailscale traffic arrives on tailscale0, which Pi-hole doesn’t
recognise as local — so it silently ignores those queries.
In Pi-hole admin → Settings → DNS, scroll down to the Interface settings section (Pi-hole v6 shows all settings by default — there is no Expert mode toggle), then set it to Permit all origins.
One security note: this is safe as long as port 53 on this machine isn’t exposed to the public internet. On a home device behind NAT, that’s true by default. If you’re running this on a VPS with a public IP, block port 53 for all traffic except the
tailscale0interface — otherwise you’ll accidentally run a public open resolver, which is a bad time for everyone.
Open the Tailscale admin console and go to DNS.
100.x.x.x) — not the LAN IP.Toggle Tailscale off and back on on one of your other devices to force a DNS refresh. Open Pi-hole’s query log — you should see queries rolling in from tailnet IPs. Try browsing something ad-heavy on your phone with WiFi off. Silence.
By default Tailscale only routes DNS through your home. Everything else goes directly to the internet from wherever you are. For most situations that’s ideal — you get the ad-blocking without any performance overhead.
If you’re on a sketchy hotel network and want all traffic routed home, you can advertise your Linux box as an exit node:
sudo tailscale up --advertise-exit-node --accept-dns=falseApprove it in the Tailscale admin console. On each client you can toggle exit node routing on or off per connection. Off by default, so you only pay the latency cost when you actually want it.
Worth being honest. The setup is genuinely great but it has real limits.
| Scenario | Blocked? |
|---|---|
| Third-party ad networks | âś… Yes |
| Tracker & telemetry domains (apps, smart TVs, IoT) | âś… Yes |
| YouTube ads | ❌ No — served from the same domains as the video |
| Ads on same-domain pages (Facebook, etc.) | ❌ No |
| Devices not on Tailscale, away from home | ❌ No |
| Everything, when the Pi-hole host is offline | ❌ DNS breaks |
YouTube is the one that still stings. Google serves ads and content from the same domains, so there’s no DNS trick that kills the ads without also killing the video. A browser extension is still the right answer there.
The offline scenario is worth planning for. If your host goes down, all
tailnet devices lose DNS resolution. Add a fallback in Tailscale’s global
nameservers — 1.1.1.1 as a secondary works fine — so devices gracefully
degrade to working internet with ads, rather than no internet at all.
Low-maintenance is an understatement. A few commands every month or so is genuinely all it takes:
# Update Pi-hole
pihole -up
# Refresh blocklists
pihole -g
# Update Unbound root hints (if you skipped the cron job)
wget -qO /var/lib/unbound/root.hints https://www.internic.net/domain/named.root
sudo systemctl restart unbound
# General system updates
sudo apt update && sudo apt upgrade -yFor blocklists beyond Pi-hole’s defaults, hagezi/dns-blocklists is the best I’ve found — actively maintained, multiple tiers from light to aggressive, and it actually documents what each list breaks so you can make an informed call.
| Component | Does what |
|---|---|
| Linux host (Pi, VM, VPS…) | Always-on platform |
| Pi-hole | DNS sinkhole — blocks ads and trackers |
| Unbound | Recursive resolver — no third party involved |
| Tailscale | WireGuard mesh — extends the whole stack everywhere |
An afternoon of setup. Months of running without touching it. No ads on my phone on the train, no overnight telemetry leaking off the TV, no DNS provider logging my queries.
The moment that really sold me on adding Unbound was opening Pi-hole’s query log for the first time and watching my smart TV make 340 DNS requests in a single evening — almost all of them to telemetry and ad endpoints. Every one hit a wall. No browser extension was ever going to catch that.
References