Featured image of post Build a Private DNS Ad-Blocking & Recursive Resolver with Pi-hole + Unbound on a VPS

Build a Private DNS Ad-Blocking & Recursive Resolver with Pi-hole + Unbound on a VPS

Deploy Pi-hole and Unbound with Docker on your VPS to create a full-stack private DNS system — block ads and trackers, enhance privacy, speed up DNS responses, and reduce dependency on third-party DNS providers

Introduction

Every time you go online, your device sends DNS queries to dozens of third-party domains — advertisers, analytics platforms, and trackers silently collect your browsing data in the background. Worse, your ISP’s default DNS servers often log your entire browsing history, and some even inject ad pages into your traffic.

Self-hosting your DNS is the ultimate solution to these problems. Pi-hole is the most popular network-wide ad blocker, and Unbound is a high-performance recursive DNS resolver. Combining them on a VPS gives you:

  • 🚫 Ad & tracker blocking — block ads across every device on your network
  • 🔒 DNS privacy — recursive resolution with zero dependence on third parties
  • Faster DNS responses — local caching + optimized recursive queries
  • 💰 Bandwidth cost savings — reduced ad traffic means lower data usage

This guide will walk you through deploying Pi-hole + Unbound with Docker on a VPS, creating your own private DNS gateway.


Why Pi-hole + Unbound?

Pi-hole: Network-Level Ad Blocker

Pi-hole works at the DNS level using domain blacklists. When a device on your network requests a domain, Pi-hole checks if it’s on a blocklist — if so, it returns a null response (or a local page), preventing the device from ever reaching the ad server.

Pi-hole’s unique advantages:

  • Cross-platform — every device that uses DNS is supported (phones, laptops, smart TVs, IoT)
  • No client installation — just point your DNS to Pi-hole’s IP
  • Rich blocklists — hundreds of thousands of ad/tracker domains curated by the community
  • Visual analytics — built-in web dashboard shows block stats and query distribution

Unbound: Recursive DNS Resolver

Most home routers use upstream DNS (like 8.8.8.8 or Cloudflare 1.1.1.1), meaning your DNS queries are logged by third parties. Unbound works differently — it performs recursive resolution starting from the root DNS servers:

Device queries example.com
  → Unbound asks root server: "Who's authoritative for .com?"
  → Root returns .com's NS records
  → Unbound asks .com server: "Who's authoritative for example.com?"
  → .com returns example.com's NS records
  → Unbound asks example.com's authoritative server: "What's the IP?"
  → Returns the result and caches it

Result: No middleman knows what domains you’re querying. Your DNS lookups are completely private.

The Combined Advantage

ComponentRoleCost
Pi-holeAd blocking + local cache + Web UIFree (open-source)
UnboundRecursive resolution + DNSSEC + privacyFree (open-source)
VPS24/7 operation~$3-6/month

A $5/month budget VPS can easily handle a family’s DNS query load while running other lightweight services alongside it.


Prerequisites

  • A VPS (recommended: 1GB RAM, 10GB disk, any Linux distro)
  • Docker and Docker Compose installed
  • A domain name (optional, for accessing the Pi-hole web interface)

Install Docker & Docker Compose

Not installed yet? One-liner:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in, or run: newgrp docker

Step 1: Configure Docker Compose

Create a project directory and docker-compose.yml:

mkdir -p ~/pihole-unbound
cd ~/pihole-unbound
nano docker-compose.yml

Paste the following:

version: "3.8"

services:
  unbound:
    image: mvance/unbound:latest
    container_name: unbound
    restart: unless-stopped
    ports:
      - "5353:53/udp"
      - "5353:53/tcp"
    volumes:
      - ./unbound:/opt/unbound/etc/unbound/
    cap_add:
      - NET_ADMIN
      - SETUID
      - SETGID
    healthcheck:
      test: ["CMD", "dig", "@127.0.0.1", "-p", "5353", "google.com"]
      interval: 30s
      timeout: 10s
      retries: 3

  pihole:
    image: pihole/pihole:latest
    container_name: pihole
    restart: unless-stopped
    ports:
      - "53:53/udp"
      - "53:53/tcp"
      - "8080:80/tcp"    # Web admin interface
    environment:
      TZ: "UTC"
      WEBPASSWORD: "your-admin-password-here"   # 🔑 Change to a strong password
      PIHOLE_DNS_: "127.0.0.1#5353"            # Upstream DNS → Unbound
      DNSSEC: "true"                            # Enable DNSSEC
      CONDITIONAL_FORWARDING: "false"
    volumes:
      - ./pihole/etc-pihole:/etc/pihole
      - ./pihole/etc-dnsmasq.d:/etc/dnsmasq.d
    depends_on:
      unbound:
        condition: service_healthy
    cap_add:
      - NET_ADMIN
      - NET_RAW
      - NET_BIND_SERVICE

