Run Pi-hole on a VPS for cloud-wide ad-blocking
Pi-hole is not just for Raspberry Pis. Running it on a VPS gives you network-wide ad-blocking that works on cellular and away-from-home, via WireGuard.

Pi-hole is the gold-standard self-hosted ad-blocker and most setups never leave home. A VPS install plus a WireGuard tunnel turns it into a network-wide ad-blocker that follows you, works on cellular data, and protects your phone from ad-network telemetry without an in-app VPN service.
Why a VPS instead of a Pi at home
- Works on mobile data, away from home Wi-Fi.
- No double-NAT, no port-forward, no exposing your home IP.
- Doesn't go down because the cleaner unplugged the Pi.
- One Pi-hole, every device, anywhere.
Plan size
Pi-hole is featherlight. Our smallest plan handles a household: 1 vCore, 2 GB RAM, 30 GB SSD.
Install
Run the official one-liner on a fresh Ubuntu 24.04 VPS. Two things to know before you pipe a remote script into a root shell: only do this with scripts from projects you trust (you can download and read it first if you prefer), and the installer reconfigures the system resolver, taking over port 53 and disabling the systemd-resolved stub on Ubuntu.
curl -sSL https://install.pi-hole.net | bash
Choose a public DNS upstream when prompted (Cloudflare 1.1.1.1 is fine) until you set up DNS-over-HTTPS later. The v6 installer doesn't ask you for a web password; it prints a randomly generated one at the end.
Replace that generated password with your own:
sudo pihole setpassword
Lock it down
Pi-hole's DNS port (53) and admin web (80) are open after install. Don't expose either to the public internet.
Two things before you run this block. First, it blocks DNS and the admin page from everywhere, including the tunnel clients you haven't created yet; that's deliberate, and the rules that open both on the tunnel interface come later. Second, ufw enable drops everything not explicitly allowed: if your SSH runs on a non-standard port, allow OpenSSH only covers port 22, so allow your actual port first or this locks you out. Keep your current session open and confirm a fresh SSH login from a second terminal after enabling.
sudo ufw default deny incoming
sudo ufw allow OpenSSH
sudo ufw allow 51820/udp # WireGuard
sudo ufw enable
WireGuard tunnel
Follow the WireGuard guide. One difference from a generic VPN setup: each client config sets DNS to the Pi-hole's WireGuard address, not Cloudflare. And one expectation to set: that guide ends by verifying full internet access through the tunnel, but with the DNS line below your client's lookups go to the Pi-hole, which the firewall still blocks at this point. Name resolution over the tunnel starts working after the next section, so bring the tunnel up, skip the guide's final browsing test, and come back here.
In client.conf:
[Interface]
DNS = 10.66.66.1
Where 10.66.66.1 is the Pi-hole's address inside the tunnel.
Open the tunnel in the firewall
The lockdown you did earlier denies everything inbound except SSH and WireGuard, and that includes traffic arriving through the tunnel itself. Once a WireGuard packet is decrypted, it re-enters the firewall as ordinary inbound traffic on wg0, so without explicit rules your clients can't reach DNS or the admin page. Now that wg0 exists, allow it:
sudo ufw allow in on wg0 to any port 53
sudo ufw allow in on wg0 to any port 80 proto tcp
Leaving the protocol off the port 53 rule covers both UDP and TCP, which DNS needs for large responses. Both rules match only the tunnel interface, so ports 53 and 80 stay shut on the public side.
Tell Pi-hole to listen on the tunnel
A fresh install today gives you Pi-hole v6, which keeps its settings in /etc/pihole/pihole.toml. Don't edit that file by hand while FTL is running; it can overwrite your changes. Use the config CLI instead.
One dependency to respect: bind mode needs the wg0 interface to exist, so make sure the tunnel is up before you restart DNS. ip addr show wg0 should list 10.66.66.1. If wg0 is down when FTL restarts, the resolver has nothing to bind to and DNS stays down.
sudo pihole-FTL --config dns.interface wg0
sudo pihole-FTL --config dns.listeningMode BIND
sudo pihole restartdns
The same dependency applies at boot: if FTL starts before WireGuard after a reboot, DNS comes up dead. Order it explicitly with a systemd drop-in:
sudo systemctl edit pihole-FTL
Add:
[Unit]
After=wg-quick@wg0.service
Wants=wg-quick@wg0.service
Then reboot once and confirm DNS answers over the tunnel before you rely on it.
Verify Pi-hole only answers DNS on the tunnel:
ss -ulnp | grep :53
You should see 10.66.66.1:53, possibly a loopback listener, and nothing on 0.0.0.0.
DNS-over-HTTPS for upstream
Out of the box, Pi-hole queries upstream DNS in plaintext. Run cloudflared as a local DNS-over-HTTPS proxy so upstream lookups leave the box encrypted.
cloudflared is not in Ubuntu's repositories, so add Cloudflare's apt repo first:
sudo mkdir -p --mode=0755 /usr/share/keyrings
curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg | sudo tee /usr/share/keyrings/cloudflare-main.gpg >/dev/null
echo 'deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared any main' | sudo tee /etc/apt/sources.list.d/cloudflared.list
sudo apt update
sudo apt install -y cloudflared
The package ships only the binary, with no DNS proxy service. Create a service user for it:
sudo useradd -r -M -s /usr/sbin/nologin cloudflared
Create /etc/default/cloudflared:
CLOUDFLARED_OPTS=--port 5053 --upstream https://1.1.1.1/dns-query --upstream https://1.0.0.1/dns-query
Create /etc/systemd/system/cloudflared.service:
[Unit]
Description=cloudflared DNS over HTTPS proxy
Wants=network-online.target
After=network-online.target
[Service]
Type=simple
User=cloudflared
EnvironmentFile=/etc/default/cloudflared
ExecStart=/usr/bin/cloudflared proxy-dns $CLOUDFLARED_OPTS
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
With the config in place, start it:
sudo systemctl daemon-reload
sudo systemctl enable --now cloudflared
Confirm something is actually listening on port 5053:
ss -ulnp | grep 5053
You should see cloudflared on 127.0.0.1:5053.
You're about to make this proxy Pi-hole's only upstream. If nothing is listening on 5053 when you flip it, every device on the tunnel loses DNS instantly, so don't proceed until the check above shows the listener. Then, in the Pi-hole admin UI (reachable only over the tunnel, at http://10.66.66.1/admin from a WireGuard-connected device), go to Settings, then DNS, untick the public upstreams and set a custom Upstream DNS of 127.0.0.1#5053 only.
Block lists
The default lists are conservative. In the admin UI (over the tunnel at http://10.66.66.1/admin), go to Lists, paste in each URL, and click Add:
https://big.oisd.nl/(broadest single-list, well-maintained)https://github.com/StevenBlack/hosts/raw/master/hosts(classic, multiple variants)
New lists do nothing until gravity rebuilds the blocking database, so finish with:
pihole -g
Don't pile on thirty lists. More lists = more false positives = more time spent allowlisting.
Connect your phone
Install the WireGuard app, scan the client QR. Toggle the tunnel. Verify ads disappear in Safari/Chrome. Check http://10.66.66.1/admin (over the tunnel) for live query stats.
That's it. Cloud-wide ad blocking that follows your devices, with no commercial DNS service in the middle.
Continue reading