Building a Pi-hole + Unbound DNS Stack

· 4 min read
pihole dns homelab privacy

Prerequisites

  • Raspberry Pi or Linux server (anything that can stay on without your partner asking “what’s that humming noise?”)
  • Access to your router’s DNS settings
  • Docker installed and working
  • A cup of tea. Non-negotiable.
  • The willingness to explain to your family why “the internet is broken” for ten minutes while you fiddle with DNS
  • A mass spectrometer (not strictly required, but you never know)

What We’re Building

Every device on your network. Every phone, tablet, smart TV, that weird IoT lightbulb you bought at 2am. All of them, ad-free. No browser extensions, no per-device configuration, just silence where the ads used to be.

We’re deploying Pi-hole as a network-wide ad blocker, backed by Unbound as a recursive DNS resolver. That means your DNS queries go straight to the root nameservers instead of being funnelled through Google or Cloudflare. Better privacy, fewer ads, and the smug satisfaction of knowing your DNS stack is better than most people’s entire home network.

The Approach

  1. Deploy Pi-hole for ad blocking
  2. Deploy Unbound for recursive DNS
  3. Connect Pi-hole to Unbound
  4. Configure the network to use Pi-hole

Straightforward enough. Lets get into it.

Step 1: Docker Compose Setup

This is the core of the whole thing. One compose file, two services, zero excuses.

services:
  pihole:
    image: pihole/pihole:latest
    ports:
      - "53:53/tcp"
      - "53:53/udp"
      - "80:80/tcp"
    environment:
      TZ: 'Europe/London'
      WEBPASSWORD: 'your-password'
      PIHOLE_DNS_: '172.20.0.2#5335'
    volumes:
      - ./etc-pihole:/etc/pihole
      - ./etc-dnsmasq.d:/etc/dnsmasq.d
    networks:
      dns:
        ipv4_address: 172.20.0.3
    restart: unless-stopped

  unbound:
    image: mvance/unbound:latest
    volumes:
      - ./unbound:/opt/unbound/etc/unbound
    networks:
      dns:
        ipv4_address: 172.20.0.2
    restart: unless-stopped

networks:
  dns:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/24

The key bit here is PIHOLE_DNS_: '172.20.0.2#5335', which tells Pi-hole to forward all its queries to Unbound on port 5335 instead of some third-party resolver. Static IPs on a custom bridge network keep everything predictable. No DNS roulette.

Step 2: Configure Unbound

Create unbound/unbound.conf:

server:
    verbosity: 0
    interface: 0.0.0.0
    port: 5335
    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    do-ip6: no

    # Security
    hide-identity: yes
    hide-version: yes
    harden-glue: yes
    harden-dnssec-stripped: yes
    use-caps-for-id: no

    # Performance
    num-threads: 2
    msg-cache-slabs: 4
    rrset-cache-slabs: 4
    infra-cache-slabs: 4
    key-cache-slabs: 4

    # Privacy
    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

The hide-identity and hide-version settings stop Unbound from advertising itself to anyone who asks. The hardening options protect against some common DNS attacks. The cache slabs should match a power of two close to your thread count, which keeps things performant even on a humble Raspberry Pi.

Step 3: Start the Stack

docker compose up -d

Now test Unbound directly to make sure its actually resolving:

dig @172.20.0.2 -p 5335 google.com

You should get an A record back. If you dont, check that your Unbound config is mounted correctly and the container is healthy.

Then test Pi-hole:

dig @172.20.0.3 google.com

If both return results, you’re golden. If not, docker compose logs is your friend.

Hackerman

Step 4: Configure Router

This is where the magic happens for the whole network. Set your router’s DNS to point at Pi-hole:

  1. Log into your router admin panel, find the DHCP settings
  2. Set the primary DNS server to 192.168.1.x (whatever your Pi-hole’s IP is)
  3. Leave the secondary DNS empty, or point it at a backup

Leaving the secondary empty means if Pi-hole goes down, DNS stops. Which sounds scary but also means you’ll notice immediately and fix it, rather than silently falling back to unfiltered DNS. Your call on that one.

Alternatively, you can configure DNS per-device if you dont want the whole network affected while you’re testing.

Step 5: Add Block Lists

Pi-hole ships with a default list, but you’ll want more. Head to Pi-hole Admin, then Adlists, and add these:

https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts
https://v.firebog.net/hosts/AdguardDNS.txt
https://v.firebog.net/hosts/Easylist.txt
https://v.firebog.net/hosts/Easyprivacy.txt

Then update Gravity to pull them all in:

docker exec pihole pihole -g

Gravity is Pi-hole’s mechanism for downloading, processing, and consolidating all your block lists into one big “nope” list. After it runs, you should see a healthy number of domains on your blocklist. Somewhere around 150,000+ is a good starting point.

You get a block, you get a block

Step 6: Whitelist Essentials

Aggressive blocking will break things. It just will. Accept it, and have your whitelist ready.

docker exec pihole pihole -w s.youtube.com
docker exec pihole pihole -w i.ytimg.com

YouTube thumbnails are the classic casualty. You’ll spot others as they come up. When something stops working, check the Pi-hole query log, find the blocked domain, and whitelist it if its legitimate. You’ll do most of this in the first week, then it settles down.

The Result

  • Network-wide ad blocking across every device
  • No client configuration needed (router handles it)
  • Recursive DNS with no third-party resolver seeing your queries
  • Full query logging and statistics
  • A genuinely beautiful dashboard that you will check far too often

Chef’s Kiss

Old man yells at ads

What I’d Do Differently

Set up a secondary Pi-hole instance. DNS is critical infrastructure. If your single Pi-hole goes down, nothing on your network resolves. Nothing. Use Gravity Sync to keep your block lists and whitelists synchronised across both instances, and point your router at both.

Our household blocks about 15% of DNS queries as ads and tracking. That’s thousands of requests per day that just quietly vanish, and honestly, nobody in the house has even noticed.

Related Posts

Comments