Pi-hole on Raspberry Pi with Docker: Network-Wide DNS Ad Blocker Setup Guide

Pi-hole is a DNS sinkhole that blocks ads and trackers at the network level. Instead of installing ad blockers on every device, Pi-hole runs on a Raspberry Pi and filters DNS queries for your entire network—including IoT devices, smart TVs, and embedded systems that don’t support traditional ad blockers.

This guide shows how to deploy Pi-hole using Docker on a Raspberry Pi with a real-world router configuration example.

Official documentation: https://docs.pi-hole.net/


Architecture Overview

The final setup looks like this:

Phones / PCs / TVs / IoT Devices
        ↓
Router DHCP (DNS = 192.168.31.49)
        ↓
Raspberry Pi (192.168.31.49)
        ↓
Pi-hole running in Docker :53
        ↓
Upstream DNS (1.1.1.1, etc.)
        ↓
Internet

Why Pi-hole with Docker?

  • Network-wide blocking: One Pi-hole instance protects all devices (phones, laptops, IoT sensors, smart home devices)
  • Containerized: Isolated, easy to update, portable configuration
  • Low resource usage: Runs efficiently on Raspberry Pi 3B+ and newer
  • Embedded DNS control: Perfect for lab networks, development environments, and IoT testbeds
  • Privacy and security: Blocks malware domains, reduces tracking
  • Visibility: See exactly which devices query which domains

Prerequisites

  • Raspberry Pi (Pi 3B+ or newer recommended)
  • Raspberry Pi OS Lite or Desktop installed
  • Network connection (Ethernet preferred for DNS reliability)
  • SSH access or direct terminal access
  • Router with DHCP server configuration access

Step 1: Update Your System

Always start with a clean, updated system:

sudo apt update
sudo apt upgrade -y

Step 2: Install Docker Engine (Important!)

⚠️ Common Trap: Don’t use apt install docker

On Debian Bullseye and some other systems, this command installs an unrelated package called docker together with wmdockernot Docker Engine.

If you accidentally installed it:

sudo apt remove docker wmdocker

Install Docker Engine from Official Repository

Follow Docker’s official installation guide for Debian/Raspberry Pi OS:

# Install prerequisites
sudo apt install ca-certificates curl gnupg

# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add Docker repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Verify Installation

docker --version
docker compose version

Expected output:

Docker version 24.x.x
Docker Compose version v2.x.x

Test Docker

sudo docker run hello-world

Add Your User to Docker Group (Optional)

Avoids needing sudo for every Docker command:

sudo usermod -aG docker $USER

Important: Log out and log back in for this to take effect.


Step 3: Check DNS Port 53 is Available

Pi-hole needs port 53 for DNS. Verify nothing else is using it:

sudo ss -tulpn | grep ':53'

If this returns no results, port 53 is free. If something is listening (like systemd-resolved), you’ll need to disable it:

sudo systemctl disable systemd-resolved
sudo systemctl stop systemd-resolved

Step 4: Create Pi-hole Docker Compose Configuration

Create a directory for Pi-hole:

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

Create compose.yml:

nano compose.yml

Add this configuration:

services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest

    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "8080:80/tcp"

    environment:
      TZ: "Europe/Stockholm"
      FTLCONF_webserver_api_password: "YOUR_SECURE_PASSWORD"
      FTLCONF_dns_listeningMode: "all"

    volumes:
      - "./etc-pihole:/etc/pihole"

    restart: unless-stopped

Configuration notes:

  • Port 53 (TCP + UDP): DNS queries
  • Port 8080:80: Web interface mapped to port 8080 to avoid conflicts
  • TZ: Set your timezone
  • FTLCONF_webserver_api_password: Replace with a strong password
  • FTLCONF_dns_listeningMode: "all": Allows Pi-hole to respond to all network interfaces

Start Pi-hole:

docker compose up -d

Verify it’s running:

docker ps
docker logs pihole

You should see:

CONTAINER ID   IMAGE                  STATUS
abc123...      pihole/pihole:latest   Up X seconds

Step 5: Access the Web Interface

