How to Set Up Pi-hole or AdGuard Home for Network-Wide Ad Blocking
Compare Pi-hole and AdGuard Home, then set up whichever fits your homelab. Full Docker Compose configs, DNS integration, and blocklist recommendations.
DNS-level ad blocking is one of the first things I’d recommend to anyone building a homelab. You install it once, point your network at it, and every device — phones, TVs, laptops, smart speakers, game consoles — stops phoning home to ad networks and tracking services. No browser extensions. No per-device configuration. It just stops.
Two tools dominate this space: Pi-hole and AdGuard Home. Both do the same core job. The differences are in how they’re configured, what the interface looks like, and what each one does better than the other.
If you already have Pi-hole running (or want a dedicated Pi-hole walkthrough), the standalone Pi-hole guide covers that in depth. This guide is for people who haven’t picked yet — or who want to understand the tradeoffs before committing. You’ll get full Docker Compose setups for both, and a clear recommendation on which to pick.
This guide uses Docker Compose. If you’re new to it, read the Docker Compose basics guide and the Getting Started guide first.
Pi-hole vs AdGuard Home: What’s Actually Different
Both tools work the same way at the network level: they act as a DNS resolver, check every query against blocklists, and return nothing for blocked domains. The differences are in everything around that core function.
| Feature | Pi-hole | AdGuard Home |
|---|---|---|
| Web UI | Functional, dated look | Modern, polished |
| DNS-over-HTTPS | Requires Cloudflared sidecar | Built-in, no extra container |
| DNS-over-TLS | No | Built-in |
| DHCP server | Yes | Yes |
| Regex blocking | No | Yes |
| Per-client settings | Basic (groups) | Full per-client overrides |
| Blocklist format | Hosts file format | Hosts + Adblock Plus format |
| Community blocklists | Very large ecosystem | Growing, slightly smaller |
| Stats/query log | Excellent, granular | Good, slightly less detail |
| Resource usage | ~50MB RAM | ~30MB RAM |
| Setup complexity | Moderate | Simpler initial config |
Both are actively maintained. Both are free and open source. Neither requires an account or phoning home.
Which Should You Pick?
Pick AdGuard Home if:
- You want the simplest setup with the most features out of the box
- You want DNS-over-HTTPS without running an extra container
- You want per-client configuration (different blocking rules for different devices)
- You’re newer to self-hosting and want a cleaner interface
Pick Pi-hole if:
- You want access to the largest blocklist ecosystem
- You want more granular query logging and statistics
- You’re already comfortable with it or have an existing setup to migrate from
- You run Technitium DNS (or another local resolver) and want Pi-hole purely as a blocklist layer
My setup uses Technitium DNS as the primary resolver for local DNS and split-DNS, with filtering handled at that layer. But if you’re not running a separate DNS server, either Pi-hole or AdGuard Home works well as your single DNS stack. AdGuard Home is where I’d start a fresh install today.
Prerequisites
- Docker and Docker Compose running on your server
- A static IP for that server (set it in your router’s DHCP reservations)
- Port 53 not in use on the host (check with
ss -tlnp | grep :53)
If you’re on Ubuntu and systemd-resolved is using port 53, disable it:
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved
sudo rm /etc/resolv.conf
echo "nameserver 1.1.1.1" | sudo tee /etc/resolv.conf
Option A: Docker Compose Setup for AdGuard Home
Create the directory:
mkdir -p /opt/adguard/work
mkdir -p /opt/adguard/conf
Create /opt/adguard/docker-compose.yml:
services:
adguardhome:
image: adguard/adguardhome:latest
container_name: adguardhome
network_mode: host
volumes:
- /opt/adguard/work:/opt/adguardhome/work
- /opt/adguard/conf:/opt/adguardhome/conf
restart: unless-stopped
network_mode: host is required for the same reason as Pi-hole — DNS runs on port 53, and host networking is the cleanest way to bind to it without jumping through port-mapping hoops.
Start it:
cd /opt/adguard
docker compose up -d
The initial setup wizard runs at http://YOUR_SERVER_IP:3000. Walk through it once — it asks for your admin credentials, your DNS upstream servers, and what interfaces to listen on. After that, the admin interface moves to port 80 (or whatever you configure). I set mine to port 3000 to avoid conflicts with other containers.
To keep it on port 3000 permanently, set the web interface port during the wizard. Or edit /opt/adguard/conf/AdGuardHome.yaml after setup:
http:
address: 0.0.0.0:3000
Then restart the container.
Configuring AdGuard Home
Upstream DNS: Settings > DNS Settings > Upstream DNS Servers. Use https://dns.cloudflare.com/dns-query for DNS-over-HTTPS. Or use 1.1.1.1 if you want plain DNS. If you’re running Technitium as your local resolver, point it here instead of the router — see the Technitium DNS guide for that configuration.
Bootstrap DNS: Required for resolving the DoH endpoint itself. Use 1.1.1.1 and 8.8.8.8.
Blocklists: Filters > DNS Blocklists > Add Blocklist. Start with:
- OISD Big:
https://big.oisd.nl - AdGuard DNS filter (included by default)
- HaGeZi Multi Pro:
https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/pro.txt
After adding lists, click “Update Filters” and check the dashboard to confirm the count jumped.
Per-client settings: This is AdGuard Home’s standout feature. Go to Settings > Client Settings and add your devices by IP or MAC. You can then assign different filtering rules per device — full blocking for most devices, looser rules for a work laptop that needs certain ad-adjacent domains, no blocking for a device that’s constantly hitting false positives.
Option B: Docker Compose Setup for Pi-hole
Create the directory:
mkdir -p /opt/pihole
Create /opt/pihole/docker-compose.yml:
services:
pihole:
image: pihole/pihole:latest
container_name: pihole
network_mode: host
environment:
TZ: "America/New_York"
WEBPASSWORD: "changeme"
PIHOLE_DNS_: "1.1.1.1;1.0.0.1"
DNSMASQ_LISTENING: "all"
volumes:
- /opt/pihole/etc-pihole:/etc/pihole
- /opt/pihole/etc-dnsmasq.d:/etc/dnsmasq.d
restart: unless-stopped
cap_add:
- NET_ADMIN
Change WEBPASSWORD to something real. Change TZ to your timezone. The PIHOLE_DNS_ value is where Pi-hole forwards non-blocked queries — Cloudflare here, but you can use any upstream resolver.
Start it:
cd /opt/pihole
docker compose up -d
Admin interface is at http://YOUR_SERVER_IP/admin.
Configuring Pi-hole
Blocklists: Group Management > Adlists. Add:
- StevenBlack Hosts:
https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts - OISD Big:
https://big.oisd.nl - HaGeZi Multi Pro:
https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/pro.txt
After adding, go to Tools > Update Gravity. Pi-hole downloads all the lists and compiles them into its database. This takes a minute or two. Watch the blocked domain count after — it typically goes from ~170K default to 400K+ after adding a few good lists.
Adding DNS-over-HTTPS to Pi-hole: Pi-hole itself doesn’t support DoH natively. If you want encrypted upstream queries, add a Cloudflared container alongside it:
services:
pihole:
image: pihole/pihole:latest
container_name: pihole
network_mode: host
environment:
TZ: "America/New_York"
WEBPASSWORD: "changeme"
PIHOLE_DNS_: "127.0.0.1#5053"
DNSMASQ_LISTENING: "all"
volumes:
- /opt/pihole/etc-pihole:/etc/pihole
- /opt/pihole/etc-dnsmasq.d:/etc/dnsmasq.d
restart: unless-stopped
cap_add:
- NET_ADMIN
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared
network_mode: host
command: proxy-dns --port 5053 --upstream https://dns.cloudflare.com/dns-query
restart: unless-stopped
This runs Cloudflared as a DNS-over-HTTPS proxy on port 5053, and Pi-hole uses it as upstream. More moving parts than AdGuard Home’s built-in DoH — which is one reason AdGuard Home has the edge for simpler setups.
Pointing Your Network at Either One
The blocker is set up. Now your devices need to actually use it.
Router DHCP (recommended)
Log into your router and find DHCP settings. Change the Primary DNS to your server’s IP. Every device on the network starts using your blocker automatically.
For secondary DNS: you can leave it blank (devices fall back to your blocker only) or set a public DNS like 1.1.1.1 as a fallback. Leaving it blank means if your blocking container goes down, DNS fails entirely — good for forcing you to notice, bad if you need reliable internet. Your call.
Common router paths:
- Unifi: Networks > [network] > DHCP > DNS Server 1
- TP-Link Omada: Network > LAN > DHCP > DNS
- pfSense/OPNsense: Services > DHCP Server > DNS Servers
- ASUS: LAN > DHCP Server > DNS and WINS Server Settings
After saving, release and renew DHCP leases on your devices (or just wait — leases refresh on their own). Check nslookup ads.google.com YOUR_SERVER_IP from a device to confirm queries are routing through.
Per-device (if your router doesn’t support it)
Set the DNS server manually on each device. In network/Wi-Fi settings, switch from Automatic/DHCP DNS to Manual and enter your server IP.
Integrating with Technitium DNS
If you’re running Technitium as your primary local resolver (which lets you use internal domain names like jellyfin.homelab.lan), you have two clean options:
Option 1: Technitium forwards to Pi-hole or AdGuard for all external queries. Technitium handles local zones. Everything else goes through your blocker first, then out to the internet. Set this in Technitium under Settings > Recursion > Use custom DNS forwarders, and enter your blocker’s IP.
Option 2: Pi-hole or AdGuard uses Technitium as upstream. The blocker handles filtering. Technitium handles local resolution for anything that passes through. This works but adds a lookup hop.
Option 1 is cleaner. The Technitium DNS guide covers the forwarder configuration in detail. The short version: in Technitium’s DNS settings, set your Pi-hole or AdGuard Home IP as the forwarder. Technitium resolves local names directly; everything external goes through your blocker first.
Either way, point your router’s DHCP DNS at Technitium (not the blocker directly). Technitium becomes the single DNS entry point for your network.
Blocklist Recommendations
Don’t overthink this. Three lists handle 95% of what you want to block:
OISD Big (https://big.oisd.nl) - Comprehensive, well-maintained, low false positives. This is the one list I’d run even if I only ran one.
HaGeZi Multi Pro (https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/pro.txt) - Aggressive tracking and ad blocking. More domains than OISD, slightly more false positives on edge cases.
StevenBlack Hosts (https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts) - Long-running community-maintained list. Solid baseline coverage.
Combined, you’ll typically land between 400K and 700K blocked domains depending on list versions. That sounds like a lot. It is. Most of them you’ll never notice because the domains are never queried — they’re background tracking infrastructure that your devices would otherwise hit silently.
What to avoid: lists with millions of domains. They bloat your gravity database, slow query resolution, and generate constant false positives. Bigger is not better here.
Troubleshooting Common Issues
Something stopped working after setup
Check the query log first. In Pi-hole, it’s under Query Log. In AdGuard Home, it’s under Query Log. Find the domain that was requested, look at whether it was blocked, and whitelist it if needed. Nine times out of ten this is what’s happening.
DNS resolution is slow or timing out
Your upstream resolver might be the issue, not the blocker. Test by temporarily setting a device to use 1.1.1.1 directly and seeing if it’s faster. If it is, check whether your upstream forwarder (Cloudflared or the direct upstream IP) is reachable from the container.
Port 53 conflict on startup
Run ss -tlnp | grep :53 on the host. If systemd-resolved is there, disable it using the commands in the Prerequisites section. If another container has it, find the conflict.
Blocklist isn’t updating
In Pi-hole, run Tools > Update Gravity manually and watch for errors. Common cause: the list URL is down or the format changed. Remove the bad list and add an alternative.
Container restarts on its own
Check logs: docker logs pihole or docker logs adguardhome. Memory pressure on low-RAM systems can cause this. Both tools run comfortably on 256MB of dedicated RAM — if you’re constrained, set a memory limit in the compose file and monitor.
The admin UI isn’t loading
For Pi-hole: make sure port 80 isn’t claimed by another container. For AdGuard Home: check what port you configured during setup. If you’re behind a reverse proxy (Nginx Proxy Manager), double-check the proxy host settings.
DNS-level blocking is one of those homelab wins that keeps paying off. Set it up once, add a couple of good blocklists, and forget about it. The dashboard is there when you’re curious about what your TV is actually doing at 2am.
For more tools to add to your stack once you have blocking in place, the best self-hosted apps roundup is a good next read.