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.