There’s a quiet kind of joy in watching a webpage load without a single ad. No autoplaying videos. No tracking pixels firing off requests to seventeen different data brokers. No “Subscribe to our newsletter!” popups blocking 40% of your screen.
That’s what Pi-hole gives you — not just for one browser, but for every device on your network. Your phone, your smart TV, your roommate’s tablet, that IoT toaster that definitely doesn’t need to phone home to a server in Virginia. Pi-hole blocks it all at the DNS level, before the junk even reaches your devices.
And the best part? You can set it up in about 15 minutes with Docker. No dedicated hardware needed. No soldering. Just a machine that’s already running, a few commands, and the smug satisfaction of a cleaner internet.
What We’re Building
By the end of this tutorial, you’ll have a Pi-hole instance running in Docker that:
- Blocks ads, trackers, and malware domains for every device on your network
- Provides a web dashboard so you can see exactly what’s being blocked
- Uses Unbound as a recursive DNS resolver for extra privacy
- Survives reboots and restarts automatically
Prerequisites
Before we start, you’ll need:
- A Linux server, Raspberry Pi, or any machine running Docker (I’m using a modest €5/month VPS)
- Docker and Docker Compose installed
- Access to your router’s admin panel (we’ll change the DNS settings there)
- About 15 minutes of uninterrupted time
Quick Docker check: Run docker --version and docker compose version in your terminal. If both return version numbers, you’re good to go. If not, install Docker first — that’s a tutorial for another day.
Step 1: Create the Project Directory
Let’s keep things tidy. Create a directory for Pi-hole:
mkdir -p ~/docker/pihole
cd ~/docker/pihole
I keep all my Docker projects under ~/docker/ — it makes backups and cleanup straightforward. But use whatever convention works for you.
Step 2: Write the Docker Compose File
Create a file called docker-compose.yml:
cat > docker-compose.yml << 'EOF'
version: "3.8"
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
ports:
- "53:53/tcp"
- "53:53/udp"
- "80:80/tcp"
environment:
TZ: 'America/New_York'
WEBPASSWORD: 'your-secure-password-here'
FTLCONF_LOCAL_IPV4: '192.168.1.100'
volumes:
- './etc-pihole:/etc/pihole'
- './etc-dnsmasq.d:/etc/dnsmasq.d'
cap_add:
- NET_ADMIN
restart: unless-stopped
EOF
Let's break down what each part does:
- Ports 53 (TCP/UDP): This is the DNS port. Devices will send DNS queries here.
- Port 80: The Pi-hole admin dashboard. We'll use this to manage and monitor our setup.
- TZ: Set your timezone so logs and query graphs show correct times.
- WEBPASSWORD: Change this. Seriously. "admin" is not a password.
- FTLCONF_LOCAL_IPV4: Set this to the IP address of the machine running Docker. This helps Pi-hole display the correct client info.
- cap_add: NET_ADMIN: Pi-hole needs this to manage DNS and network settings.
- restart: unless-stopped: Pi-hole starts automatically when Docker starts (i.e., on boot).
Step 3: Start Pi-hole
With the compose file ready, fire it up:
docker compose up -d
The -d flag runs it in detached mode (in the background). You should see Docker pulling the image and starting the container. Give it about 30 seconds to fully initialize.
Check that it's running:
docker compose ps
You should see the pihole container with state Up.
Step 4: Verify It's Working
Before we reconfigure our whole network, let's test Pi-hole directly. Use dig or nslookup to query a known ad domain:
dig doubleclick.net @127.0.0.1
You should see the response return 0.0.0.0 — that's Pi-hole saying "nope, not resolving that." Now try a legitimate domain:
dig example.com @127.0.0.1
This should return the real IP address. If both work, Pi-hole is functioning correctly.
Step 5: Access the Admin Dashboard
Open your browser and navigate to:
http://your-server-ip/admin
Log in with the password you set in WEBPASSWORD. You'll be greeted by a dashboard showing queries, blocked domains, and a pie chart that's oddly satisfying to watch.
Take a moment to explore. The "Query Log" shows every DNS request in real time. The "Top Permitted Domains" and "Top Blocked Domains" lists are fascinating — you'll be surprised how many background requests your devices make.
Step 6: Point Your Router at Pi-hole
This is the step that makes Pi-hole work for your entire network. Log in to your router's admin panel (usually 192.168.1.1 or 192.168.0.1) and find the DNS settings.
Change the primary DNS server to the IP address of the machine running Pi-hole. Leave the secondary blank or set it to something like 1.1.1.1 as a fallback (though note: if Pi-hole goes down, devices will bypass it).
Pro tip: If your router allows it, set Pi-hole as the only DNS server and disable DHCP on the router, letting Pi-hole handle DHCP instead. This ensures all DNS queries go through Pi-hole — no device can sneak around it.
After saving the router settings, you may need to renew your devices' DHCP leases:
# On Linux
sudo dhclient -r && sudo dhclient
# On Windows
ipconfig /release && ipconfig /renew
# On macOS
sudo ipconfig set en0 DHCP
Or just restart your devices. They'll pick up the new DNS settings automatically.
Step 7: Customize Your Blocklists
Pi-hole comes with a default blocklist that's quite good, but you can add more. In the admin dashboard, go to Group Management → Adlists and add lists like:
- OISD Full: Comprehensive list that blocks ads, tracking, and malware while minimizing false positives
- Steven Black's List: Combines multiple sources for broad coverage
- Perflyst's Smart TV Blocklist: Stops smart TVs from phoning home (highly recommended)
After adding lists, update Pi-hole's gravity database:
docker exec pihole pi-hole -g
This downloads and processes the updated blocklists. It might take a minute or two.
Bonus: Adding Unbound as a Recursive DNS Resolver
By default, Pi-hole forwards DNS queries to upstream providers (like Cloudflare or Google DNS). But you can go fully self-sufficient with Unbound, a recursive DNS resolver that queries the root servers directly.
This means no third party ever sees your DNS queries. Not Cloudflare, not Google, not your ISP. Just you and the DNS root servers.
Add this to your docker-compose.yml:
unbound:
container_name: unbound
image: mvance/unbound:latest
volumes:
- './unbound:/opt/unbound/etc/unbound'
ports:
- "5335:53/tcp"
- "5335:53/udp"
restart: unless-stopped
Then update Pi-hole's upstream DNS in the admin dashboard (Settings → DNS) to point to 127.0.0.1#5335.
This is optional but rewarding. It's the DNS equivalent of growing your own vegetables — more work, but you know exactly what you're getting.
Common Pitfalls and Troubleshooting
"Port 53 is already in use"
Some Linux distributions run a local DNS resolver (systemd-resolved) that occupies port 53. Fix this by disabling it:
sudo systemctl disable --now systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf
Docker can't bind to port 53
Make sure you're running the container with cap_add: NET_ADMIN and that no other service is using port 53. Check with:
sudo ss -tlnp | grep 53
Dashboard loads but ads aren't blocked
Your device might be using a hardcoded DNS (like Chrome's "Use Secure DNS" or Firefox's DNS-over-HTTPS). Disable those features in your browser settings, or Pi-hole won't see those queries.
Pi-hole blocks something I need
Check the query log to find the blocked domain, then click "Whitelist" to allow it. Sometimes legitimate services share domains with trackers — it's a balancing act.
Keeping Pi-hole Healthy
Pi-hole is famously low-maintenance, but a few habits keep it running smoothly:
- Update monthly:
docker compose pull && docker compose up -d - Check the dashboard weekly: Glance at the query log for anything suspicious
- Back up your lists: Your whitelist, blacklist, and custom settings live in the
etc-piholevolume — include it in your regular backups - Monitor disk usage: The query log can grow large over time. Consider enabling "Privacy Level 2" in settings if you don't need long-term query history
What You've Gained
With this setup, you now have:
- A network-wide ad blocker that protects every device — even ones that don't support browser extensions
- A DNS-level firewall that blocks malware domains before they load
- Complete visibility into what your devices are doing on the network
- A small but meaningful step toward digital self-reliance
There's something deeply satisfying about taking control of your network's DNS. It's not flashy. Nobody will see it. But every webpage will load a little faster, a little cleaner, and a little more privately.
And when you check that dashboard and see "42.7% of queries blocked" — well, that's a good day.
Happy blocking!
Leave a Reply