You’ve seen it before. You spin up Pi-hole in Docker, you feel like a hacker, and then you check the dashboard. Total defeat. Every single DNS query is coming from one IP address: 172.17.0.1.
Your beautiful per-client logging? Gone. Your ability to block TikTok on the kids’ iPads while keeping it open for yourself? Broken. This is the “Bridge Mode” trap, and 90% of tutorials ignore it.
We aren’t just going to install software today. We are going to deploy a sovereign DNS infrastructure that actually works. We will fix the Client IP visibility issue using “Host Mode,” and we will dodge the “Fake Recursion” bullet that plagues most Unbound guides.
🛑 CRITICAL NETWORK INFRASTRUCTURE WARNING:
- High Risk Modification: This guide disables
systemd-resolvedon the Host OS. If you make a mistake, you will lose DNS resolution and internet connectivity on your server. Ensure you have physical access (keyboard/monitor) or a static IP before proceeding. - Order of Operations: You MUST follow the blue “Pre-Flight Check” box below. If you skip it, Docker cannot download the required images.
- Port 80 Conflict: This setup uses Host Networking. If your server already runs a webserver (Apache/Nginx/Caddy) on Port 80, Pi-hole will fail to start. You must disable existing webservers first.
- No Liability: The author is not responsible for network outages or data loss. Do not run this on a production server without backups.
The “Privacy Paradox”: What ISPs Are Actually Doing
What is the point of Unbound?
Most people think installing Unbound makes them invisible to their ISP. I need to be brutally honest: it doesn’t.
Even with your own recursive DNS, your ISP can still see the SNI (Server Name Indication) in your HTTPS packets. They know you are visiting netflix.com; they just don’t know which specific thumbnail you clicked. So why bother?
Data Sovereignty.
When you use Google DNS (8.8.8.8) or Cloudflare (1.1.1.1), you are handing your browsing history to a data broker on top of your ISP. You are feeding two beasts instead of one. By running Unbound, you cut out the middleman. You talk directly to the root servers.
The stakes are higher than just “hiding ads.” A staff report by the FTC confirmed that major US ISPs don’t just route traffic—they collect real-time location data and categorize users into sensitive demographic groups (like race and sexual orientation) for advertising purposes. They are vertically integrated data brokers.
This reality aligns with Pew Research findings showing that 79% of Americans feel they have zero control over how companies use their data. Running Unbound is one of the few technical levers you can pull to take a percentage of that control back.
The “Fake Recursion” Trap: Mvance vs. Klutchell
Here is where technical literacy matters. If you search for “Pi-hole Unbound Docker,” the top result is almost always the mvance/unbound image. It’s a great project, but it has a default setting that ruins your privacy goals.
The Trap: By default, many configurations of the mvance image are set to Forward queries to Cloudflare via DNS-over-TLS. You aren’t doing recursion; you are just using a fancy proxy to Cloudflare.
I recommend and use the klutchell/unbound image for this deployment. It is built from the ground up for multi-architecture support (works on your Raspberry Pi 4 or 5 without crashing) and, more importantly, it behaves like a true recursive resolver out of the box.
| Feature | mvance/unbound (Default) | klutchell/unbound (Recommended) |
|---|---|---|
| Resolution Method | Forwarding (DoT to Cloudflare) | True Recursion (Root Servers) |
| Base Image | Alpine Linux | Distroless (Google) |
| Privacy Default | Trusts Cloudflare | Trusts No One |
The “Client IP” Black Hole (And How to Fix It)
The User Intent: You want to see which device is querying what.
In standard Docker “Bridge Mode,” Docker creates a virtual router (NAT) between your container and your home network. When your iPhone sends a DNS request, it hits the Docker host, which rewrites the packet source IP to the Docker Gateway IP (usually 172.17.0.1) and passes it to Pi-hole.
Pi-hole sees the request coming from the Gateway, not the iPhone. Result? Useless logs.