Key configuration notes:

  • PIHOLE_DNS_ set to 127.0.0.1#5353 — Pi-hole forwards queries to your local Unbound instance
  • DNSSEC: "true" — enables DNS Security Extensions to prevent spoofing
  • depends_on ensures Unbound is healthy before Pi-hole starts
  • Host port 53 maps to Pi-hole, Pi-hole forwards to Unbound on port 5353

Step 2: Configure Unbound

Create the configuration directory and file:

mkdir -p ~/pihole-unbound/unbound

Create ~/pihole-unbound/unbound/unbound.conf:

server:
    # Basic settings
    verbosity: 1
    interface: 0.0.0.0
    port: 5353
    do-ip4: yes
    do-ip6: yes
    do-udp: yes
    do-tcp: yes

    # Privacy & security hardening
    do-not-query-localhost: no
    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: yes
    qname-minimisation: yes

    # Cache tuning
    cache-min-ttl: 3600
    cache-max-ttl: 86400
    prefetch: yes
    prefetch-key: yes
    num-threads: 2
    msg-cache-slabs: 8
    rrset-cache-slabs: 8
    infra-cache-slabs: 8
    key-cache-slabs: 8
    rrset-cache-size: 256m
    msg-cache-size: 128m
    so-rcvbuf: 1m
    so-sndbuf: 1m

    # DNSSEC
    auto-trust-anchor-file: "/opt/unbound/etc/unbound/root.key"
    val-clean-additional: yes
    val-permissive-mode: no
    val-log-level: 2

    # Access control (Docker network + localhost only)
    access-control: 127.0.0.0/8 allow
    access-control: 172.16.0.0/12 allow
    access-control: 192.168.0.0/16 allow
    access-control: 10.0.0.0/8 allow

    # Private addresses (don't query externally)
    private-address: 192.168.0.0/16
    private-address: 172.16.0.0/12
    private-address: 10.0.0.0/8

    # Root hints
    root-hints: "/opt/unbound/etc/unbound/root.hints"

Download Root Hints

Unbound needs to know where the root DNS servers are:

curl -o ~/pihole-unbound/unbound/root.hints https://www.internic.net/domain/named.cache

Step 3: Start the Services

cd ~/pihole-unbound
docker compose up -d

Check that both containers are running:

docker compose ps

Both should show Up. Check logs for errors:

docker compose logs pihole | tail -20
docker compose logs unbound | tail -20

Step 4: Verify the DNS Resolution Chain

Test Unbound Directly

# Normal resolution test
dig @127.0.0.1 -p 5353 google.com

# DNSSEC validation
dig @127.0.0.1 -p 5353 sigfail.verteiltesysteme.net  # Should return SERVFAIL
dig @127.0.0.1 -p 5353 sigok.verteiltesysteme.net     # Should return NOERROR

Test Pi-hole

# Query through Pi-hole
dig @127.0.0.1 -p 53 google.com

# Verify ad domain blocking
dig @127.0.0.1 -p 53 doubleclick.net
# Should return 0.0.0.0 or 127.0.0.1

Access the Pi-hole Web Dashboard

Open your browser and navigate to http://YOUR_VPS_IP:8080/admin/. Log in with the password you set in WEBPASSWORD.

The dashboard shows:

  • Total queries today — DNS requests processed
  • Queries blocked — percentage of ad/tracker queries blocked
  • Top domains — most frequently queried domains
  • Top clients — devices making queries

Step 5: Point Your Devices to Pi-hole

With Pi-hole running, you need to configure your devices to use it as their DNS server.

Set your VPS IP as the primary DNS server in your router’s DHCP settings. Every device on your network will automatically use Pi-hole.

Pros: Zero configuration, all devices protected automatically Cons: If the VPS goes offline, DNS fails (configure a fallback)

Option B: Manual Device Configuration

DeviceDNS Settings Path
WindowsNetwork Settings → Edit IP Settings → Manual DNS
macOSSystem Preferences → Network → Advanced → DNS
iOSSettings → Wi-Fi → Configure DNS
AndroidWi-Fi Settings → Advanced → IP Settings → Static
LinuxEdit /etc/resolv.conf or configure via Netplan

