Network Services beginner ⏱ 45 minutes

Pi-hole: Complete Network-Wide Ad Blocking Guide

Deploy network-level ad blocking for all devices. DNS management, privacy protection, and custom blocklists.

By homelabbing.io • 2025-12-16
#pihole#dns#ad-blocking#privacy#networking

Introduction

Pi-hole is a powerful network-level ad blocker that acts as a DNS sinkhole, protecting every device on your network from advertisements, trackers, and malicious domains without requiring individual installation on each device. In this comprehensive guide, you’ll learn how to deploy, configure, and optimize Pi-hole in your homelab, complete with automation strategies and integration with other services.

What You’ll Learn

  • How DNS-level ad blocking works and why it’s effective
  • Multiple deployment methods (Docker, bare metal, VMs)
  • Configuring blocklists and managing false positives
  • Setting up local DNS and DHCP services
  • Monitoring performance and optimizing configuration
  • AI-powered automation for intelligent threat detection
  • Integration with monitoring stacks and home automation

Why Pi-hole?

Network-Wide Protection: A single Pi-hole instance protects all connected devices, including smart TVs, IoT devices, and mobile apps that don’t support traditional ad blockers.

Privacy by Design: By blocking tracking domains at the DNS level, you prevent data exfiltration before it happens. When self-hosted, you maintain complete control over your DNS queries.

Performance Gains: Ads never download, saving bandwidth and reducing page load times. Devices use less CPU and memory, extending battery life on mobile devices.

Educational Value: Pi-hole provides insight into network traffic patterns and hands-on experience with DNS, Docker, and system administration.

Prerequisites

Hardware Requirements:

  • CPU: 1GHz or faster
  • RAM: 512MB minimum (1GB+ recommended)
  • Storage: 2GB minimum (20GB+ recommended for logs)
  • Network: Ethernet connection strongly recommended

Knowledge Requirements:

  • Basic Linux command line familiarity
  • Understanding of home network configuration
  • Access to router admin interface
  • (Optional) Docker experience for container deployment

Time Commitment:

  • Initial setup: 30-45 minutes
  • Configuration: 1-2 hours
  • Ongoing maintenance: 15 minutes per week

Understanding DNS-Level Ad Blocking

How DNS Works

Before diving into Pi-hole, let’s understand the Domain Name System (DNS). When you type ”www.example.com” into your browser:

  1. Your device sends a DNS query: “What’s the IP address for www.example.com?”
  2. A DNS resolver responds with the IP address: “93.184.216.34”
  3. Your device connects to that IP address to load the website

This happens for every domain - legitimate websites, ad servers, and tracking domains alike.

The DNS Sinkhole Technique

Pi-hole intercepts DNS queries and implements a “sinkhole” approach:

User clicks website
    ↓
Device requests: ads.example.com
    ↓
Pi-hole checks blocklist
    ↓
Domain found in blocklist? → YES → Return 0.0.0.0 (null address)
    ↓                                      ↓
    NO                              Device can't connect
    ↓                                      ↓
Forward to upstream DNS              Ad never loads
    ↓
Return legitimate IP
    ↓
Content loads normally

Key Advantages:

  • Works across all applications, not just browsers
  • No CPU/memory impact on client devices
  • Blocks ads in native apps and smart TVs
  • Single point of control for entire network
  • Bandwidth saved by preventing downloads

Blocklists and Gravity

Blocklists (also called “adlists”) are curated text files containing domain names of known advertising and tracking servers, maintained by security researchers and the community:

# Example blocklist content
ads.example.com
tracker.network.com
analytics.advertising.com
telemetry.data-collector.com

Gravity is Pi-hole’s compiled database that merges multiple blocklists into a single searchable index. When you run “Update Gravity,” Pi-hole:

  1. Downloads all configured blocklists
  2. Removes duplicates and validates entries
  3. Compiles into an optimized SQLite database
  4. Makes it ready for instant lookups

Installation Methods

Choosing Your Deployment Method

MethodBest ForProsCons
DockerExisting Docker infrastructureEasy updates, isolated environmentNetworking complexity for DHCP
Bare MetalDedicated serversMinimal overhead, full controlOS-specific configuration
Virtual MachineVM environmentsComplete isolation, snapshotsHigher resource usage

Docker provides the easiest path to deployment with simple updates and clean isolation from other services.

Basic Docker Compose Configuration

Create a directory for your Pi-hole deployment:

mkdir -p ~/homelab/pihole
cd ~/homelab/pihole

Create docker-compose.yml:

version: '3.8'

services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
      - "443:443/tcp"
    environment:
      TZ: 'America/Chicago'
      WEBPASSWORD: 'ChangeThisToASecurePassword'
      FTLCONF_LOCAL_IPV4: '192.168.1.100'  # Your server's IP
      DNS1: '1.1.1.1'
      DNS2: '8.8.8.8'
      DNSMASQ_LISTENING: 'all'
    volumes:
      - './etc-pihole:/etc/pihole'
      - './etc-dnsmasq.d:/etc/dnsmasq.d'
    networks:
      - pihole_network
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    dns:
      - 127.0.0.1
      - 1.1.1.1

networks:
  pihole_network:
    driver: bridge

Deploy Pi-hole:

# Start the container
docker-compose up -d

# Verify it's running
docker-compose ps

# Check logs
docker-compose logs -f pihole

Docker Configuration with DHCP

If you want Pi-hole to handle DHCP for your network, use host networking:

version: '3.8'

services:
  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    network_mode: "host"
    environment:
      TZ: 'America/Chicago'
      WEBPASSWORD: 'ChangeThisToASecurePassword'
      FTLCONF_LOCAL_IPV4: '192.168.1.100'
      DNS1: '1.1.1.1'
      DNS2: '8.8.8.8'
      DHCP_ACTIVE: 'true'
      DHCP_START: '192.168.1.201'
      DHCP_END: '192.168.1.251'
      DHCP_ROUTER: '192.168.1.1'
      PIHOLE_DOMAIN: 'lan'
    volumes:
      - './etc-pihole:/etc/pihole'
      - './etc-dnsmasq.d:/etc/dnsmasq.d'
    restart: unless-stopped
    cap_add:
      - NET_ADMIN

Important: You must disable DHCP on your router before enabling it in Pi-hole.

Docker with Unbound (Privacy-Enhanced)

For maximum privacy, run Pi-hole with Unbound as a recursive DNS resolver:

version: '3.8'

services:
  unbound:
    container_name: unbound
    image: mvance/unbound:latest
    ports:
      - "5335:53/tcp"
      - "5335:53/udp"
    volumes:
      - './unbound:/opt/unbound/etc/unbound'
    networks:
      - pihole_network
    restart: unless-stopped

  pihole:
    container_name: pihole
    image: pihole/pihole:latest
    depends_on:
      - unbound
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
      - "443:443/tcp"
    environment:
      TZ: 'America/Chicago'
      WEBPASSWORD: 'ChangeThisToASecurePassword'
      FTLCONF_LOCAL_IPV4: '192.168.1.100'
      DNS1: '172.20.0.2#5335'  # Unbound container
      DNS2: 'no'
    volumes:
      - './etc-pihole:/etc/pihole'
      - './etc-dnsmasq.d:/etc/dnsmasq.d'
    networks:
      pihole_network:
        ipv4_address: 172.20.0.3
    restart: unless-stopped
    cap_add:
      - NET_ADMIN
    dns:
      - 127.0.0.1
      - 1.1.1.1