The Fix: network_mode: host
We are going to bypass the Docker network stack entirely. By using Host Mode, the Pi-hole container shares the raw network interface of your Raspberry Pi (or Linux server). It sees packets exactly as they arrive on the wire. No NAT. No masquerading. 100% visibility.
Warning for macOS/Windows Users: Host networking works differently on Docker Desktop due to the VM layer. This guide assumes you are running on Linux (Raspberry Pi OS, Ubuntu, Debian), which is the standard for 24/7 home servers.
⚡ PRE-FLIGHT CHECK (MANDATORY):
In the next step, we are going to disable the system’s DNS resolver to free up Port 53. Once we do that, your server will lose the ability to download files until Pi-hole is fully active.
You MUST download the Docker images NOW while you still have internet access:
docker pull pihole/pihole:latest && docker pull klutchell/unbound:latest
If you skip this, your deployment will fail with a “Temporary Failure in Name Resolution” error later.
Prerequisite: Taming systemd-resolved
Before we touch Docker, we have a conflict to resolve. Modern Linux distros (like Ubuntu 20.04+) love to run their own DNS stub resolver on Port 53. If we try to start Pi-hole in Host Mode, it will crash because Port 53 is already taken.
Here is how you disable the system stub resolver to free up the port for Pi-hole:
# 1. Disable the systemd-resolved stub listener
sudo sed -r -i.orig 's/#?DNSStubListener=yes/DNSStubListener=no/g' /etc/systemd/resolved.conf
# 2. Change the symlink for /etc/resolv.conf to stop using the stub
sudo sh -c 'rm /etc/resolv.conf && ln -s /run/systemd/resolve/resolv.conf /etc/resolv.conf'
# 3. Restart systemd-resolved
sudo systemctl restart systemd-resolved
Verification: Run sudo lsof -i :53. It should return empty (or at least not show systemd-resolved listening on 0.0.0.0).
The Ultimate Setup: Pi-hole & Unbound Docker Compose
We are using a “Port Shift” strategy. Pi-hole will own the standard Port 53. Unbound will hide on Port 5335, accessible only to the local machine. This keeps your recursive resolver secure from the outside world while giving Pi-hole a direct line to the root servers.
Create a file named docker-compose.yml and paste this in:
version: "3"
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
# CRITICAL: Host mode allows Pi-hole to see real Client IPs
network_mode: host
environment:
- TZ=America/New_York
- WEBPASSWORD=securepassword # Change this!
# Point Pi-hole to our local Unbound instance on port 5335
- PIHOLE_DNS_1=127.0.0.1#5335
# Since we are in host mode, we must ensure WEB_PORT doesn't conflict
- WEB_PORT=80
volumes:
- ./etc-pihole/:/etc/pihole/
- ./etc-dnsmasq.d/:/etc/dnsmasq.d/
cap_add:
- NET_ADMIN # Required for DHCP and network manipulation
restart: unless-stopped
unbound:
container_name: unbound
image: klutchell/unbound:latest
network_mode: host
environment:
- TZ=America/New_York
volumes:
# We bind mount the config so we can edit it easily
- ./unbound/unbound.conf:/opt/unbound/etc/unbound/unbound.conf
restart: unless-stopped
Configuring Unbound for “Paranoid” Privacy
The default config is okay, but we want hardened privacy. We need to create the unbound.conf file referenced in the Docker Compose above.
Create a folder structure: mkdir -p unbound and then create unbound/unbound.conf.
Key Privacy Feature: QNAME Minimisation
Normally, if you visit secret-project.dev.example.com, your DNS resolver asks the Root Server: “Where is secret-project.dev.example.com?” This leaks your specific activity to the Root. With QNAME Minimisation enabled, Unbound is smarter. It asks the Root: “Where is .com?” Then asks the .com server: “Where is example.com?” It only reveals the minimum necessary info to each upstream server.
server:
# Basic Security
verbosity: 0
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
# IP Access Control
# Only allow localhost (Pi-hole) to talk to Unbound
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.0/8 allow
# Privacy Hardening
qname-minimisation: yes
harden-glue: yes
harden-dnssec-stripped: yes
# Performance Tuning (Cache)
prefetch: yes
num-threads: 1
msg-cache-size: 50m
rrset-cache-size: 100m
so-rcvbuf: 1m
Quick Start: Run docker compose up -d. Your stack is now live.
Verification: Trust But Verify
Don’t just assume it works. I’ve seen too many “secure” setups that were actually just leaking everything to Google because of a router setting. Here is how to audit your work.
1. The Recursion Test
We need to prove Unbound is validating DNSSEC signatures. Run this from your host terminal:
dig fail01.dnssec.works @127.0.0.1 -p 5335
Expected Result: SERVFAIL. This is good! It means Unbound detected a bad signature and blocked it.
dig sigok.verteiltesysteme.net @127.0.0.1 -p 5335
Expected Result: NOERROR with an IP address. This means validation is working.
2. The Leak Test
Go to a site like dnsleaktest.com. Run the “Extended Test.”
If you see “Cloudflare” or “Google” in the results, you failed. You should see your own ISP’s IP address (or your VPN IP if you are routing the container through a VPN). Seeing your own IP proves you are the one doing the recursion. You are the resolver now.
Maintenance: The “Set and Forget” Myth
Real talk: Docker containers do not update themselves. An outdated DNS resolver is a security liability.
Since we used Bind Mounts (the ./unbound/unbound.conf path) instead of Docker Volumes, your configuration is safe on your host disk. You can update your containers instantly without losing data:
docker compose pull
docker compose up -d
Run this once a month. It takes 10 seconds. Don’t overcomplicate it with auto-updaters like Watchtower unless you really trust their stability; I prefer to pull the trigger myself so I’m there to fix it if it breaks.
Final Thoughts
You now have a DNS stack that rivals enterprise deployments. You’ve solved the Client IP visibility issue that plagues standard Docker users, and you’ve implemented true recursion to stop feeding data to Big Tech.
This setup gives you visibility and sovereignty. In a world where 79% of people feel out of control of their data, that is a massive win.