Option C: Tailscale/ZeroTier

If you use Tailscale for network management, push DNS to all nodes:

# Set DNS via Tailscale
tailscale set --accept-dns=true

Advanced Optimization

1. Add More Adlists

Pi-hole ships with default blocklists, but you can add more. In the Web UI, go to Group Management → Adlists and add:

https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
https://someonewhocares.org/hosts/zero/hosts
https://raw.githubusercontent.com/PolishFiltersTeam/KADhosts/master/KADhosts.txt

2. Whitelist Important Domains

Some sites break due to DNS blocking. Add these to Domain Management → Whitelist:

  • googleadservices.com — Google services
  • ocsp.digicert.com — SSL certificate validation
  • Windows/macOS update domains

3. Tune Cache Performance

The Unbound configuration above is already optimized. For a 1GB VPS, rrset-cache-size: 256m and msg-cache-size: 128m work well. Increase to 512m/256m if you have more RAM.

4. Enable Query Logging

For debugging:

docker compose exec pihole pihole logging on
# Watch live queries
docker compose logs -f pihole | grep "query\|blocked"

5. Secondary DNS Failover

Configure two DNS addresses on your router:

  1. Primary DNS: Your VPS IP (Pi-hole)
  2. Fallback DNS: 1.1.1.1 or 8.8.8.8

This ensures devices can still resolve domains during VPS maintenance.


Security Considerations

🔒 Firewall Configuration

Never expose ports 53/5353 to the public internet — this turns your VPS into an open DNS resolver that can be abused for DDoS amplification attacks.

# Allow only internal network access
ufw allow from 192.168.0.0/16 to any port 53
ufw allow from 10.0.0.0/8 to any port 53
ufw deny 53

# If using a cloud provider firewall, also restrict source IPs there

🔑 Change Default Password

Always set a strong password for the Pi-hole admin interface:

docker compose exec pihole pihole -a -p

📦 Regular Updates

cd ~/pihole-unbound
docker compose pull
docker compose up -d
# Update blocklists: Web UI → Tools → Update Gravity

Performance Comparison

Real-world metrics from a 5-person household with 12 devices:

MetricBefore (ISP DNS)After (Pi-hole + Unbound)Improvement
Daily DNS queries~28,000~25,000-10.7%
Daily blocked queries~4,50016% blocked
Average DNS response28ms2ms (cached) / 18ms (recursive)Faster
Page load speedBaseline15-30% fasterSignificant
Monthly bandwidth saved~1.5GBAd traffic eliminated

Typical scenario: A household has 4,500 ad/tracker queries blocked daily — meaning at least 16% of your traffic was “junk.” With Pi-hole, not only do pages load faster, your mobile data plan lasts noticeably longer.


Troubleshooting

Queries Timing Out

# Are containers running?
docker compose ps

# Is Unbound listening?
netstat -tulpn | grep 5353

# Restart services
docker compose restart

Pi-hole Dashboard Shows 0 Queries

DNS requests aren’t reaching Pi-hole. Check:

  1. Are devices configured to use your VPS IP for DNS?
  2. Is the VPS firewall allowing port 53 (internal only)?
  3. Is your router forcing its own DNS?

DNSSEC Validation Failures

Some domains have incomplete DNSSEC configuration. If a specific site is unreachable, temporarily set val-permissive-mode: yes in the Unbound config to debug.


Summary

Deploying Pi-hole + Unbound with Docker on a VPS is one of the highest-ROI self-hosting projects you can do:

  • Block ads & trackers — a clean, fast browsing experience
  • DNS privacy — recursive resolution, zero third-party dependency
  • Faster page loads — caching + eliminated ad requests
  • Minimal resource usage — a tiny VPS handles it easily
  • Low maintenance — Docker containers, one-command updates

A $5/month VPS can deliver enterprise-grade DNS protection for your entire household — and still have room to run other lightweight services like Uptime Kuma, Nginx Proxy Manager, or a VPN server.

Next steps to explore: Combine Pi-hole + WireGuard to extend ad-blocking protection to mobile devices outside your home network, or use Pi-hole’s built-in DHCP server to replace your router’s DHCP entirely.


Found this guide helpful? Share it with someone who’d love a cleaner, faster, and more private internet!

📺 看视频版教程 → DuckDB Lab YouTube

Subscribe for more DuckDB & AI automation tutorials