Access the Pi-hole dashboard at:

http://192.168.31.49:8080/admin/

Replace 192.168.31.49 with your Raspberry Pi’s IP address.

Login with the password you set in FTLCONF_webserver_api_password.


Step 6: Test Pi-hole DNS Before Changing Router Settings

Before configuring your router, verify Pi-hole DNS is working:

nslookup google.com 192.168.31.49

Expected result: Normal IP addresses returned, confirming DNS resolution works.

Verify Docker is listening on port 53:

sudo ss -tulpn | grep ':53'

You should see:

0.0.0.0:53        (docker-proxy)
[::]:53           (docker-proxy)

Step 7: Configure Pi-hole Upstream DNS

In the Pi-hole web interface:

Settings → DNS

Select an external upstream DNS provider. Important: Do not use your router’s IP as upstream DNS if the router sends DNS queries back to Pi-hole (creates a DNS loop).

Recommended options:

  • Cloudflare: 1.1.1.1 and 1.0.0.1
  • Quad9: 9.9.9.9
  • Google: 8.8.8.8 and 8.8.4.4

Example:

1.1.1.1
1.0.0.1

Step 8: Configure Your Router DHCP Settings

This is the critical step that applies Pi-hole to all devices automatically.

⚠️ Common Mistake: Don’t Change WAN DNS

Many people mistakenly change:

Router → Internet Settings → DNS

That’s the router’s upstream/WAN DNS, not what DHCP clients receive.

✅ Correct: Change DHCP DNS Settings

The correct location (varies by router model):

LAN Settings → DHCP Server → Primary DNS

Example for Xiaomi AX3000:

  • Navigate to LAN Settings → DHCP Server
  • Set:
    • DNS1: 192.168.31.49 (your Pi-hole IP)
    • DNS2: [blank]

Keep DHCP enabled on your router. The router handles IP assignment but tells clients to use Pi-hole for DNS.

Common router locations:

  • TP-Link: DHCP Settings → Primary DNS
  • ASUS: LAN → DHCP Server → DNS Server
  • Netgear: LAN Setup → Use These DNS Servers
  • Xiaomi: LAN Settings → DHCP Server → DNS Configuration

Step 9: Reserve a Fixed IP for Raspberry Pi

Since all devices depend on 192.168.31.49 (or your Pi’s IP) for DNS, the Raspberry Pi must always keep that address.

Configure your router to assign a fixed IP to the Raspberry Pi based on its MAC address.

Router → DHCP → Address Reservation / Static DHCP Lease

Find the Raspberry Pi in the DHCP client list and assign it a permanent IP.

This is better than static IP configuration on the Pi itself because:

  • Router maintains central control
  • No conflicts with DHCP range
  • Easier to change if needed

Alternative: Static IP on Raspberry Pi

If your router doesn’t support DHCP reservations:

sudo nano /etc/dhcpcd.conf

Add at the end:

interface eth0
static ip_address=192.168.31.49/24
static routers=192.168.31.1
static domain_name_servers=1.1.1.1 8.8.8.8

Restart networking:

sudo systemctl restart dhcpcd

Step 10: Verify Clients Are Using Pi-hole

After a device reconnects to Wi-Fi or renews its DHCP lease:

nslookup google.com

Expected result:

Server:  192.168.31.49
Address: 192.168.31.49#53

This confirms DNS queries are going through Pi-hole.


Step 11: Test DNS Blocking

Simple blocking test:

nslookup doubleclick.net

Expected result:

Name:    doubleclick.net
Address: 0.0.0.0
Address: ::

Pi-hole intercepts the request and returns null IPv4 (0.0.0.0) and IPv6 (::) addresses.

Test non-blocked domain:

nslookup openai.com

Should return normal IP addresses since this domain isn’t on blocklists.

Watch live queries:

Pi-hole → Query Log in the web interface shows real-time DNS requests.


Essential Docker Commands for Pi-hole

View running containers:

docker ps

View Pi-hole logs:

docker logs pihole
docker logs -f pihole  # Follow logs in real-time

Restart Pi-hole:

docker restart pihole

