Blackhole: Network-Wide Ad Blocking With No Public Address
June 14, 2026 · 7 min read
Every device you own is having a conversation you never see. Before a page finishes loading, your phone, laptop, and TV quietly resolve dozens of domain names, and a surprising share of them belong to ad networks and trackers rather than the site you actually asked for. You can block a lot of that in a browser, but a browser extension only covers that browser, on that device. Open a different app or pick up a different gadget and the trackers are back.
The classic fix is Pi-hole: a small DNS server that sits on your network and refuses to resolve known ad and tracker domains, so blocking happens for every device at once. It works beautifully at home. The trouble starts when you leave. Your laptop on café Wi-Fi can't reach the Pi-hole sitting in your living room, and the obvious workaround — exposing that DNS server to the public internet so it's reachable from anywhere — is a genuinely bad idea. An open DNS resolver on the public internet gets discovered within minutes and abused as an amplifier for attacks.
Blackhole is my answer to that tension. It runs network-wide Pi-hole blocking that follows you to any device, anywhere, without ever placing a DNS server on the public internet. The trick is that the blocker has no public address at all. The only door in is a private VPN.
The name has two meanings, and both are the point. Pi-hole works by sinkholing blocked domains: it answers them with 0.0.0.0, an address that goes nowhere, so the tracker request falls in and nothing comes back out. And the server itself is a black hole from the outside — it emits nothing to the public internet and can't be seen or probed from it. Light goes in; nothing escapes.
The whole idea fits in two words
Blackhole is two Docker containers. One runs Tailscale, a VPN built on WireGuard that joins your devices into a small private network — a "tailnet" — where they can reach each other directly no matter what physical network they happen to be on. The other runs Pi-hole, the DNS ad blocker. What makes the design click is a single line in the compose file:
network_mode: service:tailscale
That tells Docker not to give Pi-hole its own network identity. Instead, Pi-hole runs inside the Tailscale container's network namespace — it shares the same network stack, the same interfaces, the same addresses. From the network's point of view, the two containers are a single machine: Tailscale owns the only way in or out, and Pi-hole is simply listening on the inside of it.
That one decision is the security model. Pi-hole never binds to a port the public internet can reach, because it has no public-facing interface to bind to. The only network it lives on is the encrypted tailnet. There's no firewall rule to get wrong and no exposed port to forget about, because nothing is publicly listening in the first place.
So a lookup from your phone in an airport travels over the encrypted WireGuard tunnel into the Tailscale container, lands on Pi-hole listening inside the shared namespace, and gets answered — blocked or resolved — without a single packet of that exchange crossing the open internet. The public internet's entire view of the arrangement is that dotted line: nothing to connect to.
What happens to a single query
Once a query is inside, Pi-hole runs it through a short decision process. For a beginner it's two questions: is this domain junk, and have I seen it recently? For anyone who runs DNS for a living, those two questions are exactly where latency and trust are won or lost.
First, Pi-hole checks the domain against its blocklists. If it's a known ad or tracker host, Pi-hole answers immediately with 0.0.0.0 — the sinkhole — and the request dies there. If the domain is legitimate, Pi-hole checks its cache: a 25,000-entry store configured with optimistic caching, so popular answers are served instantly and refreshed in the background instead of on the critical path. Only a genuine miss goes upstream, where Blackhole forwards to Quad9 and validates the reply with DNSSEC, so the answer can't be silently forged in transit.
The beginner takeaway is simple: bad domains hit a dead end, good domains get a fast answer. The expert version is that the fast answer is fast on purpose — the cache and optimistic refresh keep tail latency flat, and the upstream path is the only one that ever touches the wider internet.
The settings that make it boring in production
The interesting architectural idea is the shared namespace. Everything else in the compose file is the unglamorous tuning that turns a clever demo into something you can leave running and forget about:
FTLCONF_dns_cache_size: 25000withcacheOptimistic: true— a large cache that serves stale-but-fresh answers while refreshing them, keeping latency flat under load.FTLCONF_dns_rateLimit: 100/60andmaxConcurrentQueries: 50— a misbehaving or compromised device can't turn your resolver into a flood.FTLCONF_dns_queryLogging: truewithmaxDBdays: 7— a week of query history for visibility and debugging, then automatic cleanup so logs don't grow without bound.cap_add: NET_ADMINon Tailscale only,no-new-privileges: trueon both containers, plus per-container CPU, memory, and PID limits — each container gets exactly the privileges it needs and nothing more.- Healthchecks on both: Tailscale verifies
tailscale status, Pi-hole runs a realdigagainst itself, andrestart: unless-stoppedbrings anything that fails back automatically.
None of this is exotic, and that's the point. The clever part is the shape of the system; the rest is just making it dependable.
Standing it up
Clone the repo, copy the environment template, fill in three values, and start it:
git clone https://github.com/boxpositron/blackhole
cd blackhole
cp .env.example .env
# set TS_AUTHKEY, PIHOLE_PASSWORD, and TZ
docker compose up -d
The one detail worth getting right is the Tailscale auth key. Blackhole expects a reusable key tagged tag:dns, which keeps the node's identity stable across restarts and scopes its permissions to exactly what a DNS appliance should have. Once the containers are up, ask Tailscale which address it claimed:
docker exec tailscale-pihole tailscale ip -4
Point your devices — or your whole tailnet, via the Tailscale admin console — at that address for DNS, and you can confirm both halves of the behavior straight from the command line:
dig @<tailscale-ip> doubleclick.net # blocked -> 0.0.0.0
dig @<tailscale-ip> example.com # resolved -> real answer
The admin dashboard lives at http://<tailscale-ip>/admin, reachable only from inside the tailnet. And if you'd rather not touch Docker commands at all, the repo deploys cleanly on Coolify: point it at the repository, set the same environment variables in the UI, and it handles volumes, health, and updates for you.
Why I built it this way
Most self-hosting security advice is a checklist of things to configure correctly: close this port, add that firewall rule, restrict this interface. Blackhole tries to make most of that checklist unnecessary by changing the shape of the system instead. When the ad blocker has no public address, "is the DNS server exposed?" stops being a question you have to keep answering. The architecture answers it for you.
That's the part I find satisfying. It's a small project — two containers and a careful compose file — but it converts a property you'd normally defend with configuration into something that's simply true by construction. Network-wide blocking that travels with you, and a resolver the public internet can't even see.
The source is on GitHub.
Enjoyed this? Let me know