networks:
  pihole_network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16

Create the Unbound configuration:

mkdir -p unbound

Create unbound/unbound.conf:

server:
    # Enable verbosity for debugging
    verbosity: 0

    # Network interface
    interface: 0.0.0.0
    port: 53
    do-ip4: yes
    do-udp: yes
    do-tcp: yes

    # May be set to yes if you have IPv6 connectivity
    do-ip6: no

    # Use this only when you downloaded the list of primary root servers!
    # root-hints: "/opt/unbound/etc/unbound/root.hints"

    # Trust glue only if it is within the server's authority
    harden-glue: yes

    # Require DNSSEC data for trust-anchored zones, if such data is absent, the zone becomes BOGUS
    harden-dnssec-stripped: yes

    # Don't use Capitalization randomization as it known to cause DNSSEC issues sometimes
    use-caps-for-id: no

    # Reduce EDNS reassembly buffer size.
    edns-buffer-size: 1232

    # Perform prefetching of close to expired message cache entries
    prefetch: yes

    # One thread should be sufficient
    num-threads: 1

    # Ensure kernel buffer is large enough to not lose messages in traffic spikes
    so-rcvbuf: 1m

    # Ensure privacy of local IP ranges
    private-address: 192.168.0.0/16
    private-address: 169.254.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8
    private-address: fd00::/8
    private-address: fe80::/10

Method 2: Bare Metal Installation

For dedicated hardware or maximum performance, install directly on the operating system.

Ubuntu/Debian Installation

Update your system:

sudo apt update
sudo apt upgrade -y
sudo apt install curl -y

Install Pi-hole:

curl -sSL https://install.pi-hole.net | bash

The installer will guide you through:

  1. Interface Selection: Choose your primary network interface (usually eth0)
  2. Upstream DNS Provider: Select from Cloudflare, Google, OpenDNS, etc.
  3. Block Lists: Default lists are recommended for beginners
  4. IP Protocols: Enable both IPv4 and IPv6 if your network supports it
  5. Static IP Warning: Ensure your Pi-hole has a static IP address
  6. Web Admin Interface: Enable this (recommended)
  7. Web Server: Install lighttpd (yes)
  8. Logging: Enable query logging (yes)
  9. Privacy Mode: Choose your preferred level

After installation:

# Set admin password
pihole -a -p

# Check status
pihole status

# View version
pihole -v

Post-Installation Configuration

Configure a static IP address (if not already set):

# Edit netplan configuration (Ubuntu 20.04+)
sudo nano /etc/netplan/00-installer-config.yaml

Example configuration:

network:
  version: 2
  ethernets:
    eth0:
      dhcp4: no
      addresses:
        - 192.168.1.100/24
      gateway4: 192.168.1.1
      nameservers:
        addresses:
          - 127.0.0.1
          - 1.1.1.1

Apply changes:

sudo netplan apply

Method 3: Virtual Machine Deployment

Proxmox LXC Container (Most Efficient)

Create an LXC container in Proxmox:

  1. Download Ubuntu 22.04 LXC template

  2. Create container with:

    • CPU cores: 2
    • Memory: 2048 MB
    • Storage: 20 GB
    • Network: Bridge to your LAN
    • Static IP: Assign static IP (e.g., 192.168.1.100)
  3. Start container and access console

  4. Run bare metal installation steps above

VMware/VirtualBox VM

Create a new VM:

  • OS: Ubuntu Server 22.04 LTS
  • vCPU: 2 cores
  • RAM: 2 GB
  • Disk: 30 GB
  • Network: Bridged adapter

Install Ubuntu Server, then follow bare metal installation steps.

Accessing the Web Interface

Once installed, access the Pi-hole admin dashboard:

http://pi.hole/admin

Or by IP address:

http://192.168.1.100/admin

Login with the password you set during installation.


Initial Configuration

Dashboard Overview

The Pi-hole dashboard provides real-time metrics:

Total Queries: DNS queries received in the last 24 hours Queries Blocked: Number of queries matching blocklists Percent Blocked: Percentage of total queries blocked Blocklist Last Updated: When Gravity was last updated Domains on Blocklist: Total number of blocked domains

Configuring Network Devices

You have three options for directing traffic to Pi-hole:

Most home routers allow you to set custom DNS servers:

  1. Access your router’s admin interface (usually 192.168.1.1 or 192.168.0.1)
  2. Navigate to DHCP settings (location varies by router)
  3. Find DNS server configuration
  4. Set Primary DNS to your Pi-hole IP (e.g., 192.168.1.100)
  5. Set Secondary DNS to 1.1.1.1 or leave blank
  6. Save settings

Clients will receive Pi-hole DNS on their next DHCP lease renewal.

To force renewal on clients:

# Windows
ipconfig /release
ipconfig /renew

# Linux
sudo dhclient -r
sudo dhclient

# macOS
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

Option 2: Manual Configuration Per Device

For testing or specific devices:

Windows:

  1. Settings → Network & Internet → Change adapter options
  2. Right-click your network adapter → Properties
  3. Select “Internet Protocol Version 4 (TCP/IPv4)” → Properties
  4. Select “Use the following DNS server addresses”
  5. Set Preferred DNS: 192.168.1.100 (your Pi-hole)
  6. Set Alternate DNS: 1.1.1.1

macOS:

  1. System Preferences → Network
  2. Select your connection → Advanced
  3. DNS tab → Add Pi-hole IP

Linux:

  1. Edit /etc/resolv.conf:
    nameserver 192.168.1.100
    nameserver 1.1.1.1

Option 3: Pi-hole as DHCP Server

For advanced users who want centralized management:

Admin Panel → Settings → DHCP

Configuration:

  • DHCP Active: Enable
  • DHCP Authoritative Mode: Enable
  • Range Start: 192.168.1.201
  • Range End: 192.168.1.251
  • Router (Gateway): 192.168.1.1
  • Lease Time: 24 hours

Critical: Disable DHCP on your router before enabling in Pi-hole to avoid conflicts.

Verifying Pi-hole is Working

Test DNS resolution:

# Should resolve to Pi-hole IP
nslookup pi.hole

# Test ad domain (should return 0.0.0.0)
nslookup ads.google.com

# Test legitimate domain
nslookup google.com

Check query log in web interface:

Admin Panel → Query Log

You should see your test queries appearing in real-time.

Setting Your Timezone

Settings → System → Timezone

Select your timezone to ensure logs and statistics show correct times.

Creating Configuration Backups

Regular backups protect against data loss:

Settings → Teleporter → Backup

This downloads a .tar.gz file containing:

  • Blocklist configuration
  • Custom DNS records
  • DHCP reservations
  • Whitelist/blacklist entries
  • Group assignments

Store backups securely and test restoration periodically.


Managing Blocklists

Understanding Blocklist Strategy

Quality Over Quantity: Use 5-10 well-maintained lists rather than 50 random lists. Too many blocklists:

  • Slow down gravity updates
  • Increase memory usage
  • Create redundancy (same domains multiple times)
  • Raise false positive rates

