Pi-hole is a DNS server that blocks advertising, tracking and malware domains for every device that uses it, without installing anything on the devices themselves. When a client asks for a domain on one of its blocklists, Pi-hole answers with 0.0.0.0 and the request never leaves the device. In this tutorial you will install Pi-hole v6 on Ubuntu 24.04, restrict who can use it, manage blocklists and allowlists from the command line and the web interface, and use a local Unbound instance as a private recursive upstream.
Prerequisites
To follow this guide you need:
- A server running Ubuntu 24.04 LTS with a static IP address, for example a CubePath VPS. Pi-hole is light: 1 GB of RAM and 2 GB of free disk are enough.
- A non-root user with
sudoprivileges. - A way for your devices to reach the server privately: a private network, a VPN such as WireGuard, or at least a fixed public IP for your home or office that you can allow in the firewall.
- The public IP address of the network your devices use, referred to as
your_client_ipin this guide.
WarningDo not expose Pi-hole's port 53 to the whole Internet. An open DNS resolver is quickly abused for amplification attacks. Allow only your own networks, as shown in Step 2.
Step 1 - Installing Pi-hole
Pi-hole's supported installation method on Ubuntu is its installer script. Download it first so you can review it before running it with root privileges:
curl -sSL https://install.pi-hole.net -o basic-install.sh
less basic-install.sh
Run the installer:
sudo bash basic-install.sh
The installer is interactive. Answer the main prompts as follows:
- Interface: choose the interface that your clients will reach, for example the private network interface or
eth0. - Upstream DNS provider: pick any provider for now, such as Quad9. You will switch to Unbound in Step 5.
- Blocklists: accept the default list (StevenBlack's unified hosts).
- Web admin interface: install it.
- Query logging: enable it; you can change the privacy level later.
On Ubuntu 24.04 the installer disables the systemd-resolved stub listener, because both services want port 53. At the end it prints the address of the web interface and a randomly generated admin password.
Set a password you control:
sudo pihole setpassword
Check that the DNS engine, pihole-FTL, is running:
pihole status
[✓] FTL is listening on port 53
[✓] UDP (IPv4)
[✓] TCP (IPv4)
[✓] UDP (IPv6)
[✓] TCP (IPv6)
[✓] Pi-hole blocking is enabled
Step 2 - Restricting access with UFW
Pi-hole v6 includes its own web server, so the web interface and DNS are both served by pihole-FTL. Allow SSH first, then DNS and the web interface only from your trusted networks:
sudo ufw allow OpenSSH
sudo ufw allow from your_client_ip to any port 53 proto udp
sudo ufw allow from your_client_ip to any port 53 proto tcp
sudo ufw allow from your_client_ip to any port 80,443 proto tcp
sudo ufw enable
sudo ufw status
If your clients reach Pi-hole over a private network or VPN, use that range (for example 10.10.0.0/24) instead of your_client_ip.
By default Pi-hole v6 only answers queries from devices on directly connected subnets (the LOCAL listening mode). Clients on the private network are covered. If your clients arrive from elsewhere, such as your home IP or a routed VPN, switch to answering on all interfaces and let the firewall do the filtering:
sudo pihole-FTL --config dns.listeningMode ALL
Step 3 - Testing blocking and pointing clients to Pi-hole
From the server, query a well-known advertising domain and a normal domain:
dig @127.0.0.1 doubleclick.net +short
dig @127.0.0.1 cubepath.com +short
0.0.0.0
The ad domain returns 0.0.0.0, while the normal domain returns its real address. If dig is missing, install it with sudo apt install bind9-dnsutils.
Now configure your devices or your router to use the server's IP address as their only DNS server. Keep no secondary public resolver, because devices use it at random and ads will slip through. From a client, repeat the test against your_server_ip:
dig @your_server_ip doubleclick.net +short
Open the web interface at http://your_server_ip/admin and log in with the password you set. The dashboard shows total queries, the percentage blocked and the clients using Pi-hole.
Step 4 - Managing blocklists, allowlists and denylists
Pi-hole compiles all its blocklists into a single database called gravity. To add more lists, open Lists in the web interface, paste the list URL, add a comment and click Add blocklist. A good second list is the Hagezi Multi Normal list:
https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/multi.txt
After adding lists, rebuild gravity so they take effect:
sudo pihole -g
Adding many lists increases false positives without blocking much more. Two or three well maintained lists are usually enough.
When a website or app breaks, find out why a domain is blocked:
pihole -q example-blocked-domain.com
The output names the list that contains it. Allow the domain so it is never blocked:
sudo pihole allow cdn.example.com
Block a domain that is not in any list:
sudo pihole deny tracker.example.net
To block a domain together with all its subdomains, add a regular expression:
sudo pihole --regex '(^|\.)example-ads\.com$'
You can review and remove all these entries under Domains in the web interface.
Step 5 - Using Unbound as a private upstream
With the default setup, Pi-hole forwards every query that is not blocked to a public resolver, which then sees your full browsing history. Running Unbound on the same server lets Pi-hole resolve domains directly from the root servers, with DNSSEC validation, and nothing is shared with a third-party resolver.
Install Unbound:
sudo apt install unbound
Create a configuration that makes Unbound listen only on localhost, port 5335, so it does not conflict with Pi-hole on port 53:
sudo nano /etc/unbound/unbound.conf.d/pi-hole.conf
server:
interface: 127.0.0.1
port: 5335
do-ip4: yes
do-udp: yes
do-tcp: yes
do-ip6: no
harden-glue: yes
harden-dnssec-stripped: yes
use-caps-for-id: no
edns-buffer-size: 1232
prefetch: yes
num-threads: 1
# Never return private addresses for public names (DNS rebinding protection)
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
Ubuntu's Unbound package already enables DNSSEC through /etc/unbound/unbound.conf.d/root-auto-trust-anchor-file.conf, and Unbound has the root server addresses built in, so you do not need to download a root hints file.
Check the configuration and restart Unbound:
sudo unbound-checkconf && sudo systemctl restart unbound
Test Unbound directly on port 5335. The second test must fail, which proves DNSSEC validation works:
dig @127.0.0.1 -p 5335 cubepath.com +short
dig @127.0.0.1 -p 5335 dnssec-failed.org | grep status:
;; ->>HEADER<<- opcode: QUERY, status: SERVFAIL, id: 25004
Now tell Pi-hole to use Unbound as its only upstream. In the web interface open Settings > DNS, clear all the preset upstream servers, add 127.0.0.1#5335 as a custom upstream and save. Then confirm from the command line:
sudo pihole-FTL --config dns.upstreams
dig @127.0.0.1 cubepath.com +short
[ 127.0.0.1#5335 ]
Step 6 - Keeping Pi-hole up to date
Pi-hole updates its blocklists weekly through its own scheduled job. To update Pi-hole itself, run:
sudo pihole -up
Ubuntu packages, including Unbound, are updated through the normal sudo apt update && sudo apt upgrade cycle.
Troubleshooting
Ads are still shown on a device. Check that the device uses only Pi-hole as DNS, with no public secondary server, and that it does not use its own encrypted DNS (browsers such as Firefox and Chrome can enable DNS-over-HTTPS on their own). Then check the domain with pihole -q domain.
Clients time out or get no answer. Check the UFW rules from Step 2 and the listening mode. With LOCAL mode, Pi-hole ignores clients that are not on a directly connected subnet.
pihole-FTL fails to start. Read the log with sudo journalctl -u pihole-FTL -n 50 --no-pager and check that nothing else uses port 53 with sudo ss -lnup 'sport = :53'.
Every query returns SERVFAIL after switching to Unbound. Confirm Unbound is running with systemctl status unbound and that the upstream is written exactly as 127.0.0.1#5335.
The server itself cannot resolve names. After the installer disables the systemd-resolved stub, the server uses Pi-hole on 127.0.0.1. Check cat /etc/resolv.conf and pihole status.
Conclusion
You now have Pi-hole v6 on Ubuntu 24.04 blocking ads and trackers for all your devices, protected by firewall rules and resolving names privately through a local Unbound instance. Next, you can connect remote devices with WireGuard so they use Pi-hole from anywhere, create groups under Group Management to apply different lists to different clients, or compare it with AdGuard Home if you need built-in DNS-over-HTTPS and DNS-over-TLS endpoints.
