Pi-hole on Docker: Block Ads Network-Wide

Pi-hole on Docker: Block Ads Network-Wide

Why Docker for Pi-hole?

Pi-hole's traditional setup assumes a dedicated Raspberry Pi running its own OS. For homelabbers already running containerized infrastructure, this wastes hardware and complicates management. Docker lets you run Pi-hole alongside your other services on existing hardware—I run it on my T5810 workstation with 8GB allocated to containers—while keeping DNS isolated in its own container.

This post covers deploying Pi-hole 5.18.4 on Docker with proper volume persistence, upstream resolver configuration, and access to the stats dashboard. You'll block ads network-wide without hardware redundancy or OS sprawl.

Prerequisites

  • Docker 26.1.3+ and Docker Compose 2.24+
  • Linux host (Ubuntu 24.04 LTS tested) with systemd
  • Port 53 available on your host for DNS (both UDP/TCP)
  • Port 80 available for the web dashboard (or use 8080 if you reverse-proxy it)
  • Static IP for your Docker host or DHCP reservation—clients need a stable DNS target
  • Basic understanding of Docker volumes and networking

Docker Compose Setup

Create a dedicated directory for Pi-hole and set up a docker-compose.yml file. This approach uses named volumes for persistence across container restarts, which is critical for DNS configurations.

version: '3.8'
services:
  pihole:
    image: pihole/pihole:2024.07.0
    container_name: pihole
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
    environment:
      TZ: 'UTC'
      WEBPASSWORD: 'your-secure-password-here'
      PIHOLE_DOMAIN: 'pi.hole'
      DNSMASQ_LISTENING: 'all'
      DNS1: '8.8.8.8'
      DNS2: '8.8.4.4'
      INTERFACE: 'eth0'
    volumes:
      - pihole_etc:/etc/pihole
      - dnsmasq_etc:/etc/dnsmasq.d
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    networks:
      - pihole_net

volumes:
  pihole_etc:
  dnsmasq_etc:

networks:
  pihole_net:
    driver: bridge

Critical gotcha: The INTERFACE variable should match your container's internal interface. Run docker exec pihole ip route to verify. If you're using bridge networking, it's typically eth0, but some setups default to veth. Get this wrong and Pi-hole won't listen on all addresses.

Launch the stack:

docker-compose up -d
docker-compose logs -f pihole  # Watch startup for ~30 seconds

Verify DNS is listening:

docker exec pihole ss -ulnp | grep :53

You should see both TCP and UDP on port 53.

Configuring Upstream DNS Resolvers

By default, Pi-hole forwards queries to Google's resolvers. For privacy-focused setups, you'll want to change this. The environment variables in compose set global defaults, but the web UI overrides them—and that's what you'll use.

Access the dashboard at http://<your-host-ip> and log in with the password you set. Navigate to Settings → DNS.

For a privacy-first setup, I use Quad9 + Cloudflare:

  • Upstream DNS (IPv4): 9.9.9.9 (Quad9, blocks malware)
  • Secondary DNS: 1.1.1.1 (Cloudflare, fast + privacy-respecting)

If you want DNS-over-HTTPS (DoH) upstream, you'll need to run a separate DoH proxy container and point Pi-hole to 127.0.0.1:3000. That's beyond this post's scope, but tools like cloudflared work well for this.

Save and restart gravity with a quick refresh. Pi-hole will test the resolvers and report success.

Adlist Management and Whitelisting

Pi-hole's power comes from blocklists (adlists). The defaults are decent, but you can curate them. Go to Adlists in the dashboard.

I maintain this configuration on my setup:

Paste URLs, enable them, and gravity updates automatically. Check the Gravity page to see how many domains are blocked (mine shows 1.8M currently).

Whitelisting is essential: Some legitimate services get caught by aggressive lists. Navigate to Whitelist and add regexes for false positives. For example, to whitelist Microsoft CDN domains while still blocking trackers:

^[a-z0-9-]+\.cloudflare\.net$

Test with a simple domain first. I always whitelist time.nist.gov and pool.ntp.org to avoid breaking NTP sync.

Setting Pi-hole as Your Network DNS

To actually block ads, devices must query Pi-hole. Configure your router's DHCP settings to hand out your Docker host's IP as the DNS server. On most consumer routers (Asus, Ubiquiti, OpenWrt), this is under DHCP → DNS Server.

If you can't modify the router, configure static DNS on each device:

# Linux/Mac (temporary, until reboot)
sudo nameserver <your-host-ip>

# Or persistent on Ubuntu 24.04 with systemd-resolved:
# Edit /etc/netplan/00-installer-config.yaml:
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: true
      dhcp4-overrides:
        use-dns: false
      nameservers:
        addresses: [<your-host-ip>]
sudo netplan apply

Verify from a client:

dig google-analytics.com @<your-host-ip>
# Should return 0.0.0.0 (blocked) or NXDOMAIN

Monitoring and Dashboard Access

The web dashboard is your window into what Pi-hole is doing. At http://<your-host-ip>/admin, you'll see real-time query stats, blocked percentages, and top clients/domains.

On my setup, I get ~15K queries per day across 12 devices, with a 22% block rate. The dashboard updates live—useful for testing blocklist changes immediately.

For external access (e.g., when away from home), don't expose Pi-hole's admin panel directly to the internet. Instead, use a reverse proxy like Nginx with basic auth, or set up a VPN to your homelab. The dashboard credentials are valuable.

Check query logs under Query Log to audit what's being blocked. This is how you catch false positives. If a service stops working after enabling a new blocklist, check here first.

Common Issues

DNS queries timing out or failing

Problem: Clients can't resolve domains. nslookup example.com <pihole-ip> hangs.

Solution: Verify the container is listening with docker exec pihole ss -ulnp | grep :53. If it's not listening on all interfaces, check the INTERFACE variable—it likely doesn't match your actual interface. Restart the container after fixing it.

High CPU usage on the Pi-hole container

Problem: After a few days, the container consumes 40%+ CPU.

Solution: Gravity is recalculating. This is normal but shouldn't persist. If it does, you have a malformed adlist URL. Go to Adlists, check for red X marks, and disable that list. Restart Pi-hole and monitor with docker stats pihole.

Port 53 already in use

Problem: Container won't start; port 53 is bound elsewhere (often systemd-resolved on the host).

Solution: Disable the host's resolver:

sudo systemctl disable systemd-resolved
sudo systemctl stop systemd-resolved
# Remove /etc/resolv.conf symlink and create a static one
sudo rm /etc/resolv.conf
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf

Then restart the Pi-hole container.

Blocklist not updating

Problem: Gravity shows stale data; domains that should be blocked aren't.

Solution: Navigate to Settings → Maintenance → Gravity Update and trigger it manually. If it fails, check that your upstream resolvers are responding with docker exec pihole nslookup example.com 8.8.8.8. If that fails, your host can't reach the internet—fix network routing first.

What You Now Have

A containerized DNS sinkhole blocking millions of ad domains across your entire network. The setup uses named Docker volumes for persistence, handles both TCP and UDP DNS properly, and gives you a real-time dashboard to monitor what's happening.

Next steps: Set up a local recursive resolver with Unbound for better privacy (Pi-hole will forward to Unbound instead of public DNS). Or implement conditional DNS forwarding to split domains—route internal requests to a local DNS server and external ones through Pi-hole.

For redundancy, run a second Pi-hole instance on another host. Configure clients to use both as primary and secondary DNS. They'll failover automatically if one goes down.

Disclosure: This post contains affiliate links. If you purchase through these links, we may earn a small commission at no extra cost to you. We only recommend services we've tested and trust.

Read more