Pi-hole Advanced: Unbound DNS and Custom Blocklists
Advanced Pi-hole with recursive DNS. Custom blocklists and performance optimization.
Introduction
Pi-hole is a network-wide ad blocker that acts as a DNS sinkhole, blocking ads and tracking domains at the network level before they even reach your devices. This comprehensive guide will help you set up an advanced Pi-hole installation with Unbound for recursive DNS queries, DNS-over-HTTPS support, custom blocklist management, and performance optimizations.
By the end of this guide, you’ll have a production-ready privacy solution that:
- Blocks ads and trackers network-wide across all devices
- Performs recursive DNS queries without relying on third-party DNS providers
- Supports DNS-over-HTTPS (DoH) and DNS-over-TLS (DoT)
- Implements intelligent caching for faster DNS resolution
- Maintains comprehensive custom blocklists
- Includes automated backup procedures
Prerequisites
Before starting, ensure you have:
- A Linux server (bare metal, VM, or Raspberry Pi) with at least 1GB RAM
- Docker and Docker Compose installed
- Static IP address configured for your server
- Root or sudo access
- Basic understanding of DNS and networking concepts
- At least 10GB free disk space
Architecture Overview
Our setup consists of three main components:
- Pi-hole: The DNS filtering and ad-blocking interface
- Unbound: A recursive DNS resolver for enhanced privacy
- Cloudflared (optional): DNS-over-HTTPS proxy for upstream queries
The flow works like this:
- Clients query Pi-hole on port 53
- Pi-hole checks blocklists and returns 0.0.0.0 for blocked domains
- Allowed domains are forwarded to Unbound
- Unbound performs recursive DNS lookups starting from root servers
- Responses are cached and returned to clients
Directory Structure Setup
First, create the necessary directory structure:
mkdir -p ~/pihole-advanced/{pihole,unbound,cloudflared,scripts,backups}
cd ~/pihole-advanced Create subdirectories for configuration:
mkdir -p pihole/etc-pihole
mkdir -p pihole/etc-dnsmasq.d
mkdir -p unbound/conf
mkdir -p cloudflared/config Docker Compose Configuration
Create a docker-compose.yml file with the following configuration:
version: '3.8'
services:
pihole:
container_name: pihole
image: pihole/pihole:latest
hostname: pihole
ports:
- "53:53/tcp"
- "53:53/udp"
- "8080:80/tcp"
environment:
TZ: 'America/New_York'
WEBPASSWORD: 'changeme_secure_password'
FTLCONF_LOCAL_IPV4: 'YOUR-SERVER-IP'
PIHOLE_DNS_: '172.20.0.3#5335'
DNSSEC: 'false'
DNSMASQ_LISTENING: 'all'
REV_SERVER: 'true'
REV_SERVER_TARGET: 'YOUR-ROUTER-IP'
REV_SERVER_CIDR: '192.168.1.0/24'
REV_SERVER_DOMAIN: 'local'
TEMPERATUREUNIT: 'f'
WEBTHEME: 'default-dark'
QUERY_LOGGING: 'true'
PIHOLE_UID: '1000'
PIHOLE_GID: '1000'
volumes:
- './pihole/etc-pihole:/etc/pihole'
- './pihole/etc-dnsmasq.d:/etc/dnsmasq.d'
dns:
- 127.0.0.1
- 1.1.1.1
cap_add:
- NET_ADMIN
restart: unless-stopped
networks:
pihole_net:
ipv4_address: 172.20.0.2
unbound:
container_name: unbound
image: mvance/unbound:latest
hostname: unbound
ports:
- "5335:5335/tcp"
- "5335:5335/udp"
volumes:
- './unbound/conf:/opt/unbound/etc/unbound'
restart: unless-stopped
networks:
pihole_net:
ipv4_address: 172.20.0.3
cloudflared:
container_name: cloudflared
image: cloudflare/cloudflared:latest
hostname: cloudflared
command: proxy-dns
environment:
TUNNEL_DNS_UPSTREAM: 'https://1.1.1.1/dns-query,https://1.0.0.1/dns-query'
TUNNEL_DNS_PORT: '5053'
TUNNEL_DNS_ADDRESS: '0.0.0.0'
ports:
- "5053:5053/udp"
restart: unless-stopped
networks:
pihole_net:
ipv4_address: 172.20.0.4
networks:
pihole_net:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/24 Replace YOUR-SERVER-IP with your server’s actual IP address and YOUR-ROUTER-IP with your router’s IP address.
Unbound Configuration
Create an optimized Unbound configuration file at unbound/conf/unbound.conf:
server:
# Logging
verbosity: 1
log-queries: no
log-replies: no
log-servfail: yes
# Performance
num-threads: 4
msg-cache-slabs: 8
rrset-cache-slabs: 8
infra-cache-slabs: 8
key-cache-slabs: 8
# Cache sizes
rrset-cache-size: 256m
msg-cache-size: 128m
key-cache-size: 128m
# Cache lifetimes
cache-min-ttl: 3600
cache-max-ttl: 86400
# Network
interface: 0.0.0.0
port: 5335
do-ip4: yes
do-ip6: no
do-udp: yes
do-tcp: yes
# Access control
access-control: 0.0.0.0/0 refuse
access-control: 127.0.0.0/8 allow
access-control: 172.20.0.0/24 allow
access-control: 192.168.0.0/16 allow
access-control: 10.0.0.0/8 allow
# Privacy
hide-identity: yes
hide-version: yes
harden-glue: yes
harden-dnssec-stripped: yes
harden-referral-path: yes
# Security
unwanted-reply-threshold: 10000000
do-not-query-localhost: no
prefetch: yes
prefetch-key: yes
# Optimization
so-rcvbuf: 1m
so-sndbuf: 1m
so-reuseport: yes
# Root hints
root-hints: "/opt/unbound/etc/unbound/root.hints"
# Trust anchor for DNSSEC
auto-trust-anchor-file: "/opt/unbound/etc/unbound/root.key"
# Private domains
private-address: 192.168.0.0/16
private-address: 172.16.0.0/12
private-address: 10.0.0.0/8
private-address: 169.254.0.0/16
private-address: fd00::/8
private-address: fe80::/10 Download the root hints file:
curl -o unbound/conf/root.hints https://www.internic.net/domain/named.root Custom Pi-hole Configuration
Create custom DNS settings in pihole/etc-dnsmasq.d/99-custom.conf:
# Increase cache size
cache-size=10000
# Enable DNSSEC
dnssec
trust-anchor=.,20326,8,2,E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D
# Local domain
local=/home.lan/
domain=home.lan
expand-hosts
# Never forward plain names
domain-needed
# Never forward addresses in non-routed address spaces
bogus-priv
# Custom DNS records
address=/pihole.local/YOUR-SERVER-IP
# Conditional forwarding for better local name resolution
server=/local/YOUR-ROUTER-IP Starting the Stack
Start all services:
docker-compose up -d Verify all containers are running:
docker-compose ps Check logs for any errors:
docker-compose logs -f Pi-hole Web Interface Configuration
Access the Pi-hole admin interface at http://YOUR-SERVER-IP:8080/admin
Initial Setup Steps
- Log in with the password you set in the docker-compose file
- Navigate to Settings and configure:
- DNS tab: Ensure only custom DNS (Unbound at 172.20.0.3#5335) is enabled
- DHCP tab: Enable DHCP if you want Pi-hole to handle IP assignments
- API/Web interface: Enable query logging and statistics
DNS Configuration
Navigate to Settings DNS and configure:
- Upstream DNS Servers: Only check “Custom 1 (IPv4)” with
172.20.0.3#5335 - Interface listening behavior: Listen on all interfaces
- Advanced DNS settings:
- Enable: Use DNSSEC
- Enable: Use Conditional Forwarding
- Enable: Never forward non-FQDNs
- Enable: Never forward reverse lookups for private IP ranges
Advanced Blocklist Management
Pi-hole’s power comes from comprehensive blocklists. Here’s how to manage them effectively.
Recommended Blocklists
Add these blocklists via Settings Blocklists in the web interface:
General Ad Blocking:
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
https://adaway.org/hosts.txt
https://v.firebog.net/hosts/AdguardDNS.txt
https://v.firebog.net/hosts/Easylist.txt Privacy & Tracking:
https://v.firebog.net/hosts/Easyprivacy.txt
https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt
https://hostfiles.frogeye.fr/firstparty-trackers-hosts.txt Malicious & Suspicious:
https://raw.githubusercontent.com/DandelionSprout/adfilt/master/Alternate%20versions%20Anti-Malware%20List/AntiMalwareHosts.txt
https://urlhaus.abuse.ch/downloads/hostfile/
https://v.firebog.net/hosts/Prigent-Crypto.txt Social Media Tracking:
https://raw.githubusercontent.com/lightswitch05/hosts/master/ads-and-tracking-extended.txt
https://www.github.developerdan.com/hosts/lists/facebook-extended.txt Custom Blocklist Management Script
Create a script at scripts/update-blocklists.sh:
#!/bin/bash
PIHOLE_CONTAINER="pihole"
GRAVITY_DB="/etc/pihole/gravity.db"
# Array of blocklist URLs
BLOCKLISTS=(
"https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts"
"https://adaway.org/hosts.txt"
"https://v.firebog.net/hosts/AdguardDNS.txt"
"https://v.firebog.net/hosts/Easylist.txt"
"https://v.firebog.net/hosts/Easyprivacy.txt"
"https://raw.githubusercontent.com/crazy-max/WindowsSpyBlocker/master/data/hosts/spy.txt"
)
echo "Updating Pi-hole blocklists..."
# Add each blocklist
for url in "${BLOCKLISTS[@]}"; do
echo "Adding: $url"
docker exec "$PIHOLE_CONTAINER" pihole -a addlist "$url"
done
# Update gravity
echo "Running gravity update..."
docker exec "$PIHOLE_CONTAINER" pihole -g
echo "Blocklists updated successfully!" Make it executable:
chmod +x scripts/update-blocklists.sh Whitelist Management
Create a whitelist script at scripts/whitelist-common.sh:
#!/bin/bash
PIHOLE_CONTAINER="pihole"
# Common domains that should be whitelisted
WHITELIST=(
"www.msftconnecttest.com"
"msftconnecttest.com"
"clients3.google.com"
"clients2.google.com"
"s.youtube.com"
"video-stats.l.google.com"
"android.clients.google.com"
"connectivitycheck.gstatic.com"
"captive.apple.com"
"ocsp.apple.com"
"ipv4.connman.net"
)
echo "Adding common whitelisted domains..."
for domain in "${WHITELIST[@]}"; do
echo "Whitelisting: $domain"
docker exec "$PIHOLE_CONTAINER" pihole -w "$domain"
done
echo "Whitelist updated successfully!" Make it executable:
chmod +x scripts/whitelist-common.sh DNS-over-HTTPS (DoH) Setup
For clients that support DoH, you can configure cloudflared as an additional option.
Cloudflared Configuration
The cloudflared container is already configured in our docker-compose file. To use it:
- Configure your router or devices to use
YOUR-SERVER-IP:5053for DNS-over-HTTPS - Alternatively, modify Pi-hole to use cloudflared as upstream:
- Change
PIHOLE_DNS_to172.20.0.4#5053in docker-compose.yml - This routes all queries through DoH instead of Unbound
- Change
Mobile Device DoH Configuration
iOS (iPhone/iPad):
- Install “DNSCloak” or “DNSecure” from App Store
- Add custom DNS profile with your server IP
- Enable the DNS configuration profile in Settings
Android:
- Go to Settings Network & Internet Advanced Private DNS
- Select “Private DNS provider hostname”
- Enter your server’s hostname or configure using apps like “Intra”
Performance Optimization
Cache Tuning
Monitor cache performance:
docker exec unbound unbound-control stats_noreset Adjust cache sizes based on your query volume:
- For light use (under 100 queries/day): Current settings are fine
- For medium use (100-1000 queries/day): Double cache sizes
- For heavy use (over 1000 queries/day): Quadruple cache sizes and increase threads
Query Response Time Optimization
Monitor Pi-hole query times:
docker exec pihole pihole -c -e Optimize dnsmasq cache in pihole/etc-dnsmasq.d/99-custom.conf:
# Increase cache size for better performance
cache-size=20000
# Prefetch DNS queries
dns-forward-max=1000 Resource Monitoring
Create a monitoring script at scripts/monitor-performance.sh:
#!/bin/bash
echo "=== Pi-hole Performance Monitor ==="
echo ""
echo "Container Stats:"
docker stats --no-stream pihole unbound cloudflared
echo ""
echo "Pi-hole Queries (last 24h):"
docker exec pihole pihole -c -j | jq '.queries_forwarded, .queries_cached'
echo ""
echo "Unbound Cache Hit Rate:"
docker exec unbound unbound-control stats_noreset | grep "num.query" | head -5
echo ""
echo "Memory Usage:"
docker exec pihole cat /proc/meminfo | grep -E "MemTotal|MemFree|MemAvailable" Backup Procedures
Automated Backup Script
Create a comprehensive backup script at scripts/backup-pihole.sh:
#!/bin/bash
BACKUP_DIR="./backups"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="pihole_backup_$TIMESTAMP"
PIHOLE_CONTAINER="pihole"
# Create backup directory
mkdir -p "$BACKUP_DIR/$BACKUP_NAME"
echo "Starting Pi-hole backup..."
# Backup Pi-hole configuration
echo "Backing up Pi-hole configuration..."
docker exec "$PIHOLE_CONTAINER" pihole -a -t "$BACKUP_NAME.tar.gz"
docker cp "$PIHOLE_CONTAINER:/etc/pihole/$BACKUP_NAME.tar.gz" "$BACKUP_DIR/$BACKUP_NAME/"
# Backup custom DNS settings
echo "Backing up custom DNS configuration..."
cp -r pihole/etc-dnsmasq.d "$BACKUP_DIR/$BACKUP_NAME/"
# Backup Unbound configuration
echo "Backing up Unbound configuration..."
cp -r unbound/conf "$BACKUP_DIR/$BACKUP_NAME/"
# Backup docker-compose file
echo "Backing up Docker Compose configuration..."
cp docker-compose.yml "$BACKUP_DIR/$BACKUP_NAME/"
# Create archive
echo "Creating compressed archive..."
cd "$BACKUP_DIR"
tar -czf "$BACKUP_NAME.tar.gz" "$BACKUP_NAME"
rm -rf "$BACKUP_NAME"
# Keep only last 7 backups
echo "Cleaning old backups (keeping last 7)..."
ls -t pihole_backup_*.tar.gz | tail -n +8 | xargs -r rm
echo "Backup completed: $BACKUP_DIR/$BACKUP_NAME.tar.gz" Make it executable:
chmod +x scripts/backup-pihole.sh Automated Backup with Cron
Add a cron job to run backups daily:
crontab -e Add this line to run backup at 2 AM daily:
0 2 * * * cd /path/to/pihole-advanced && ./scripts/backup-pihole.sh >> ./backups/backup.log 2>&1 Restore Procedure
Create a restore script at scripts/restore-pihole.sh:
#!/bin/bash
if [ -z "$1" ]; then
echo "Usage: $0 backup_file.tar.gz"
exit 1
fi
BACKUP_FILE="$1"
RESTORE_DIR="/tmp/pihole_restore"
echo "Restoring from $BACKUP_FILE..."
# Extract backup
mkdir -p "$RESTORE_DIR"
tar -xzf "$BACKUP_FILE" -C "$RESTORE_DIR"
BACKUP_NAME=$(basename "$BACKUP_FILE" .tar.gz)
# Stop containers
echo "Stopping containers..."
docker-compose down
# Restore configurations
echo "Restoring configurations..."
cp -r "$RESTORE_DIR/$BACKUP_NAME/etc-dnsmasq.d/"* pihole/etc-dnsmasq.d/
cp -r "$RESTORE_DIR/$BACKUP_NAME/conf/"* unbound/conf/
# Start containers
echo "Starting containers..."
docker-compose up -d
# Wait for Pi-hole to be ready
sleep 10
# Restore Pi-hole settings
echo "Restoring Pi-hole settings..."
docker cp "$RESTORE_DIR/$BACKUP_NAME/$BACKUP_NAME.tar.gz" pihole:/tmp/
docker exec pihole pihole -a -t /tmp/$BACKUP_NAME.tar.gz
# Cleanup
rm -rf "$RESTORE_DIR"
echo "Restore completed successfully!" Make it executable:
chmod +x scripts/restore-pihole.sh Client Configuration
Router-Level Configuration
Configure your router to use Pi-hole as the primary DNS server:
- Access your router’s admin interface
- Navigate to DHCP/DNS settings
- Set Primary DNS to
YOUR-SERVER-IP - Set Secondary DNS to
1.1.1.1(fallback) - Save and reboot router
Individual Device Configuration
Windows:
# Run as Administrator
netsh interface ipv4 set dns "Ethernet" static YOUR-SERVER-IP primary
netsh interface ipv4 add dns "Ethernet" 1.1.1.1 index=2 macOS:
networksetup -setdnsservers Wi-Fi YOUR-SERVER-IP 1.1.1.1 Linux: Edit /etc/resolv.conf:
nameserver YOUR-SERVER-IP
nameserver 1.1.1.1 iOS (iPhone/iPad):
- Settings Wi-Fi [Your Network]
- Configure DNS Manual
- Add Server: YOUR-SERVER-IP
- Save
Android:
- Settings Network & Internet Wi-Fi [Your Network] Advanced
- IP Settings Static
- DNS 1: YOUR-SERVER-IP
- DNS 2: 1.1.1.1
- Save
Monitoring and Maintenance
Regular Maintenance Tasks
Create a maintenance script at scripts/maintenance.sh:
#!/bin/bash
echo "=== Pi-hole Maintenance ==="
echo ""
# Update Docker images
echo "Updating Docker images..."
docker-compose pull
# Restart containers with new images
echo "Restarting containers..."
docker-compose up -d
# Update gravity
echo "Updating gravity database..."
docker exec pihole pihole -g
# Update root hints
echo "Updating root hints..."
curl -o unbound/conf/root.hints https://www.internic.net/domain/named.root
docker-compose restart unbound
# Clean old logs
echo "Cleaning old logs..."
docker exec pihole pihole flush
# Backup
echo "Creating backup..."
./scripts/backup-pihole.sh
echo "Maintenance completed!" Make it executable:
chmod +x scripts/maintenance.sh Schedule monthly maintenance:
crontab -e Add:
0 3 1 * * cd /path/to/pihole-advanced && ./scripts/maintenance.sh >> ./backups/maintenance.log 2>&1 Health Check Script
Create a health check script at scripts/healthcheck.sh:
#!/bin/bash
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
echo "=== Pi-hole Health Check ==="
# Check containers
echo -n "Pi-hole container: "
if docker ps | grep -q pihole; then
echo -e "${GREEN}Running${NC}"
else
echo -e "${RED}Not running${NC}"
fi
echo -n "Unbound container: "
if docker ps | grep -q unbound; then
echo -e "${GREEN}Running${NC}"
else
echo -e "${RED}Not running${NC}"
fi
# Test DNS resolution
echo -n "DNS resolution test: "
if dig @localhost google.com +short > /dev/null 2>&1; then
echo -e "${GREEN}Passed${NC}"
else
echo -e "${RED}Failed${NC}"
fi
# Check blocking
echo -n "Ad blocking test: "
RESULT=$(dig @localhost ads.google.com +short)
if [ "$RESULT" = "0.0.0.0" ]; then
echo -e "${GREEN}Working${NC}"
else
echo -e "${RED}Not blocking${NC}"
fi
# Check disk space
echo "Disk usage:"
df -h | grep -E "Filesystem|/mnt|/$"
echo ""
echo "Query statistics (last 24h):"
docker exec pihole pihole -c -j | jq '{total: .dns_queries_today, blocked: .ads_blocked_today, percent: .ads_percentage_today}' Make it executable:
chmod +x scripts/healthcheck.sh Troubleshooting
Common Issues
Issue: DNS queries timing out
# Check if containers are running
docker-compose ps
# Check Unbound logs
docker-compose logs unbound
# Test Unbound directly
dig @172.20.0.3 -p 5335 google.com Issue: Websites not loading
# Check Pi-hole logs
docker-compose logs pihole | tail -100
# Check if domain is blocked
docker exec pihole pihole -q example.com
# Temporarily disable Pi-hole
docker exec pihole pihole disable 5m Issue: High memory usage
# Check container stats
docker stats pihole unbound
# Reduce cache sizes in unbound.conf
# Restart Unbound
docker-compose restart unbound Advanced Features
Conditional Forwarding
For better local network name resolution, configure conditional forwarding in pihole/etc-dnsmasq.d/99-custom.conf:
server=/home.lan/YOUR-ROUTER-IP
server=/local/YOUR-ROUTER-IP DHCP Server Configuration
If using Pi-hole as DHCP server, add to docker-compose.yml:
ports:
- "67:67/udp"
- "68:68/udp" Then enable in Pi-hole web interface under Settings DHCP.
Regex Blocking
Create advanced blocking rules using regex in Pi-hole:
- Go to Pi-hole admin Regex Filters
- Add patterns like:
^ad[sxv]?[0-9]*\\.- Matches common ad subdomains^analytics?\\.- Blocks analytics subdomains^tracker[s0-9]*\\.- Blocks tracker domains
Security Considerations
- Change default password: Always set a strong WEBPASSWORD in docker-compose.yml
- Firewall rules: Restrict port 53 to your local network only
- Regular updates: Run maintenance script monthly
- Monitor logs: Check for unusual query patterns
- Backup encryption: Encrypt backup files if storing off-site
Performance Benchmarks
Expected performance metrics:
- Query response time: 10-50ms (cached), 100-300ms (uncached)
- Memory usage: 150-300MB for Pi-hole, 50-100MB for Unbound
- Blocking rate: 15-30% depending on blocklists
- Cache hit rate: 60-80% after initial warm-up
Conclusion
You now have a production-ready Pi-hole installation with:
- Recursive DNS resolution through Unbound for maximum privacy
- Optional DNS-over-HTTPS support via cloudflared
- Comprehensive blocklists covering ads, trackers, and malware
- Automated backups and maintenance procedures
- Performance optimizations for fast query resolution
- Mobile device support and configuration
Your network traffic is now protected at the DNS level, blocking ads and trackers before they reach any device on your network. Regular maintenance and monitoring will ensure optimal performance and security.
Next Steps
Consider these enhancements:
- Set up a secondary Pi-hole for redundancy
- Implement DNS load balancing
- Add Grafana dashboards for visualization
- Configure alerts for service disruptions
- Explore additional privacy tools like VPN integration
For support and updates, visit the Pi-hole community forums and documentation at https://pi-hole.net