Recommended Approach: Start with basic lists and add more based on your specific needs.

Essential Blocklists (Start Here)

1. Steven Black’s Unified Hosts

https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
  • ~200,000 domains
  • Combines multiple reputable sources
  • Excellent balance of coverage and accuracy
  • Updated regularly

2. AdGuard DNS Filter

https://v.firebog.net/hosts/AdguardDNS.txt
  • High-quality ad blocking
  • Minimal false positives
  • Well-tested

3. EasyList (Browser Ads)

https://v.firebog.net/hosts/Easylist.txt
  • Focuses on browser-based ads
  • Widely used in browser extensions
  • Good complement to other lists

Advanced Blocklists

4. Tracking & Telemetry

https://v.firebog.net/hosts/Easyprivacy.txt
  • Blocks tracking and analytics
  • Improves privacy significantly

5. Malware Domains

https://raw.githubusercontent.com/DandelionSprout/adfilt/master/Alternate%20versions%20Anti-Malware%20List/AntiMalwareHosts.txt
  • Known malware and phishing domains
  • Security-focused

6. OISD (Comprehensive)

https://small.oisd.nl/
  • ~1.5 million domains
  • Highly comprehensive
  • Good curation
  • Requires more resources

Adding Blocklists

Navigate to:

Admin Panel → Group Management → Adlists

Steps:

  1. Click “Add a new adlist”
  2. Paste the URL
  3. Add description (e.g., “Steven Black - Unified Hosts”)
  4. Click “Add”
  5. Repeat for additional lists
  6. Click “Tools → Update Gravity”

Gravity update process:

[i] Neutrino emissions detected...
[✓] Downloading blocklist source files
[✓] Parsing blocklist sources
[✓] Deduplicating domains
[✓] Building gravity database
[✓] Gravity updated successfully

Managing False Positives with Allowlists

Some legitimate services get caught in blocklists. Common examples:

Microsoft Services:

www.msftncsi.com
*.msftconnecttest.com

Smart Home Devices:

*.amazon.com
*.amazonaws.com

Streaming Services:

www.youtube.com
*.googlevideo.com

Adding to Allowlist

Via Web Interface:

Admin Panel → Domains → Whitelist (Exact)

Enter domain and click “Add to Whitelist”

Via Command Line:

pihole -w domain.com

# Whitelist multiple domains
pihole -w domain1.com domain2.com domain3.com

# Whitelist with regex
pihole --regex-whitelist '.*.example.com$'

Advanced: Regex Filtering

Regex allows pattern-based blocking/allowing:

Common Regex Patterns

Block all subdomains of a domain:

^(.+.)?adnetwork.com$

Block multiple similar domains:

^(ads|tracking|telemetry).example.com$

Block domains containing specific keywords:

.*analytics.*

Allow exceptions to broad rules:

^(?!important).*.ads.com$

Add regex filters:

Admin Panel → Domains → Regex Filter

Group Management (Advanced)

Create different filtering profiles for different devices:

Admin Panel → Group Management → Groups

Example Setup:

Group: Kids Devices

  • Strict ad blocking
  • Social media blocking
  • Adult content blocking
  • Assigned to: Tablets, game consoles

Group: Work Devices

  • Basic ad blocking
  • Allow business domains
  • Minimal tracking blocking
  • Assigned to: Work laptops

Group: IoT Devices

  • Telemetry blocking
  • Cloud service access
  • No social media needed
  • Assigned to: Smart TVs, cameras

Assign devices to groups via MAC address or client name.


Local DNS and DHCP Features

Local DNS Records

Pi-hole can resolve internal hostnames without querying external DNS servers.

Use Cases

  • Access homelab services by name: homeassistant.local → 192.168.1.50
  • Short aliases: nas → 192.168.1.40
  • Development environments: dev.myapp.local → 192.168.1.100

Adding A Records

Admin Panel → Local DNS → DNS Records

Example entries:

DomainIP AddressDescription
homelab.local192.168.1.100Main server
nas.local192.168.1.40NAS device
homeassistant.local192.168.1.50Home Assistant
pihole.local192.168.1.100Pi-hole itself
router.local192.168.1.1Router

Via dnsmasq configuration (advanced):

# Create custom configuration file
sudo nano /etc/dnsmasq.d/02-custom-dns.conf

Add entries:

address=/homelab.local/192.168.1.100
address=/nas.local/192.168.1.40
address=/homeassistant.local/192.168.1.50

Restart Pi-hole:

pihole restartdns

Adding CNAME Records

CNAME records create aliases pointing to existing records:

Admin Panel → Local DNS → CNAME Records

Examples:

DomainTargetResult
media.localnas.localmedia.local → 192.168.1.40
home.localhomeassistant.localhome.local → 192.168.1.50
dns.localpihole.localdns.local → 192.168.1.100

Conditional Forwarding

Enable seamless integration between Pi-hole and your router’s DNS:

Settings → DNS → Conditional Forwarding

Configuration:

  • Enable: Check
  • Local network in CIDR: 192.168.1.0/24
  • IP address of DHCP server: 192.168.1.1
  • Local domain name: lan (or your custom domain)

How it works:

Client queries: computer.lan
    ↓
Pi-hole checks local DNS records
    ↓
Not found? → Forward to router at 192.168.1.1
    ↓
Router's DHCP provides hostname
    ↓
Response returned to client

This allows router-assigned hostnames to work while Pi-hole handles ad blocking.

DHCP Server Configuration

When to Use Pi-hole DHCP

Advantages:

  • Better client identification in logs (hostnames vs IP addresses)
  • Automatic DNS registration for all clients
  • Centralized network management
  • Custom DHCP options

Disadvantages:

  • Single point of failure if Pi-hole goes down
  • Requires disabling router DHCP
  • More complex failover setup

DHCP Configuration

Settings → DHCP

Settings:

DHCP Active: ✓
DHCP Authoritative Mode: ✓

IPv4 Settings:
  Range from: 192.168.1.201
  Range to: 192.168.1.251
  Router (gateway): 192.168.1.1
  Lease time: 24 hours

DHCP Options:
  Domain: lan

Static DHCP Reservations

Reserve specific IPs for important devices:

Settings → DHCP → Static DHCP Leases

Example:

MAC AddressIP AddressHostname
aa:bb:cc:dd:ee:01192.168.1.50homeassistant
aa:bb:cc:dd:ee:02192.168.1.40nas
aa:bb:cc:dd:ee:03192.168.1.200printer

Benefits:

  • Critical services always have same IP
  • Easier firewall rule management
  • Consistent DNS resolution

Performance Monitoring and Optimization

Understanding Dashboard Metrics

Query Types

Cached Queries: Answered from Pi-hole’s memory

  • Fastest response time (1-5ms)
  • No upstream DNS query needed
  • Indicates effective caching

Blocked Queries: Matched blocklists

  • Instant response (0.0.0.0)
  • No bandwidth used
  • Ads/trackers never load

Forwarded Queries: Sent to upstream DNS

  • Response time depends on upstream (10-50ms)
  • Legitimate domains not in cache

Regex Blocked: Matched regex patterns

  • Slightly slower than exact matches
  • More flexible filtering

Key Performance Indicators

Queries Per Second (QPS)

  • Light usage: 1-10 QPS
  • Medium home: 10-50 QPS
  • Heavy usage: 50-100+ QPS

