Setting Up Pi-hole in Docker for Network-Wide Ad Blocking and DNS Management
We earn commissions when you shop through the links on this page, at no additional cost to you. Learn more.
Pi-hole is one of those tools that, once you run it, you genuinely cannot imagine going back. It acts as a network-level DNS sinkhole — every device on your network benefits from ad blocking without installing a single browser extension. Running it in Docker is my preferred approach because it keeps the installation clean, portable, and easy to update. This tutorial walks you through a complete Docker Compose setup, covers the one gotcha that trips up almost everyone (port 53 on modern Linux), and shows you how to make Pi-hole genuinely useful as a local DNS manager, not just an ad blocker.
Why Docker Instead of a Dedicated Pi-hole Install?
The traditional Pi-hole install script works fine, but it installs directly onto the host and can conflict with other services. When I first set up Pi-hole years ago I ran it bare-metal on a Raspberry Pi — and the moment I wanted to run anything else on that Pi, I ran into dependency conflicts. Moving to Docker means Pi-hole lives in its own isolated container, I can update it with a single docker compose pull, and if something breaks I can nuke the container without touching the host OS.
You can run this on a Raspberry Pi 4, an old mini-PC, a cheap VPS, or any Linux host with Docker installed. I personally run mine on a small Debian VM on my Proxmox server with 512 MB of RAM allocated — Pi-hole is remarkably light.
The Port 53 Problem You Need to Know About First
On Ubuntu 20.04 and newer, and on most modern Debian-based systems, systemd-resolved is running and listening on port 53. If you try to map Pi-hole's DNS port without addressing this first, the container will fail to start. I've seen this catch people out more than any other step.
You have two options: disable systemd-resolved's stub listener, or use host network mode for the container. I prefer disabling the stub listener because it keeps things cleaner. Here's how:
# Edit the resolved configuration
sudo nano /etc/systemd/resolved.conf
# Add or modify these lines under [Resolve]:
# DNSStubListener=no
# Restart the service
sudo systemctl restart systemd-resolved
# Verify port 53 is now free
sudo ss -tulnp | grep ':53'
After this, your host will still resolve DNS via systemd-resolved — it just won't hold port 53 hostage. The symlink at /etc/resolv.conf may need updating if you see DNS failures on the host itself; point it at /run/systemd/resolve/resolv.conf instead of the stub.
/etc/resolv.conf (like nameserver 1.1.1.1) before you restart systemd-resolved.The Docker Compose File
Create a directory for Pi-hole and drop in a docker-compose.yml:
mkdir -p ~/pihole && cd ~/pihole
Now create the Compose file. I use named volumes for persistence so Pi-hole survives container recreation:
cat > docker-compose.yml << 'EOF'
services:
pihole:
image: pihole/pihole:latest
container_name: pihole
hostname: pihole
ports:
- "53:53/tcp"
- "53:53/udp"
- "8080:80/tcp"
environment:
TZ: "Europe/London"
WEBPASSWORD: "changeme_set_a_strong_password"
PIHOLE_DNS_: "1.1.1.1;1.0.0.1"
DNSSEC: "true"
DNSMASQ_LISTENING: "all"
VIRTUAL_HOST: "pihole.lan"
WEBTHEME: "default-dark"
volumes:
- pihole_data:/etc/pihole
- dnsmasq_data:/etc/dnsmasq.d
restart: unless-stopped
cap_add:
- NET_ADMIN
networks:
- pihole_net
networks:
pihole_net:
driver: bridge
volumes:
pihole_data:
dnsmasq_data:
EOF
A few things worth calling out in this config: PIHOLE_DNS_ sets upstream resolvers — I use Cloudflare here but you can substitute 8.8.8.8;8.8.4.4 for Google or 9.9.9.9;149.112.112.112 for Quad9. The cap_add: NET_ADMIN is required for Pi-hole's DHCP functionality, though I rarely use Pi-hole for DHCP. The DNSMASQ_LISTENING: "all" setting tells dnsmasq to answer queries on all interfaces, which matters when you're running inside a Docker bridge network.
Set a real password before you deploy — do not leave it as "changeme". Then bring it up:
docker compose up -d
# Watch the logs to confirm it started cleanly
docker compose logs -f pihole
After about 30 seconds you should see Pi-hole's web interface at http://your-host-ip:8080/admin. Log in with the password you set in WEBPASSWORD.
Pointing Your Router at Pi-hole
The real power of Pi-hole comes when your entire network uses it as the DNS server. The cleanest way to do this is at the router level: log into your router's admin panel, find the DHCP settings, and change the DNS server it hands out to DHCP clients to your Pi-hole host's IP address. Every device that renews its DHCP lease will then use Pi-hole automatically — no configuration needed on phones, smart TVs, or anything else.
If you can't change your router's DNS settings (some ISP-provided routers lock this down), you can instead configure Pi-hole as the DNS server on individual devices, or put Pi-hole into DHCP server mode. I'd recommend the router approach whenever possible.
Adding Custom Local DNS Entries
This is where Pi-hole moves from "useful" to "essential" for homelab users. You can give your local services proper hostnames instead of remembering IP addresses and port numbers. In the Pi-hole web UI, go to Local DNS → DNS Records and add entries like:
jellyfin.lan→ your Jellyfin server IPnextcloud.lan→ your Nextcloud server IPportainer.lan→ your Portainer instance IPpihole.lan→ Pi-hole's own host IP
You can also do this via config file if you want to keep it in version control. Create a file in the dnsmasq volume:
# Get the volume's mount path
docker inspect pihole | grep -A 2 dnsmasq_data
# Or exec into the container directly
docker exec -it pihole bash
# Inside the container, create a custom hosts file
cat > /etc/dnsmasq.d/02-custom-dns.conf << 'EOF'
address=/jellyfin.lan/192.168.1.50
address=/nextcloud.lan/192.168.1.51
address=/portainer.lan/192.168.1.52
address=/pihole.lan/192.168.1.10
EOF
# Restart dnsmasq inside the container
pihole restartdns
Adding Blocklists
Pi-hole ships with a default blocklist, but I always add a few extras. In the web UI go to Adlists and add URLs from the Firebog ticked lists — these are curated and unlikely to cause false positives. After adding lists, run Tools → Update Gravity to pull them in. My setup blocks around 1.2 million domains with three carefully chosen lists, which hits 98% of ads without breaking legitimate services.
Keeping Pi-hole Updated
With Docker, updates are a one-liner. I combine this with a quick backup of the config volume first:
cd ~/pihole
# Pull the latest image
docker compose pull
# Recreate the container with the new image
docker compose up -d --force-recreate
# Verify it's running on the new image
docker compose images
If you want to automate this, Watchtower can handle it — but for Pi-hole specifically, I prefer manual updates because breaking DNS at an unexpected time is not fun. I schedule a monthly maintenance window and update then.
Running Pi-hole Alongside a Reverse Proxy
You might want Pi-hole's web UI behind a reverse proxy like Caddy or Nginx Proxy Manager rather than exposed on port 8080. I use Caddy personally. The key thing to remember: Pi-hole's admin interface is at /admin, not the root path. Your Caddy config should proxy to http://pihole:8080 (using the container name if they share a Docker network) and Pi-hole will handle the path routing internally.
If you're looking for a VPS to host Pi-hole alongside your other services — particularly if you want a cloud-based DNS resolver you can reach over WireGuard — DigitalOcean Droplets are a solid choice. Their $6/month Droplet runs Pi-hole, WireGuard, and several other lightweight services without breaking a sweat.
Conclusion
A Docker-based Pi-hole setup takes maybe 20 minutes to get right and then runs silently in the background blocking ads and serving local DNS for your entire network. The two things that will save you the most time: deal with the port 53 conflict before you deploy, and give your Pi-hole host a static IP immediately. From there, the web UI is intuitive and the custom local DNS feature alone is worth the effort. Your next step should be pointing your router's DHCP DNS setting at Pi-hole and watching the query log fill up with blocked trackers — it's genuinely eye-opening how much background traffic your devices generate.
Discussion