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 wmdocker — not 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 timezoneFTLCONF_webserver_api_password: Replace with a strong passwordFTLCONF_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.1and1.0.0.1 - Quad9:
9.9.9.9 - Google:
8.8.8.8and8.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.
Best Solution: DHCP Reservation (Recommended)
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:
- https://firebog.net/ (curated collection)
- https://github.com/StevenBlack/hosts
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:
Verify DNS is set correctly on devices:
nslookup google.comShould show Pi-hole IP as the server.
Check Pi-hole container is running:
docker psUpdate blocklists:
docker exec pihole pihole -gCheck logs:
docker logs piholeVerify blocklists are loaded: Web interface → Tools → Update Gravity
DNS resolution failures:
Check upstream DNS servers: Web interface → Settings → DNS
Verify network connectivity from container:
docker exec pihole ping 1.1.1.1Restart container:
docker restart piholeWatch live queries:
docker exec pihole pihole -t
Web interface not accessible:
Check container is running:
docker psCheck port mapping:
docker port piholeShould show:
80/tcp -> 0.0.0.0:8080Check logs for errors:
docker logs piholeVerify firewall isn’t blocking port 8080:
sudo iptables -L | grep 8080
Container won’t start:
Check port 53 isn’t already in use:
sudo ss -tulpn | grep ':53'View detailed error logs:
docker logs piholeCheck 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
- Official documentation: https://docs.pi-hole.net/
- Docker Hub: https://hub.docker.com/r/pihole/pihole
- GitHub repository: https://github.com/pi-hole/pi-hole
- Docker installation: https://github.com/pi-hole/docker-pi-hole
- Community forum: https://discourse.pi-hole.net/
- Blocklist sources: https://firebog.net/
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.