How to Self-Host Pi-hole with Docker Compose: Network-Wide Ad Blocking for Your Entire Home

Why Your Router’s Ads Are Winning (And How to Fight Back)

Here’s something nobody tells you when you move into a new apartment or set up your home network: your ISP and every device on your LAN are phoning home constantly. Smart TVs serve ads. Phones ping tracking servers. Even your router’s admin page is probably riddled with telemetry. The internet at the DNS level is noisy, bloated, and leaky.

Enter Pi-hole: a network-wide sinkhole that sits between your devices and the internet’s ad-serving infrastructure. It’s not a browser extension. It’s not a one-device fix. It’s a DNS server that answers “I’ve never heard of that domain” to every tracker, ad server, and analytics endpoint on the blocklist — and it does it for every device on your network without installing a single client.

In this guide, we’ll deploy Pi-hole using Docker Compose so it’s easy to update, easy to back up, and easy to reason about. By the end, you’ll have a lean, mean DNS server protecting your entire network.


Prerequisites

  • A Linux machine (Ubuntu/Debian/Raspberry Pi OS) with Docker and Docker Compose installed
  • Port 53 (DNS) and port 80 (or a custom web UI port) available on the host
  • Router access to change DNS settings (we’ll do this at the end)
  • Basic comfort with the terminal

Note on port 53: On some systems, systemd-resolved already listens on port 53 for local DNS caching. If you hit a “port already in use” error later, check the troubleshooting section at the bottom.


Step 1: Create Your Project Directory

Let’s keep things tidy. Create a dedicated directory for Pi-hole:

mkdir -p ~/docker/pihole && cd ~/docker/pihole

Everything lives here. When you need to back up or migrate, this single folder has it all.

Step 2: Write the Docker Compose File

Create your docker-compose.yml:

nano docker-compose.yml

Paste the following configuration:

version: "3.8"

services:
  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    restart: unless-stopped
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "8080:80/tcp"
    environment:
      TZ: "America/New_York"
      WEBPASSWORD: "changeme-strong-password-here"
      DNSMASQ_LISTENING: "all"
      FTLCONF_LOCAL_IPV4: "YOUR.HOST.IP.ADDRESS"
    volumes:
      - ./etc-pihole:/etc/pihole
      - ./etc-dnsmasq.d:/etc/dnsmasq.d
    cap_add:
      - NET_ADMIN
    networks:
      - pihole_net

networks:
  pihole_net:
    driver: bridge

Now let’s break down what each part does, because understanding this file makes debugging so much easier:

Setting What It Does
restart: unless-stopped Auto-restarts on reboot and after crashes. You intentionally stopping the container is the only thing that keeps it down.
53:53/tcp & 53:53/udp Exposes DNS. Both TCP and UDP are needed — standard DNS queries use UDP, but large responses and zone transfers use TCP.
8080:80 Maps the Pi-hole admin web UI to port 8080 on the host. We skip 80 to avoid conflicts with existing web servers.
WEBPASSWORD The password for the admin dashboard. Change it significantly. I’m not kidding.
FTLCONF_LOCAL_IPV4 Your host machine’s LAN IP. This is critical for the “Client details” view to work correctly.
volumes Persists Pi-hole settings and blocklists across container updates. Without this, you’d lose every config change on upgrade.
NET_ADMIN Grants network administration capability. Pi-hole needs this for its internal DNS/firewall rules.

Important: Replace YOUR.HOST.IP.ADDRESS with your actual server IP (e.g., 192.168.1.50). And please, for the love of everything, replace the WEBPASSWORD. Run openssl rand -base64 32 to generate something strong.

Step 3: Launch Pi-hole

Pull the image and start the container:

docker compose up -d

Give it about 30 seconds to initialize. Then verify it’s running:

docker compose logs --tail=20 pihole

You should see something like:

pihole  | [✓] DNS service is running
pihole  | [✓] Pi-hole blocking is enabled

Step 4: Test Your DNS Sinkhole

Before reconfiguring anything, let’s verify Pi-hole actually works. From any other device on your network:

dig doubleclick.net @YOUR.HOST.IP.ADDRESS

You should see the response resolve to 0.0.0.0 — that’s Pi-hole saying “that domain doesn’t exist.” The ad/tracker domain has been swallowed into the void.

For comparison, try a normal domain:

dig example.com @YOUR.HOST.IP.ADDRESS

This should return the real IP address, because Pi-hole forwards legitimate queries to the upstream DNS servers (by default, Google’s 8.8.8.8 and Cloudflare’s 1.1.1.1).

Step 5: Explore the Admin Dashboard

Open your browser and navigate to:

http://YOUR.HOST.IP.ADDRESS:8080/admin