Block Percentage

  • Typical: 10-30%
  • High (>50%): May indicate overly aggressive blocking
  • Low (<5%): Consider adding more blocklists

Cache Hit Ratio

  • Good: 30-50%
  • Excellent: >50%
  • Indicates effective caching strategy

Performance Optimization

Optimizing Cache Configuration

Edit dnsmasq configuration:

sudo nano /etc/dnsmasq.d/05-pihole-custom.conf

Add optimizations:

# Increase cache size (default: 10000)
cache-size=20000

# Cache negative responses
no-negcache=false

# Set minimum TTL for caching
min-cache-ttl=300

Restart DNS:

pihole restartdns

Blocklist Optimization

Identify Underperforming Lists:

Admin Panel → Group Management → Adlists

Look at “Last updated” and note any lists that:

  • Fail to update regularly
  • Take excessive time to download
  • Have zero contribution to blocking

Consolidation Strategy:

  1. Start with 3-5 high-quality lists
  2. Monitor blocking effectiveness for 1 week
  3. Add specialized lists if needed (malware, tracking)
  4. Remove lists that contribute <1% of blocks

Check Gravity Performance:

# Measure gravity update time
time pihole -g

Target: <60 seconds for update completion

System Resource Optimization

Memory Usage:

# Check Pi-hole memory usage
docker stats pihole  # For Docker
htop  # For bare metal (filter by pihole-FTL)

Typical usage: 50-200MB RAM

CPU Usage: Pi-hole should use minimal CPU (<5%) during normal operation. High CPU indicates:

  • Excessive queries per second
  • Inefficient regex patterns
  • System resource constraints

Disk I/O:

# Monitor disk usage
df -h

# Check database size
ls -lh /etc/pihole/pihole-FTL.db

Long-Term Statistics with InfluxDB

For historical analysis beyond Pi-hole’s built-in 24-hour window:

InfluxDB Integration

Create monitoring-stack.yml:

version: '3.8'

services:
  influxdb:
    container_name: influxdb
    image: influxdb:2.7-alpine
    ports:
      - "8086:8086"
    environment:
      DOCKER_INFLUXDB_INIT_MODE: setup
      DOCKER_INFLUXDB_INIT_USERNAME: admin
      DOCKER_INFLUXDB_INIT_PASSWORD: SecurePassword123
      DOCKER_INFLUXDB_INIT_ORG: homelab
      DOCKER_INFLUXDB_INIT_BUCKET: pihole
      DOCKER_INFLUXDB_INIT_ADMIN_TOKEN: YourSuperSecretToken
    volumes:
      - influxdb-data:/var/lib/influxdb2
    networks:
      - monitoring

  pihole-exporter:
    container_name: pihole-exporter
    image: ekofr/pihole-exporter:latest
    environment:
      PIHOLE_HOSTNAME: 192.168.1.100
      PIHOLE_API_TOKEN: YourPiholeAPIToken
      INTERVAL: 30s
      PORT: 9617
    ports:
      - "9617:9617"
    networks:
      - monitoring
    depends_on:
      - influxdb

  grafana:
    container_name: grafana
    image: grafana/grafana:latest
    ports:
      - "3000:3000"
    environment:
      GF_SECURITY_ADMIN_PASSWORD: admin
      GF_INSTALL_PLUGINS: grafana-clock-panel,grafana-simple-json-datasource
    volumes:
      - grafana-data:/var/lib/grafana
    networks:
      - monitoring
    depends_on:
      - influxdb

volumes:
  influxdb-data:
  grafana-data:

networks:
  monitoring:
    driver: bridge

Deploy:

docker-compose -f monitoring-stack.yml up -d

Access Grafana at http://localhost:3000 (admin/admin)

API Usage for Metrics

Get your API token:

Settings → API → Show API token

Common API Endpoints

Summary Statistics:

curl "http://pi.hole/admin/api.php?summaryRaw&auth=YOUR_TOKEN"

Response:

{
  "dns_queries_today": 45000,
  "ads_blocked_today": 8500,
  "ads_percentage_today": 18.9,
  "unique_clients": 12,
  "unique_domains": 2300,
  "gravity_last_updated": {
    "absolute": 1703001600
  }
}

Top Blocked Domains:

curl "http://pi.hole/admin/api.php?topItems=10&auth=YOUR_TOKEN"

Client Activity:

curl "http://pi.hole/admin/api.php?getQuerySources&auth=YOUR_TOKEN"

Query Logs:

curl "http://pi.hole/admin/api.php?getAllQueries&auth=YOUR_TOKEN"

AI Automation and Advanced Features

Intelligent Blocklist Management

Use machine learning to optimize your blocklists based on actual query patterns.

Pattern Analysis Script

Create analyze_blocks.py:

#!/usr/bin/env python3
"""
Analyze Pi-hole query patterns to identify new ad/tracking domains
and suggest blocklist optimizations.
"""

import requests
import json
from collections import Counter, defaultdict
from datetime import datetime
import re

PIHOLE_API = "http://192.168.1.100/admin/api.php"
API_TOKEN = "YOUR_API_TOKEN_HERE"

def get_query_data():
    """Fetch all queries from Pi-hole API"""
    url = f"{PIHOLE_API}?getAllQueries&auth={API_TOKEN}"
    response = requests.get(url)
    return response.json()

def analyze_blocked_domains(queries):
    """Analyze blocked domains for patterns"""
    blocked_domains = []

    for query in queries.get('data', []):
        if query[4] in ['2', '3']:  # Status codes for blocked queries
            domain = query[2]
            blocked_domains.append(domain)

    # Find common patterns
    patterns = defaultdict(list)

    for domain in blocked_domains:
        parts = domain.split('.')
        if len(parts) >= 2:
            # Group by second-level domain
            base_domain = '.'.join(parts[-2:])
            patterns[base_domain].append(domain)

    return patterns

def suggest_regex_patterns(patterns, min_count=5):
    """Suggest regex patterns for common domain groups"""
    suggestions = []

    for base, domains in patterns.items():
        if len(domains) >= min_count:
            # Common subdomains
            subdomains = [d.split('.')[0] for d in domains if d.count('.') &gt; 1]
            if len(set(subdomains)) >= 3:
                suggestion = {
                    'base_domain': base,
                    'count': len(domains),
                    'regex': f'^(.+\.)?{re.escape(base)}$',
                    'example_domains': domains[:5]
                }
                suggestions.append(suggestion)

    return sorted(suggestions, key=lambda x: x['count'], reverse=True)

def identify_new_trackers(queries):
    """Identify potentially malicious domains based on query patterns"""
    suspicious = []

    for query in queries.get('data', []):
        domain = query[2]

        # Long, random-looking subdomains (potential DNS tunneling)
        if len(domain) &gt; 50:
            parts = domain.split('.')
            if any(len(part) &gt; 20 for part in parts):
                suspicious.append({
                    'domain': domain,
                    'reason': 'Suspiciously long subdomain',
                    'length': len(domain)
                })

        # Many numeric characters (possible DGA malware)
        if sum(c.isdigit() for c in domain) > len(domain) * 0.4:
            suspicious.append({
                'domain': domain,
                'reason': 'High numeric ratio (possible DGA)',
                'numeric_ratio': sum(c.isdigit() for c in domain) / len(domain)
            })

    return suspicious

