Set up Pi-Hole for network-wide ad blocking and Unbound for recursive DNS
Besides just using a browser extension for ad blocking, I've been using Pi-Hole for years to prevent all devices on my network from getting ads, and stopping smart home devices from phoning home for telemetry and tracking. Pi-Hole will run on almost anything that can run Linux, is very easy to set up, and super effective with the right ad lists.
Sections
- Pre-Requisites and Caveats
- Installing Pi-Hole
- Installing Unbound
- Running Pi-Hole and Unbound together in Docker
- Configuring DNS
- Using adlists to block domains
- Advanced DNS settings
- Further steps
- Reference
Pre-Requisites and Caveats
Before anything, make sure the machine you’re installing Pi-Hole on has a static IP, otherwise if your machine’s IP changes it will break DNS resolution for the network.\
Also, Pi-Hole will run a web server at port 80, for serving the web UI page, so make sure no other web server like Apache or NGinx is running.
When installing Pi-Hole on Ubuntu you may get an error message along the lines of this:
Error starting userland proxy: listen tcp4 0.0.0.0:53: bind: address already in use
This is because Pi-Hole includes dnsmasq
which binds to port 53, but Ubuntu by default uses systemd-resolved
on that port. If this happens, you’ll need to disable systemd-resolved
prior to installing Pi-Hole.
sudo systemctl stop systemd-resolved
sudo systemctl disable systemd-resolved.service
Then edit the file at /etc/resolv.conf
and change nameserver 127.0.0.53
to nameserver 9.9.9.9
(or whatever public DNS you prefer) to not break DNS resolution. Afterwards use dig google.com
or ping google.com
to verify DNS is still working, then proceed with installing Pi-Hole.
Alternately, you may already have another DNS server like Unbound running on port 53. If so, it needs to be disabled for now — we will create a new configuration file for Unbound to run alongside Pi-Hole, and it will not use port 53.
If you’re running Unbound, disable it with the following command:
sudo systemctl stop unbound.service
Installing Pi-Hole
This is for installing Pi-Hole bare metal, so if you want to run it in Docker, skip to this section.
The quickest and easiest way to get Pi-Hole up and running bare metal is via their official installer. We’ll use the following command to execute it:
curl -sSL https://install.pi-hole.net | bash
Installation will prompt a number of dialogs, pay attention and make sure you input all the correct information or to the best of your knowledge. (You can change it later.)
A random password will be generated during install for logging in to the Pi-Hole web UI. You should change the admin password with
pihole -a -p newpassword
or if you don’t want to login at all, leave it blank withpihole -a -p
.
Now you should be able to access the Pi-Hole Web UI via either IP address, e.g. http://192.168.1.250/admin
or using the machine hostname, http://hostname/admin
. (Later, when Pi-Hole is set as the DNS server, you can access the web UI at http://pi.hole/admin
)
Installing Unbound
Again, this is for installing Unbound bare metal, so if you want to run it in Docker, skip to this section.
Unbound is available on most, if not all, Linux package managers and should be installed that way whenever possible. On Debian and Ubuntu, for example, you’d install with this command:
sudo apt install unbound
As per the official docs, we’ll create a configuration file located at /etc/unbound/unbound.conf.d/pi-hole.conf
with the below contents:
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
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
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
Unbound should already be running when you installed it, let’s restart it so the new config takes effect, and test that it’s working:
sudo service unbound restart
dig pi-hole.net @127.0.0.1 -p 5335
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 58337
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;pi-hole.net. IN A
;; ANSWER SECTION:
pi-hole.net. 300 IN A 3.18.136.52
;; Query time: 60 msec
;; SERVER: 127.0.0.1#5335(127.0.0.1) (UDP)
If your output looks similar to the above, then everything is working as intended.
Running Pi-Hole and Unbound together on Docker
I always run Pi-Hole bare metal as a personal preference, so I’ll be giving instructions for something I haven’t used myself. Maybe I’ll spin up some containers and test it out, but for now… let me know how it goes.
I’m going to suggest using Chris Crowe’s project for running both Pi-Hole and Unbound either in one or two containers.
This compose.yaml
should get you going as a starting point:
volumes:
etc_pihole-unbound:
etc_pihole_dnsmasq-unbound:
services:
pihole:
container_name: pihole
image: cbcrowe/pihole-unbound:latest
hostname: pihole
ports:
- 443:443/tcp
- 53:53/tcp
- 53:53/udp
- 8800:80/tcp
- 5335:5335/tcp
- 22/tcp
environment:
- FTLCONF_LOCAL_IPV4=192.168.0.100
- TZ=America/New_York
- WEBPASSWORD=changeme
- PIHOLE_DNS_=127.0.0.1#5335
- DNSSEC="true"
- DNSMASQ_LISTENING=all
volumes:
- etc_pihole-unbound:/etc/pihole:rw
- etc_pihole_dnsmasq-unbound:/etc/dnsmasq.d:rw
restart: unless-stopped
I’ve never really run Unbound in a container, even when using Pi-Hole in a container I just run Unbound bare metal. That said, you can try it out and let me know if these instructions work or not.
Here is a compose.yaml
to run Pi-Hole and Unbound containers together:
networks:
dns_net:
driver: bridge
ipam:
config:
- subnet: 10.1.1.0/16
services:
pihole:
container_name: pihole
hostname: pihole
image: pihole/pihole:latest
networks:
dns_net:
ipv4_address: 10.1.1.2
ports:
- "53:53/tcp"
- "53:53/udp"
- "67:67/udp"
- "8000:80/tcp"
- "4430:443/tcp"
environment:
- 'TZ=America/New_York'
- 'WEBPASSWORD='
- 'DNS1=10.1.1.3#53'
- 'DNS2=no'
volumes:
- './etc-pihole/:/etc/pihole/'
- './etc-dnsmasq.d/:/etc/dnsmasq.d/'
cap_add:
- NET_ADMIN
restart: unless-stopped
unbound:
container_name: unbound
image: mvance/unbound:latest
networks:
dns_net:
ipv4_address: 10.1.1.3
volumes:
- '/opt/docker/unbound:/opt/unbound/etc/unbound'
- '/opt/docker/unbound/pihole.conf:/opt/unbound/etc/unbound/pihole.conf'
ports:
- "5053:53/tcp"
- "5053:53/udp"
healthcheck:
disable: true
restart: unless-stopped
Configuring DNS
(Note: If running in Docker using the above instructions, this should already be setup, but you can double-check it.)
If running bare metal, we need to make Unbound the upstream DNS resolver for Pi-Hole. In the Pi-Hole web UI, go to Settings on the sidebar, then click on the DNS tab.
Uncheck any public DNS in the left column, and in the right column enable the checkmark under Custom 1 and add 127.0.0.1#5335
as the server. Scroll down to the bottom and click the Save.
You can also set Interface settings to Permit all origins, as long as the machine running Pi-Hole is not accessible from the internet (and especially port 53 is not exposed) then this is totally safe, and will ensure Pi-Hole receives and answers queries from every device in your network. You can try using Allow only local requests first and see if it works for you.
Next, in order for Pi-Hole to work network-wide for all devices (including phones and tablets on Wi-Fi), you’ll need to configure your router to use the Pi-Hole server as DNS. Some routers do not let you change this setting, like AT&T’s Arris BGW210-700, but most Netgear and TP-Link routers do.
If the option is available, it’s usually under DNS Servers in DHCP Settings, change whatever IP address is there to your Pi-Hole’s IP, and your router will begin handing out Pi-Hole as the network-wide DNS when clients renew their DHCP leases.
If your router does not have the option of setting a DNS server, you won’t be able to block ads for all devices on your network automatically. Instead you’ll have to configure each device’s DNS or turn off DHCP on your router (if possible) and use Pi-Hole as the DHCP server.
You should now be set up for Pi-Hole to be the DNS server for your whole network. If you added Pi-Hole’s IP address as the DNS server in your router, devices will gradually begin querying Pi-Hole as they renew their DHCP leases and get the updated DNS server.
Otherwise you add Pi-Hole’s IP manually to each device’s DNS settings. As Pi-Hole starts answering DNS requests from clients, it will build up a cache — domain resolution will start slow, but once they are cached Pi-Hole should be even faster than a public DNS.
Next, you’ll notice the Pi-Hole web UI will show all clients as IP addresses, but there’s a few methods to show hostnames instead. (Or hostname.domain
if you prefer.) The easiest way is to use Conditional Forwarding, though it seems to be an uncommon feature with most consumer-grade routers, so don’t be surprised if it doesn’t work for you.
Go to Settings on the sidebar, click on the DNS tab, and scroll down to Advanced DNS settings.
Check the box to Use Conditional Forwarding, type in your subnet (most likely 192.168.0.0/24
or 192.168.1.0/24
), type in the domain if your router uses one and you know what it is (e.g. .local
or .home
) otherwise leave it blank, and click Save. Check the dashboard and see if that’s enough to display hostnames instead of IP addresses. Doing a hard reload (Ctrl + F5) is usually enough to make them appear, but if you want to be sure, restart Pi-Hole by going to Settings -> Restart DNS Resolver on the web UI, or using the command pihole restartdns
in the terminal.
If the hostnames are not showing up, there’s something else we can try. Go back to Advanced DNS settings and you’ll see two checkmarks like in the picture below.
For the best security, both of these should be checked, but you can try unchecking one or both to see if they make the hostnames show. If the hostnames still don’t populate, it’s likely your router simply does not support conditional forwarding.
You’ll have to manually add each device’s IP address and hostname/domain. Go to Local DNS on the navigation bar, and click on DNS Records.
Alternately, you can manually edit the /etc/hosts
file on the server running Pi-Hole. You can bind an IP to a hostname, domain or any other alias.
# /etc/hosts
192.168.0.200 hostname1
192.168.0.215 hostname2
192.168.0.230 mydomain.tld
192.168.0.245 laptop
Using adlists to block domains
On the Pi-Hole web UI, click on Adlists on the navigation bar:
The most efficient way to block URLs in Pi-Hole is to use an adlist, which is a list of URLs to block en masse. (You can also blacklist individual URLs by going to Domains on the sidebar.) While installing Pi-Hole you had the option of including a default adlist that blocks around 300k URLs, but there’s many more adlists curated by the community that will block many more ads, malware sites, telemetry and tracking. Here are the ones I use:
- The Firebog
- The Block List Project
- Developer Dan’s Adlists
- 1Hosts (I suggest Lite or Pro)
- OISD
- This gist of Regex Expressions
Once you’ve added all the adlists (and any time you add additional ones), make sure to “update gravity” for the changes to take effect. Go to Tools on the navigation bar, click on Update Gravity, and click the big Update button. Do not leave the page until the process is done!
You may end up with several million “domains on adlists” as shown in the dashboard. Don’t panic. You’ll see your dashboard stats explode with blocked requests, especially from mobile devices. Pay attention to any issues you have visiting websites and using online apps/services that you commonly do, and whitelist domains as needed. (You can also use a curated whitelist.)
Advanced DNS Settings
By default the dashboard will show all clients as IP addresses, but there’s a few methods to show hostnames instead. (Or hostname.domain
) The easiest way is to use Conditional Forwarding, though it does not work with every router.
Go to Settings on the navigation bar, click on the DNS tab, and scroll down to Advanced DNS settings.
Check the box to Use Conditional Forwarding, enter your network information, and hit Save. Check the dashboard and see if that’s enough to display hostnames instead of IP addresses.
If the hostnames are not showing (sometimes it takes a minute), go back to Advanced DNS settings.
The above settings should be checked for more security, but try unchecking one or both to see if they make the hostnames show. If not, it’s possible your router does not broadcast a local domain.
You’ll have to manually add each device’s IP address and hostname/domain. Go to Local DNS on the navigation bar, and click on DNS Records.
Alternately, you can manually edit the /etc/hosts
file on the server running Pi-Hole. You can bind an IP to a hostname, domain or any other alias.
# /etc/hosts
192.168.0.200 hostname1
192.168.0.215 hostname2
192.168.0.230 mydomain.tld
192.168.0.245 laptop
After saving your changes to the file, use the following command for them to take effect.
pihole restartdns
If you’ve been following the instructions, you’re all set to block ads. Pi-Hole will be a DNS sinkhole for every device on the network, blocking many ads, tracking and telemetry domains. Domain name resolution will also be done recursively through Unbound, bypassing public DNS resolvers like Google and Cloudflare, going straight to the authoritative nameservers.
If you want to go a bit deeper and improve the Pi-Hole experience, read on below.
Further steps
Updating Pi-Hole
When an update to Pi-Hole, FTL and/or the Web Interface is available, you can easily update your bare metal Pi-Hole in the terminal by using the command pihole -up
. Pi-Hole will not update on it’s own, so you have to do it manually, and the Pi-Hole team does not recommend automating it. (Though you can, if you so choose.)
If you’d rather update Pi-Hole during off-hours, like in the middle of the night, I suggest using at
— it lets you use schedule a one-time task for a later time, similar to cron
but non-recurring. (The syntax for at
is also more human-readable than cron
.) For example, the below command will schedule pihole -up
to be executed at 5:00 AM:
pihole -up | at 5AM
Backup and/or restore Pi-Hole configuration
You may want to regularly create a backup of your Pi-Hole configuration. You can’t automate it, but that’s ok because it’s very simple — just to go the web UI, click on Settings, then go to the Teleporter tab and click the Backup button. This will download a tar.gz
file to the computer you’re accessing the web UI from, and within this same screen you can restore from a backup file if necessary. You might consider committing your backup to a private GitHub repo too.
Use local time instead of UTC
If you notice the query log displaying times as UTC instead of your local time zone, and you want the logs to use your time zone, use (for example if you want EST):
sudo timedatectl set-timezone America/New_York
If you want to find out your time zone in the tz database, see here.
Run and sync two Pi-Holes
When you make Pi-Hole your primary DNS it becomes a critical part of your network — if it goes down, devices on your network won’t be able to resolve any domains. For this reason, you may want to run another Pi-Hole as a secondary DNS in case the host running your main instance of Pi-Hole crashes. (These things happen.) If your entire network will go down from an issue with Pi-Hole, running a second instance of it makes a lot of sense. If you go this route, I strongly suggest using Gravity Sync to keep the adlists and other settings identical between the two.
Gravity Sync has been archived and is no longer updated, however it still works perfectly with the current (as of October 2024) version of Pi-Hole. In the future Pi-Hole v6 release, Gravity Sync will not work. Two alternatives that do work with v6 are Orbital Sync and Nebula Sync.
This guide will be updated in the future once Pi-Hole v6 is generally available.