Log in with the WEBPASSWORD you set. The dashboard is gorgeous and packed with useful info:

  • Query Log: Watch DNS requests in real time. It’s hypnotic and slightly horrifying to see how often your devices phone home.
  • Top Domains: See which domains are queried most — and which are being blocked.
  • Blocklists: Pi-hole comes with several pre-configured blocklists. You can add more from the Group Management → Adlists section.

Take a few minutes to poke around. The dashboard is where you’ll spend 99% of your time with Pi-hole after initial setup.

Step 6: Point Your Router at Pi-hole

This is the moment it all comes together. Instead of configuring DNS device-by-device, you change your router’s DNS setting so every device on your network automatically uses Pi-hole:

  1. Log into your router’s admin page (usually 192.168.1.1 or 192.168.0.1).
  2. Find the DHCP or DNS settings section.
  3. Set the primary DNS to your Pi-hole server’s IP address.
  4. Optionally set secondary DNS to a public resolver (like 1.1.1.1) as a fallback in case Pi-hole goes down.
  5. Save and reboot your router (or lease-renew your devices with sudo dhclient -r && sudo dhclient on Linux).

Now every phone, laptop, smart TV, and IoT gadget on your network is blocking ads. No per-device configuration needed.

Pro tip: If you set a secondary public DNS as a fallback, devices can bypass Pi-hole when your server is offline. For strict blocking, leave the secondary blank — but be aware that if Pi-hole goes down, devices won’t resolve anything until it comes back.

Optional: Set Up Conditional Forwarding

If you want Pi-hole to show device hostnames instead of IP addresses in the dashboard (so you can see “living-room-tv” instead of “192.168.1.47”), enable conditional forwarding:

  1. Go to Settings → DNS in the Pi-hole admin.
  2. Enable Use Conditional Forwarding.
  3. Enter your router’s IP as the upstream DNS.
  4. Enter your local domain (often local or lan — check your router).

This lets Pi-hole ask your router for local hostnames while still blocking ads upstream.


Common Pitfalls & Troubleshooting

Port 53 Already in Use

If you see bind: address already in use in the logs, something on your host is already claiming port 53. On Ubuntu:

sudo lsof -i :53

If it’s systemd-resolved, disable its DNS stub listener:

sudo nano /etc/systemd/resolved.conf

Set:

[Resolve]
DNSStubListener=no

Then:

sudo systemctl restart systemd-resolved
sudo systemctl restart docker

Permission Denied on /etc/pihole

Pi-hole runs as a specific user inside the container and expects certain filesystem permissions. If you see permission errors:

sudo chown -R 999:999 ~/docker/pihole/etc-pihole

Or simply remove the volumes and let Pi-hole recreate them:

docker compose down
rm -rf ~/docker/pihole/etc-pihole ~/docker/pihole/etc-dnsmasq.d
docker compose up -d

No Blocking, But DNS Works Fine

Check that gravity (the blocklist updater) ran successfully:

docker exec -it pihole pihole -g

This forces a gravity update. Check the admin dashboard — the “domains on blocklist” count should be well into the hundreds of thousands.

The Web UI Is Accessible But Shows No Data

Make sure FTLCONF_LOCAL_IPV4 is set to your server’s LAN IP, not 127.0.0.1. Pi-hole uses this to correlate queries to client IPs.


Keeping Pi-hole Updated

One of the beautiful things about the Docker setup: updating is trivial.

cd ~/docker/pihole
docker compose pull
docker compose up -d

Your configuration and blocklists live in the mounted volumes, so they survive container recreation. Run this every couple of weeks, or set up a Watchtower container for automatic updates.


What to Expect After Going Live

The first 24 hours are eye-opening. You’ll watch the query log and realize that your “smart” devices are anything but private. Here’s what typically blocks on a home network:

  • Smart TV telemetry (Roku, Samsung, Vizio all phone home constantly)
  • Windows telemetry endpoints
  • Mobile app analytics and ad networks
  • IoT device health checks (some of which double as tracking beacons)

You can expect to block anywhere from 20% to 50% of total DNS queries, depending on the noisiest devices in your house. On my network, a Roku Ultra alone accounts for a staggering amount of background chatter.

The first time you open a streaming app and there are no pre-roll ads, you’ll feel like you’ve hacked the matrix. You haven’t. You just stopped asking the internet’s loudest servers the questions they shouldn’t be answering.

Welcome to a quieter internet. Your future self will thank you.


Got questions or want to share your Pi-hole stats? Drop a comment below or reach out on Discord. I’m always curious what the noisiest device in your network turns out to be.

Leave a Reply

Your email address will not be published. Required fields are marked *