Illustration of a shield with a checkmark surrounded by DNS query flow lines

Running AdGuard Home and Unbound on a home server

After building my Intel N100 home server, the next step was improving the network itself. The server was sitting there running 24/7, barely using any resources. Perfect for handling DNS.

My goals were straightforward:

  • Block ads and tracking across every device on the network
  • Block malicious and adult content domains
  • Stop relying on third-party DNS resolvers for privacy
  • Get better visibility into what's happening on the network

The solution I settled on was AdGuard Home for filtering, combined with Unbound for recursive DNS resolution. Together they give you a private, fast, ad-free DNS setup that you fully control.

The architecture

The DNS query flow looks like this:

Device → Router → AdGuard Home → Unbound → Root DNS servers
                   (filter)      (resolve)

Every device on the network sends DNS queries to AdGuard Home (via the router's DHCP settings). AdGuard checks the query against its blocklists. If the domain is allowed, it forwards the query to Unbound, which performs recursive resolution starting from the root DNS servers.

No Google DNS. No Cloudflare. No third party sees your queries.

AdGuard Home

AdGuard Home is a network-wide DNS filtering gateway. Think Pi-hole, but with a more polished interface and built-in support for DNS-over-HTTPS, DNS-over-TLS and DNS-over-QUIC.

Every device that uses the network DNS automatically gets:

  • Ad blocking across all apps, not just browsers
  • Tracker blocking for analytics, telemetry and fingerprinting domains
  • Malware domain blocking via regularly updated threat lists
  • Adult content filtering (configurable per-client, handy with children in the house)
  • Query logging with a clean dashboard showing top clients, blocked queries, and trends

Installation

I installed AdGuard Home directly on the host using their official script:

curl -s -S -L https://raw.githubusercontent.com/AdguardTeam/AdGuardHome/master/scripts/install.sh | sh -s -- -v

After installation, the web UI is available on port 3000 for initial setup, then moves to port 80 (or wherever you configure it).

Configuration

The key settings I changed from defaults:

  • Upstream DNS: Set to 127.0.0.1:5335 (Unbound, see below)
  • Bootstrap DNS: 9.9.9.9 (Quad9, only used to resolve filter list domains during startup)
  • Blocking mode: Default (0.0.0.0) which returns a null route for blocked domains
  • Rate limit: Disabled (it's a home network, not a public resolver)
  • Query log retention: 7 days

For blocklists, I use:

  • AdGuard DNS filter (default)
  • OISD blocklist (comprehensive, well-maintained)
  • Steven Black's unified hosts
  • A custom list for a handful of domains I want blocked specifically

Between these lists, around 15-20% of all DNS queries on the network get blocked. That's a lot of ads, trackers and telemetry that never reaches any device.

Unbound

Unbound is a validating, recursive, caching DNS resolver. Rather than forwarding queries to an upstream provider like 8.8.8.8 or 1.1.1.1, Unbound resolves domains itself by walking the DNS hierarchy from the root servers down.

Why bother with recursive resolution?

When you use a third-party resolver, they see every domain you look up. Even with DNS-over-HTTPS, you're trusting that provider with your full browsing history. With Unbound resolving recursively, no single upstream server sees the full picture. The root servers see you asking about .dev, the .dev TLD servers see you asking about martinhicks.dev, and the authoritative nameserver sees the full query. Privacy through distribution.

Installation and configuration

sudo apt install unbound

The main configuration file at /etc/unbound/unbound.conf.d/pi-hole.conf (the name is a leftover from many guides, works fine):

server:
    verbosity: 0

    interface: 127.0.0.1
    port: 5335

    do-ip4: yes
    do-udp: yes
    do-tcp: yes
    do-ip6: no

    prefer-ip6: no

    # Root hints for recursive resolution
    root-hints: "/var/lib/unbound/root.hints"

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

    use-caps-for-id: no

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

    prefetch: yes

    num-threads: 1

    so-rcvbuf: 1m

    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

The root hints file needs updating periodically (I have a cron job for this):

sudo wget -O /var/lib/unbound/root.hints https://www.internic.net/domain/named.root

Unbound listens on 127.0.0.1:5335, and AdGuard Home forwards allowed queries to it. They work together seamlessly.

Performance

Running DNS locally is fast. After the initial recursive lookup, Unbound caches the result, so subsequent queries for the same domain return in under a millisecond. Even uncached lookups typically resolve in 20-50ms.

The N100 handles this effortlessly. CPU usage from AdGuard and Unbound combined is negligible, hovering around 1-2%. Memory usage is around 80MB total for both services.

The dashboard in AdGuard Home is genuinely useful too. Being able to see which devices are making the most queries, what's being blocked, and spotting unusual patterns has been eye-opening. You quickly learn just how chatty some devices and apps are.

Router configuration

The final step is telling your router to hand out the server's IP as the DNS server via DHCP. On most routers this is somewhere in the LAN/DHCP settings. Set the primary DNS to your server's local IP and remove any secondary DNS (otherwise devices will fall back to it and bypass filtering).

If your router doesn't support custom DNS settings via DHCP, you can configure devices individually, though that's less convenient.


This is the second article in my home server series. Previously: building the N100 server. Next: using WireGuard so kids' devices stay protected on any network.