def main():
    print("Fetching query data from Pi-hole...")
    queries = get_query_data()

    print("\n=== Analyzing blocked domains ===")
    patterns = analyze_blocked_domains(queries)

    print(f"\nFound {len(patterns)} unique base domains")

    print("\n=== Regex Pattern Suggestions ===")
    suggestions = suggest_regex_patterns(patterns)

    for i, suggestion in enumerate(suggestions[:10], 1):
        print(f"\n{i}. Base Domain: {suggestion['base_domain']}")
        print(f"   Blocked Count: {suggestion['count']}")
        print(f"   Suggested Regex: {suggestion['regex']}")
        print(f"   Example Domains:")
        for domain in suggestion['example_domains']:
            print(f"     - {domain}")

    print("\n=== Suspicious Domain Detection ===")
    suspicious = identify_new_trackers(queries)

    if suspicious:
        print(f"\nFound {len(suspicious)} potentially suspicious domains:")
        for item in suspicious[:10]:
            print(f"\n  Domain: {item['domain']}")
            print(f"  Reason: {item['reason']}")
    else:
        print("\nNo suspicious domains detected.")

    # Generate report
    report = {
        'timestamp': datetime.now().isoformat(),
        'total_patterns': len(patterns),
        'regex_suggestions': len(suggestions),
        'suspicious_domains': len(suspicious),
        'recommendations': []
    }

    # Add recommendations
    if len(suggestions) &gt; 5:
        report['recommendations'].append(
            "Consider adding regex patterns for top 5 domain groups"
        )

    if len(suspicious) &gt; 0:
        report['recommendations'].append(
            f"Investigate {len(suspicious)} suspicious domains for potential threats"
        )

    # Save report
    with open('pihole_analysis_report.json', 'w') as f:
        json.dump(report, f, indent=2)

    print(f"\n\nReport saved to pihole_analysis_report.json")

if __name__ == "__main__":
    main()

Usage:

# Install dependencies
pip3 install requests

# Run analysis
python3 analyze_blocks.py

Automated Threat Detection

Monitor for DNS-based attacks and anomalies:

DNS Anomaly Detector

Create dns_monitor.py:

#!/usr/bin/env python3
"""
Real-time DNS anomaly detection for Pi-hole
Detects DNS tunneling, DGA malware, and suspicious query patterns
"""

import requests
import time
import json
from collections import defaultdict, deque
from datetime import datetime
import statistics

class DNSMonitor:
    def __init__(self, pihole_ip, api_token):
        self.pihole_ip = pihole_ip
        self.api_token = api_token
        self.api_url = f"http://{pihole_ip}/admin/api.php"
        self.query_history = deque(maxlen=1000)
        self.domain_stats = defaultdict(lambda: {'count': 0, 'first_seen': None})

    def fetch_recent_queries(self):
        """Fetch recent queries from Pi-hole"""
        url = f"{self.api_url}?getAllQueries&auth={self.api_token}"
        try:
            response = requests.get(url, timeout=10)
            return response.json()
        except Exception as e:
            print(f"Error fetching queries: {e}")
            return None

    def detect_dns_tunneling(self, domain):
        """Detect potential DNS tunneling attempts"""
        # Characteristics of DNS tunneling:
        # 1. Very long domain names
        # 2. High entropy (random-looking)
        # 3. Frequent queries to same base domain

        if len(domain) &gt; 100:
            return True, "Extremely long domain name"

        # Check for high entropy in subdomains
        parts = domain.split('.')
        for part in parts[:-2]:  # Exclude TLD and base domain
            if len(part) &gt; 30:
                # Check if subdomain looks random
                unique_chars = len(set(part))
                if unique_chars > len(part) * 0.6:
                    return True, f"High entropy subdomain: {part}"

        return False, None

    def detect_dga(self, domain):
        """Detect Domain Generation Algorithm (DGA) malware"""
        # DGA domains typically have:
        # 1. High ratio of consonants to vowels
        # 2. Random-looking character sequences
        # 3. Short to medium length

        parts = domain.split('.')
        if len(parts) &lt; 2:
            return False, None

        subdomain = parts[0]

        # Check length
        if not (8 <= len(subdomain) <= 20):
            return False, None

        # Calculate consonant/vowel ratio
        vowels = set('aeiou')
        vowel_count = sum(1 for c in subdomain if c.lower() in vowels)
        consonant_count = sum(1 for c in subdomain if c.isalpha() and c.lower() not in vowels)

        if consonant_count &gt; 0:
            ratio = vowel_count / consonant_count
            if ratio &lt; 0.3:  # Very few vowels
                return True, f"Suspicious consonant/vowel ratio: {ratio:.2f}"

        # Check for numeric characters
        digit_count = sum(1 for c in subdomain if c.isdigit())
        if digit_count > len(subdomain) * 0.4:
            return True, f"High numeric content: {digit_count}/{len(subdomain)}"

        return False, None

    def detect_high_frequency(self, domain):
        """Detect unusually high query frequency"""
        stats = self.domain_stats[domain]

        # Update stats
        stats['count'] += 1
        if stats['first_seen'] is None:
            stats['first_seen'] = datetime.now()

        # Calculate queries per minute
        time_delta = (datetime.now() - stats['first_seen']).total_seconds()
        if time_delta &gt; 0:
            qpm = stats['count'] / (time_delta / 60)
            if qpm &gt; 100:  # More than 100 queries per minute
                return True, f"High frequency: {qpm:.1f} queries/minute"

        return False, None

    def analyze_queries(self, queries):
        """Analyze queries for threats"""
        threats = []

        if not queries or 'data' not in queries:
            return threats

        for query in queries.get('data', []):
            if len(query) &lt; 3:
                continue

            domain = query[2]
            timestamp = query[0]

            # Run detection algorithms
            is_tunneling, tunnel_reason = self.detect_dns_tunneling(domain)
            is_dga, dga_reason = self.detect_dga(domain)
            is_high_freq, freq_reason = self.detect_high_frequency(domain)

            threat = None

            if is_tunneling:
                threat = {
                    'domain': domain,
                    'type': 'DNS Tunneling',
                    'reason': tunnel_reason,
                    'timestamp': timestamp,
                    'severity': 'HIGH'
                }
            elif is_dga:
                threat = {
                    'domain': domain,
                    'type': 'DGA Malware',
                    'reason': dga_reason,
                    'timestamp': timestamp,
                    'severity': 'HIGH'
                }
            elif is_high_freq:
                threat = {
                    'domain': domain,
                    'type': 'High Frequency',
                    'reason': freq_reason,
                    'timestamp': timestamp,
                    'severity': 'MEDIUM'
                }

            if threat:
                threats.append(threat)

        return threats

    def monitor(self, interval=60):
        """Continuous monitoring loop"""
        print(f"Starting DNS monitoring on {self.pihole_ip}")
        print(f"Checking every {interval} seconds...")

        while True:
            try:
                queries = self.fetch_recent_queries()
                threats = self.analyze_queries(queries)

                if threats:
                    print(f"\n[{datetime.now()}] THREATS DETECTED:")
                    for threat in threats:
                        print(f"\n  ⚠️  {threat['type']} - {threat['severity']}")
                        print(f"      Domain: {threat['domain']}")
                        print(f"      Reason: {threat['reason']}")

                    # Save to log file
                    with open('dns_threats.log', 'a') as f:
                        for threat in threats:
                            f.write(json.dumps(threat) + '\n')
                else:
                    print(f"[{datetime.now()}] No threats detected")

                time.sleep(interval)

            except KeyboardInterrupt:
                print("\nMonitoring stopped by user")
                break
            except Exception as e:
                print(f"Error during monitoring: {e}")
                time.sleep(interval)