Stop Pi-hole:

cd ~/docker/pihole
docker compose down

Start Pi-hole:

cd ~/docker/pihole
docker compose up -d

Update Pi-hole to latest version:

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

Execute Pi-hole commands inside container:

docker exec pihole pihole status
docker exec pihole pihole -g       # Update gravity/blocklists
docker exec pihole pihole disable  # Temporarily disable blocking
docker exec pihole pihole enable   # Re-enable blocking

Access container shell:

docker exec -it pihole bash

Advanced Configuration

Add custom blocklists:

Web interface → Group Management → Adlists → Add a new adlist

Popular blocklist sources:

After adding lists:

docker exec pihole pihole -g

Whitelist domains:

Via web interface: Whitelist section

Or via command line:

docker exec pihole pihole -w example.com

Blacklist additional domains:

Via web interface: Blacklist section

Or via command line:

docker exec pihole pihole -b ads.example.com

Configure conditional forwarding:

For local DNS resolution of router admin pages, NAS, printers, etc.:

Settings → DNS → Advanced DNS settings → Conditional forwarding

Enable and set:

  • Local network in CIDR notation: 192.168.31.0/24
  • Router IP: 192.168.31.1
  • Local domain name: lan (or your local domain)

Troubleshooting

Pi-hole not blocking ads:

  1. Verify DNS is set correctly on devices:

    nslookup google.com
    

    Should show Pi-hole IP as the server.

  2. Check Pi-hole container is running:

    docker ps
    
  3. Update blocklists:

    docker exec pihole pihole -g
    
  4. Check logs:

    docker logs pihole
    
  5. Verify blocklists are loaded: Web interface → Tools → Update Gravity

DNS resolution failures:

  1. Check upstream DNS servers: Web interface → Settings → DNS

  2. Verify network connectivity from container:

    docker exec pihole ping 1.1.1.1
    
  3. Restart container:

    docker restart pihole
    
  4. Watch live queries:

    docker exec pihole pihole -t
    

Web interface not accessible:

  1. Check container is running:

    docker ps
    
  2. Check port mapping:

    docker port pihole
    

    Should show: 80/tcp -> 0.0.0.0:8080

  3. Check logs for errors:

    docker logs pihole
    
  4. Verify firewall isn’t blocking port 8080:

    sudo iptables -L | grep 8080
    

Container won’t start:

  1. Check port 53 isn’t already in use:

    sudo ss -tulpn | grep ':53'
    
  2. View detailed error logs:

    docker logs pihole
    
  3. Check Docker daemon is running:

    sudo systemctl status docker
    

⚠️ Important Note: ICMP/Ping is Not a Reliable Connectivity Test

You may encounter situations where:

ping 1.1.1.1       # fails
ping 192.168.31.1  # fails

But at the same time:

sudo apt update    # works perfectly
curl google.com    # works perfectly
nslookup google.com # works perfectly

This is normal in some network configurations. Routers or ISPs may block ICMP packets while allowing TCP/HTTP traffic.

ping fails ≠ internet is down

Better connectivity tests:

curl -I https://google.com
nslookup google.com
apt update

These test actual DNS and HTTP connectivity, which is what matters for Pi-hole.


Performance Monitoring

Monitor Docker resource usage:

docker stats pihole

Shows real-time CPU, memory, network I/O.

Monitor Raspberry Pi resources:

htop

Pi-hole typically uses < 100 MB RAM.

Check disk usage:

du -sh ~/docker/pihole/etc-pihole

Backup and Restore

Backup Pi-hole configuration:

Method 1: Web Interface

Settings → Teleporter → Backup

Downloads a .tar.gz file with all settings, blocklists, and configurations.

Method 2: Manual Backup

cd ~/docker/pihole
tar -czf pihole-backup-$(date +%Y%m%d).tar.gz etc-pihole/

Method 3: Copy Docker Volume

cp -r ~/docker/pihole/etc-pihole ~/pihole-backup

Restore from backup:

Web Interface: Settings → Teleporter → Restore

Manual Restore:

cd ~/docker/pihole
docker compose down
tar -xzf pihole-backup-20260829.tar.gz
docker compose up -d

Updating Pi-hole

Update Pi-hole container to latest version:

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

Verify new version:

docker exec pihole pihole version

Update blocklists (gravity):

docker exec pihole pihole -g

Automate updates with cron:

crontab -e

Add:

# Update Pi-hole container every Sunday at 3 AM
0 3 * * 0 cd ~/docker/pihole && docker compose pull && docker compose up -d

# Update gravity/blocklists every day at 3 AM
0 3 * * * docker exec pihole pihole -g

Security Best Practices

Change web admin password:

Update in compose.yml:

FTLCONF_webserver_api_password: "new_secure_password"

Then:

docker compose up -d

Restrict web interface access:

Change port mapping to only allow local access:

ports:
  - "127.0.0.1:8080:80/tcp"

Or use SSH tunneling:

ssh -L 8080:localhost:8080 pi@192.168.31.49

Then access via: http://localhost:8080/admin/

Enable HTTPS (Optional):

Use a reverse proxy like nginx with Let’s Encrypt, or Caddy for automatic HTTPS.


Integration with Embedded Projects

Use Pi-hole in IoT development networks:

  • Block telemetry from IoT devices during testing
  • Monitor which external services your devices contact
  • Create isolated test networks with custom DNS rules
  • Block malicious domains that target IoT devices

Query Pi-hole API from embedded devices:

Get summary statistics:

curl "http://192.168.31.49:8080/admin/api.php?summary"

Returns JSON with:

{
  "domains_being_blocked": 123456,
  "dns_queries_today": 5432,
  "ads_blocked_today": 1234,
  "ads_percentage_today": 22.7
}

Check if domain is blocked:

curl "http://192.168.31.49:8080/admin/api.php?domain=doubleclick.net"

Add custom DNS records:

Create /etc/pihole/custom.list inside the container:

docker exec -it pihole bash
echo "192.168.31.10 homeserver.lan" >> /etc/pihole/custom.list
pihole restartdns

Or add via web interface: Local DNS → DNS Records


Final Network Diagram

Router (192.168.31.1)
    │
    │ DHCP distributes:
    │ DNS = 192.168.31.49
    │
    ↓
All Devices (phones, laptops, TVs, IoT)
    │
    │ DNS queries
    ↓
Raspberry Pi (192.168.31.49)
    │
    │ Docker :53
    ↓
Pi-hole Container
    │
    ├── Blocked → 0.0.0.0 / ::
    │
    └── Allowed
          ↓
      Upstream DNS (1.1.1.1)
          ↓
       Internet

No per-device configuration needed. Everything receiving network settings through DHCP automatically uses Pi-hole for DNS.


Quick Reference: Most Used Commands

# Container management
docker ps                              # Show running containers
docker logs pihole                     # View logs
docker restart pihole                  # Restart Pi-hole
docker exec -it pihole bash            # Access container shell

# Pi-hole commands (inside container)
docker exec pihole pihole status       # Check status
docker exec pihole pihole -g           # Update blocklists
docker exec pihole pihole -t           # Tail query log
docker exec pihole pihole disable 300  # Disable for 5 minutes
docker exec pihole pihole enable       # Re-enable blocking

# Docker Compose
cd ~/docker/pihole
docker compose up -d                   # Start Pi-hole
docker compose down                    # Stop Pi-hole
docker compose pull                    # Pull latest image
docker compose logs -f                 # Follow logs

# Testing
nslookup doubleclick.net               # Test blocking (should return 0.0.0.0)
nslookup google.com                    # Test normal resolution

Resources


Summary

Pi-hole running on Raspberry Pi with Docker provides network-wide DNS filtering with minimal setup and maintenance. By configuring your router’s DHCP to point to Pi-hole, every device on your network automatically benefits from ad blocking, tracker protection, and malware domain filtering—including IoT devices and embedded systems that can’t run traditional ad blockers.

The most satisfying test:

nslookup doubleclick.net
Address: 0.0.0.0
Address: ::

Network-wide DNS blocking: working.