Privacy intermediate ⏱ 60 minutes

Pi-hole Advanced: Unbound DNS and Custom Blocklists

Advanced Pi-hole with recursive DNS. Custom blocklists and performance optimization.

By homelabbing.io • 2025-12-17
#pihole#unbound#dns#privacy#blocklists

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:

  1. Pi-hole: The DNS filtering and ad-blocking interface
  2. Unbound: A recursive DNS resolver for enhanced privacy
  3. 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

  1. Log in with the password you set in the docker-compose file
  2. 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:

  1. Upstream DNS Servers: Only check “Custom 1 (IPv4)” with 172.20.0.3#5335
  2. Interface listening behavior: Listen on all interfaces
  3. 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.

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:

  1. Configure your router or devices to use YOUR-SERVER-IP:5053 for DNS-over-HTTPS
  2. Alternatively, modify Pi-hole to use cloudflared as upstream:
    • Change PIHOLE_DNS_ to 172.20.0.4#5053 in docker-compose.yml
    • This routes all queries through DoH instead of Unbound

Mobile Device DoH Configuration

iOS (iPhone/iPad):

  1. Install “DNSCloak” or “DNSecure” from App Store
  2. Add custom DNS profile with your server IP
  3. Enable the DNS configuration profile in Settings

Android:

  1. Go to Settings Network & Internet Advanced Private DNS
  2. Select “Private DNS provider hostname”
  3. 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:

  1. Access your router’s admin interface
  2. Navigate to DHCP/DNS settings
  3. Set Primary DNS to YOUR-SERVER-IP
  4. Set Secondary DNS to 1.1.1.1 (fallback)
  5. 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):

  1. Settings Wi-Fi [Your Network]
  2. Configure DNS Manual
  3. Add Server: YOUR-SERVER-IP
  4. Save

Android:

  1. Settings Network & Internet Wi-Fi [Your Network] Advanced
  2. IP Settings Static
  3. DNS 1: YOUR-SERVER-IP
  4. DNS 2: 1.1.1.1
  5. 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:

  1. Go to Pi-hole admin Regex Filters
  2. Add patterns like:
    • ^ad[sxv]?[0-9]*\\. - Matches common ad subdomains
    • ^analytics?\\. - Blocks analytics subdomains
    • ^tracker[s0-9]*\\. - Blocks tracker domains

Security Considerations

  1. Change default password: Always set a strong WEBPASSWORD in docker-compose.yml
  2. Firewall rules: Restrict port 53 to your local network only
  3. Regular updates: Run maintenance script monthly
  4. Monitor logs: Check for unusual query patterns
  5. 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