def main():
    # Configuration
    PIHOLE_IP = "192.168.1.100"
    API_TOKEN = "YOUR_API_TOKEN_HERE"
    CHECK_INTERVAL = 60  # seconds

    monitor = DNSMonitor(PIHOLE_IP, API_TOKEN)
    monitor.monitor(interval=CHECK_INTERVAL)

if __name__ == "__main__":
    main()

Run as a service:

# Make executable
chmod +x dns_monitor.py

# Run in background
nohup python3 dns_monitor.py &

# Or create systemd service
sudo nano /etc/systemd/system/dns-monitor.service

Service file:

[Unit]
Description=Pi-hole DNS Anomaly Monitor
After=network.target

[Service]
Type=simple
User=pihole
WorkingDirectory=/opt/dns-monitor
ExecStart=/usr/bin/python3 /opt/dns-monitor/dns_monitor.py
Restart=always

[Install]
WantedBy=multi-user.target

Enable and start:

sudo systemctl enable dns-monitor
sudo systemctl start dns-monitor

Automated Allowlist Management

Automatically manage allowlists based on user behavior:

Create auto_allowlist.py:

#!/usr/bin/env python3
"""
Automatically manage Pi-hole allowlists based on query patterns
Suggests domains to allowlist when blocking causes issues
"""

import requests
import json
from collections import Counter
from datetime import datetime, timedelta

class AutoAllowlist:
    def __init__(self, pihole_ip, api_token):
        self.pihole_ip = pihole_ip
        self.api_token = api_token
        self.api_url = f"http://{pihole_ip}/admin/api.php"

    def get_blocked_queries(self, hours=24):
        """Get all blocked queries from last N hours"""
        url = f"{self.api_url}?getAllQueries&auth={self.api_token}"
        response = requests.get(url)
        data = response.json()

        blocked = []
        cutoff = datetime.now() - timedelta(hours=hours)

        for query in data.get('data', []):
            if query[4] in ['2', '3']:  # Blocked statuses
                timestamp = datetime.fromtimestamp(query[0])
                if timestamp > cutoff:
                    blocked.append({
                        'domain': query[2],
                        'client': query[3],
                        'timestamp': timestamp
                    })

        return blocked

    def suggest_allowlist(self, blocked_queries, min_frequency=10):
        """Suggest domains that might need allowlisting"""
        # Count blocked domains
        domain_counts = Counter(q['domain'] for q in blocked_queries)

        suggestions = []

        for domain, count in domain_counts.most_common(50):
            # Check if domain is frequently blocked
            if count >= min_frequency:
                # Check if it's from multiple clients
                clients = set(q['client'] for q in blocked_queries if q['domain'] == domain)

                if len(clients) &gt; 1:  # Blocked by multiple devices
                    suggestion = {
                        'domain': domain,
                        'block_count': count,
                        'affected_clients': len(clients),
                        'reason': 'Frequently blocked by multiple clients',
                        'priority': 'HIGH' if count &gt; 50 else 'MEDIUM'
                    }
                    suggestions.append(suggestion)

        return suggestions

    def check_known_services(self, domain):
        """Check if domain belongs to known legitimate services"""
        known_services = {
            'microsoft.com': 'Microsoft Services',
            'msftncsi.com': 'Windows Network Connectivity Check',
            'apple.com': 'Apple Services',
            'icloud.com': 'iCloud',
            'amazon.com': 'Amazon Services',
            'amazonaws.com': 'AWS',
            'googlevideo.com': 'YouTube',
            'gvt1.com': 'Google Services',
            'cloudfront.net': 'CloudFront CDN'
        }

        for service_domain, service_name in known_services.items():
            if service_domain in domain:
                return True, service_name

        return False, None

    def auto_allowlist(self, domain, dry_run=True):
        """Automatically add domain to allowlist"""
        if dry_run:
            print(f"[DRY RUN] Would allowlist: {domain}")
            return

        url = f"{self.api_url}?list=white&add={domain}&auth={self.api_token}"
        response = requests.get(url)

        if response.status_code == 200:
            print(f"✓ Added to allowlist: {domain}")
        else:
            print(f"✗ Failed to allowlist: {domain}")

    def generate_report(self):
        """Generate allowlist recommendation report"""
        print("Analyzing blocked queries...")
        blocked = self.get_blocked_queries(hours=24)

        print(f"\nTotal blocked queries in last 24h: {len(blocked)}")

        suggestions = self.suggest_allowlist(blocked)

        print(f"\n=== Allowlist Recommendations ===\n")

        for i, suggestion in enumerate(suggestions, 1):
            print(f"{i}. {suggestion['domain']}")
            print(f"   Priority: {suggestion['priority']}")
            print(f"   Blocked: {suggestion['block_count']} times")
            print(f"   Clients affected: {suggestion['affected_clients']}")

            # Check if it's a known service
            is_known, service_name = self.check_known_services(suggestion['domain'])
            if is_known:
                print(f"   ℹ️  Known service: {service_name}")
                print(f"   ✓ SAFE TO ALLOWLIST")

            print()

        return suggestions

def main():
    PIHOLE_IP = "192.168.1.100"
    API_TOKEN = "YOUR_API_TOKEN_HERE"

    manager = AutoAllowlist(PIHOLE_IP, API_TOKEN)
    suggestions = manager.generate_report()

    # Optionally auto-allowlist known services
    print("\n=== Auto-Allowlisting Known Services ===\n")
    for suggestion in suggestions:
        is_known, service_name = manager.check_known_services(suggestion['domain'])
        if is_known and suggestion['block_count'] &gt; 20:
            manager.auto_allowlist(suggestion['domain'], dry_run=False)

if __name__ == "__main__":
    main()

Integration with Home Assistant

Automate Pi-hole management through Home Assistant:

Add to configuration.yaml:

# Pi-hole Integration
rest:
  - resource: "http://192.168.1.100/admin/api.php?summaryRaw&auth=YOUR_TOKEN"
    scan_interval: 60
    sensor:
      - name: "Pi-hole Total Queries"
        value_template: "[[ value_json.dns_queries_today ]]"
        unit_of_measurement: "queries"

      - name: "Pi-hole Blocked Queries"
        value_template: "[[ value_json.ads_blocked_today ]]"
        unit_of_measurement: "queries"

      - name: "Pi-hole Block Percentage"
        value_template: "[[ value_json.ads_percentage_today | round(1) ]]"
        unit_of_measurement: "%"

      - name: "Pi-hole Unique Clients"
        value_template: "[[ value_json.unique_clients ]]"

      - name: "Pi-hole Status"
        value_template: "[[ value_json.status ]]"

# Automation: Alert if blocking fails
automation:
  - alias: "Pi-hole Blocking Alert"
    trigger:
      - platform: numeric_state
        entity_id: sensor.pihole_block_percentage
        below: 5
        for:
          minutes: 10
    action:
      - service: notify.mobile_app
        data:
          title: "Pi-hole Alert"
          message: "Pi-hole blocking percentage is unusually low ([[ states('sensor.pihole_block_percentage') ]]%). Check configuration."

  - alias: "Pi-hole High Query Load"
    trigger:
      - platform: numeric_state
        entity_id: sensor.pihole_total_queries
        above: 100000
    action:
      - service: notify.mobile_app
        data:
          title: "Pi-hole Performance"
          message: "Pi-hole has processed [[ states('sensor.pihole_total_queries') ]] queries today. Monitor performance."

# Switch to enable/disable Pi-hole
switch:
  - platform: rest
    resource: "http://192.168.1.100/admin/api.php"
    name: "Pi-hole"
    body_on: "auth=YOUR_TOKEN&enable"
    body_off: "auth=YOUR_TOKEN&disable=300"
    is_on_template: "[[ value_json.status == 'enabled' ]]"

Troubleshooting

Common Issues and Solutions

Issue: Websites Not Loading

Symptoms:

  • Specific websites fail to load
  • “Server not found” errors
  • Partial page rendering (layout loads but images/videos don’t)

Diagnosis:

  1. Check Query Log:
    Admin Panel → Tools → Query Log
  2. Search for the problematic domain
  3. Note if it shows as “Blocked” (red)
  4. Identify which blocklist is blocking it

Solutions:

Temporary disable Pi-hole:

pihole disable 5m  # Disable for 5 minutes

If site works with Pi-hole disabled, it’s a false positive.

Add to allowlist:

Admin Panel → Whitelist → Add domain

Common domains to allowlist:

# Microsoft services
www.msftncsi.com
*.microsoft.com

# Amazon/AWS
*.amazon.com
*.amazonaws.com

# Google services
*.googlevideo.com
*.gstatic.com

# Apple
*.apple.com
*.icloud.com

Issue: Pi-hole Not Blocking Ads

Symptoms:

  • Ads still visible in browsers
  • Low block percentage (<5%)
  • Dashboard shows few blocked queries

Diagnosis:

Check if devices are using Pi-hole:

# Windows
nslookup pi.hole

# Linux/Mac
dig pi.hole

# Should show Pi-hole's IP address

Check router DHCP settings:

  • Verify DNS is set to Pi-hole IP
  • Ensure no secondary DNS bypasses Pi-hole

Solutions:

Force DNS on router:

  1. Router admin → DHCP settings
  2. Primary DNS: Pi-hole IP
  3. Secondary DNS: Leave blank (or use another Pi-hole)
  4. Save and reboot router

Flush DNS cache on clients:

# Windows
ipconfig /flushdns
ipconfig /release
ipconfig /renew

# Linux
sudo systemd-resolve --flush-caches

# macOS
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

Add more blocklists:

  • Ensure you have at least 3-5 quality blocklists
  • Run “Update Gravity” after adding lists

Issue: Slow DNS Resolution

Symptoms:

  • Websites take long to start loading
  • Frequent timeouts
  • High query response times

Diagnosis:

Check upstream DNS performance:

# Test upstream DNS
dig @1.1.1.1 google.com
dig @8.8.8.8 google.com

# Compare to Pi-hole
dig @192.168.1.100 google.com

Check Pi-hole resource usage:

# Docker
docker stats pihole

# Bare metal
htop  # Look for pihole-FTL process

Solutions:

Change upstream DNS:

Settings → DNS → Upstream DNS Servers

Try different providers:

  • Cloudflare: 1.1.1.1, 1.0.0.1
  • Google: 8.8.8.8, 8.8.4.4
  • Quad9: 9.9.9.9
  • OpenDNS: 208.67.222.222, 208.67.220.220

Reduce blocklists:

  • Remove slowest-updating lists
  • Keep only 5-10 high-quality lists

Increase cache size:

sudo nano /etc/dnsmasq.d/05-pihole-custom.conf

Add:

cache-size=20000

Restart:

pihole restartdns

Issue: Can’t Access Web Interface

Symptoms:

Diagnosis:

Check if service is running:

# Docker
docker ps | grep pihole

# Bare metal
sudo systemctl status lighttpd
sudo systemctl status pihole-FTL

Check port 80:

sudo netstat -tulpn | grep :80

Solutions:

Restart services:

# Docker
docker restart pihole

# Bare metal
sudo systemctl restart lighttpd
sudo systemctl restart pihole-FTL

Access by IP:

http://192.168.1.100/admin

Check firewall:

# Ubuntu/Debian
sudo ufw status
sudo ufw allow 80/tcp

# CentOS/RHEL
sudo firewall-cmd --add-port=80/tcp --permanent
sudo firewall-cmd --reload

Issue: Pi-hole Service Crashes

Symptoms:

  • DNS resolution fails intermittently
  • Service restarts frequently
  • High CPU or memory usage

Diagnosis:

Check logs:

# Docker
docker logs pihole --tail 100

# Bare metal
sudo journalctl -u pihole-FTL -n 100

Check system resources:

free -h  # Memory
df -h    # Disk space
top      # CPU usage

Solutions:

Increase memory:

  • For Docker: Update docker-compose.yml with memory limits
  • For VM: Allocate more RAM
  • For bare metal: Add swap space
# Add 2GB swap
sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile

Reduce blocklists:

  • Remove lists you don’t need
  • Consolidate similar lists

Clean database:

pihole -g  # Rebuild gravity
pihole flush  # Clear query logs

Issue: DHCP Conflicts

Symptoms:

  • Devices can’t get IP addresses
  • “169.254.x.x” addresses assigned
  • Network intermittently unavailable

Diagnosis:

Check for multiple DHCP servers:

# On a client
sudo nmap --script broadcast-dhcp-discover

Solutions:

Disable router DHCP:

  • Access router admin interface
  • Find DHCP settings
  • Disable DHCP server
  • Save and reboot router

Disable Pi-hole DHCP temporarily:

Settings → DHCP → Uncheck "DHCP server enabled"

Use router DHCP + Pi-hole DNS:

  • Keep router DHCP enabled
  • Configure router to push Pi-hole IP as DNS
  • Disable Pi-hole DHCP

Emergency Recovery

If Pi-hole is completely broken:

Option 1: Restore from backup

Settings → Teleporter → Restore

Upload your backup file and restart services.

Option 2: Use public DNS temporarily

On router:

  • Primary DNS: 1.1.1.1
  • Secondary DNS: 8.8.8.8

This bypasses Pi-hole while you troubleshoot.

Option 3: Reinstall

# Docker
docker-compose down
docker volume rm pihole_etc-pihole
docker-compose up -d

# Bare metal
pihole uninstall
curl -sSL https://install.pi-hole.net | bash

Integration with Homelab Services

Pi-hole + Unbound (Recursive DNS)

For maximum privacy, use Unbound to resolve DNS queries recursively without relying on third-party DNS providers.

Architecture

Client Device
    ↓
Pi-hole (DNS Sinkhole + Ad Blocking)
    ↓
Unbound (Recursive DNS Resolver)
    ↓
Root DNS Servers (authoritative)

See the Docker deployment section for Unbound integration.

Pi-hole + Nginx Proxy Manager

Access Pi-hole web interface via custom domain with HTTPS:

Nginx Proxy Manager Configuration:

  1. Add Proxy Host:

    • Domain: pihole.yourdomain.com
    • Scheme: http
    • Forward Hostname: 192.168.1.100
    • Forward Port: 80
  2. Enable SSL:

    • Request Let’s Encrypt certificate
    • Force SSL
  3. Add custom Pi-hole DNS record:

    Local DNS → DNS Records
    pihole.yourdomain.com → 192.168.1.100

Pi-hole + AdGuard Home (Redundancy)

Run both for failover:

Router Configuration:

  • Primary DNS: Pi-hole (192.168.1.100)
  • Secondary DNS: AdGuard Home (192.168.1.101)

If Pi-hole fails, AdGuard Home provides continued ad blocking.

Pi-hole + VPN (Wireguard/OpenVPN)

Access Pi-hole DNS while away from home:

Wireguard Server Configuration:

Add to wg0.conf:

[Interface]
Address = 10.0.0.1/24
DNS = 192.168.1.100  # Pi-hole IP

PostUp = iptables -A FORWARD -i %i -j ACCEPT
PostDown = iptables -D FORWARD -i %i -j ACCEPT

Clients connected to VPN will use Pi-hole for DNS.


Best Practices

Security Hardening

Change Default Password:

pihole -a -p

Use a strong password (16+ characters, mixed case, numbers, symbols).

Enable HTTPS:

sudo apt install certbot
sudo certbot --apache -d pihole.yourdomain.com

Restrict Web Interface Access:

Edit lighttpd configuration:

sudo nano /etc/lighttpd/lighttpd.conf

Add IP restriction:

$HTTP["remoteip"] !~ "192.168.1.0/24" {
    url.access-deny = ( "" )
}

Disable Query Logging for Privacy:

Settings → Privacy → Privacy Level → Anonymous mode

Backup Strategy

Automated Backups:

Create backup script:

#!/bin/bash
# backup-pihole.sh

BACKUP_DIR="/path/to/backups"
DATE=$(date +%Y%m%d_%H%M%S)

# For Docker
docker exec pihole pihole -a -t
cp ./etc-pihole/pi-hole-*.tar.gz "$BACKUP_DIR/pihole-backup-$DATE.tar.gz"

# Keep only last 7 backups
find "$BACKUP_DIR" -name "pihole-backup-*.tar.gz" -mtime +7 -delete

Schedule with cron:

crontab -e

Add:

0 2 * * 0 /path/to/backup-pihole.sh

Monitoring and Alerts

Email Alerts for Issues:

Create monitoring script:

#!/bin/bash
# check-pihole.sh

PIHOLE_IP="192.168.1.100"
EMAIL="[email protected]"

# Check if Pi-hole is responding
if ! curl -s "http://$PIHOLE_IP/admin/api.php" > /dev/null; then
    echo "Pi-hole is not responding!" | mail -s "ALERT: Pi-hole Down" "$EMAIL"
fi

# Check block percentage
BLOCK_PCT=$(curl -s "http://$PIHOLE_IP/admin/api.php?summaryRaw" | jq -r '.ads_percentage_today')
if (( $(echo "$BLOCK_PCT &lt; 5" | bc -l) )); then
    echo "Pi-hole blocking percentage is unusually low: $BLOCK_PCT%" | mail -s "ALERT: Pi-hole Low Blocking" "$EMAIL"
fi

Maintenance Schedule

Weekly:

  • Review query log for issues
  • Check for new blocklist updates
  • Review top blocked domains

Monthly:

  • Update Pi-hole: pihole -up or docker-compose pull && docker-compose up -d
  • Review and optimize blocklists
  • Check system resources
  • Test backup restoration

Quarterly:

  • Review and update allowlist
  • Evaluate new blocklists
  • Update documentation
  • Test failover procedures

Conclusion

Pi-hole is an essential component of a privacy-focused homelab, providing network-wide ad blocking, DNS management, and valuable insights into network traffic. Through this guide, you’ve learned how to deploy Pi-hole using multiple methods, configure advanced features, optimize performance, and integrate AI-powered automation for intelligent threat detection and management.

Key Takeaways

  1. DNS-level blocking is powerful: One Pi-hole protects all devices on your network without per-device configuration
  2. Multiple deployment options: Choose Docker for ease of use, bare metal for performance, or VMs for isolation
  3. Quality over quantity: 5-10 well-maintained blocklists outperform 50 random lists
  4. Integration is key: Pi-hole works best when integrated with monitoring, automation, and other homelab services
  5. Automation enhances value: AI-powered scripts can detect threats, optimize blocklists, and reduce maintenance

Next Steps

Immediate Actions:

  • Deploy your chosen Pi-hole configuration
  • Configure network devices to use Pi-hole
  • Add 3-5 quality blocklists
  • Create your first configuration backup

Short-Term Improvements:

  • Set up Unbound for recursive DNS
  • Implement automated monitoring
  • Integrate with Home Assistant or monitoring stack
  • Create allowlist for false positives

Long-Term Goals:

  • Deploy redundant Pi-hole instance for failover
  • Implement AI-powered threat detection
  • Build comprehensive monitoring dashboards
  • Create automated maintenance workflows

Additional Resources

Explore these complementary guides on Homelabbing.io:

  • Unbound DNS Resolver: Maximum privacy with recursive DNS
  • Nginx Proxy Manager: Secure access to homelab services
  • Docker Networking: Advanced container configurations
  • Prometheus & Grafana: Comprehensive monitoring solutions
  • Home Assistant: Smart home automation and integration

Document Version: 1.0 Last Updated: December 16, 2025 Difficulty: Beginner Estimated Completion Time: 2-3 hours Maintenance Required: 15 minutes per week


Quick Reference

Essential Commands

# Docker
docker-compose up -d              # Start Pi-hole
docker-compose down               # Stop Pi-hole
docker-compose logs -f pihole     # View logs
docker restart pihole             # Restart
docker exec pihole pihole -g      # Update gravity

# Bare Metal
pihole status                     # Check status
pihole -up                        # Update Pi-hole
pihole -g                         # Update gravity
pihole -a -p                      # Change password
pihole -t                         # Tail query log
pihole restartdns                 # Restart DNS
pihole -w domain.com              # Whitelist domain
pihole -b domain.com              # Blacklist domain

# Network Testing
nslookup pi.hole                  # Test local DNS
nslookup google.com 192.168.1.100 # Test Pi-hole resolution
dig @192.168.1.100 example.com    # Detailed DNS query
ping 1.1.1.1                      # Test connectivity

Configuration Files

LocationPurpose
/etc/pihole/Main configuration
/etc/dnsmasq.d/DNS configuration
/var/log/pihole/Log files
/etc/pihole/pihole-FTL.dbGravity database
./etc-pihole/Docker volume

Default Ports

PortProtocolPurpose
53TCP/UDPDNS
80TCPWeb interface (HTTP)
443TCPWeb interface (HTTPS)
67UDPDHCP (if enabled)
4711TCPFTL API

Happy homelabbing! If you found this guide helpful, consider contributing improvements or sharing your Pi-hole setup in the community forums.