Why Self-host Pi-hole on a VPS
Most Pi-hole guides assume a Raspberry Pi on your local network. A VPS completely changes the equation: your DNS blocker is reachable from any device, anywhere in the world. Configure your smartphone on the go to point to your VPS, and ads disappear even on mobile networks and public Wi-Fi.
Concrete benefits of Pi-hole on a VPS
- Universal protection: covers all your devices (PCs, phones, tablets) without per-device configuration
- Bandwidth reduction: blocked requests consume zero download bytes
- Centralized DNS logs: full visibility on domains contacted by each device
- ISP independence: your DNS no longer transits through your ISP's resolver
- IoT tracker blocking: connected devices cannot contact their telemetry servers
- Customizable: add your own rules, whitelist what you need, choose your lists
Pi-hole v5 vs Pi-hole v6: What Changed
Pi-hole v5 vs v6
Scroll the table
| Feature | Pi-hole v5 | Pi-hole v6 (2025) |
|---|---|---|
| Web interface | PHP + lighttpd | Built into FTL, no lighttpd needed |
| DNS engine | Separate dnsmasq | Unified FTL (Faster Than Light) embeds DNS |
| API | Limited API, key in URL | Full REST API with token authentication |
| Configuration | Files in /etc/pihole/* | Single TOML file pihole.toml |
| Installation | curl | bash script | curl | bash script (identical, same URL) |
| Docker | pihole/pihole image | pihole/pihole image (updated, same name) |
Pi-hole v6 came out on February 18, 2025. Upgrading from v5 goes through the usual install script run on a v5 system: Pi-hole detects the existing version, offers the upgrade and converts the configuration to TOML (/etc/pihole/pihole.toml). If you are starting fresh, install v6 directly.
Two changes matter on a VPS. First, every pihole.toml setting has an environment-variable equivalent, FTLCONF_<section>_<key>: v5's WEBPASSWORD becomes FTLCONF_webserver_api_password, and upstream servers are written as FTLCONF_dns_upstreams, separated by semicolons. A variable set this way becomes read-only in the interface: it is the source of truth. Second, blocklists are joined by subscribed allowlists (antigravity), handy to neutralise known false positives.
As of November 7, 2026, the current releases are Pi-hole Core v6.5, FTL v6.8 and Web v6.6.1 (October 17, 2026); the matching Docker image is pihole/pihole:2026.10.1, published on October 31, 2026 to refresh its Alpine base.
Technical Requirements
What you need
- A VPS running Ubuntu 22.04/24.04 or Debian 12 with at least 512 MB RAM (1 GB recommended)
- A static IPv4 address and ideally an IPv6 address
- Root or sudo SSH access
- Port 53 (TCP and UDP) closed to the Internet: open only to the WireGuard interface or to your own IP addresses, never to everyone
- Port 80 or 443 for the web interface (optional but recommended with HTTPS)
Security first: no open resolver, and tracked updates
On a Raspberry Pi behind a home router, port 53 is only reachable from the house. On a VPS it sits on the Internet: a resolver that answers everyone becomes a relay for DNS amplification DDoS attacks, and your IP ends up on blocklists. Pi-hole knows this. Its default listening mode, “Allow only local requests”, only accepts hosts on a subnet for which an interface exists on the server; the ALL mode “permits all origins”, and the FTL documentation warns that it can turn your Pi-hole into an open resolver if it is not firewalled.
The right architecture follows from that: your devices reach the VPS through a WireGuard tunnel, whose subnet is local for Pi-hole, and port 53 stays closed to the rest of the world. Check it from outside with nmap -sU -p 53 YOUR_VPS_IP: the port must show as closed or filtered.
Follow the fixes too. In 2026 Pi-hole published several rounds of security advisories (April, May, July, September and October), including remote code execution through configuration injection, such as CVE-2026-35517 via dns.upstreams, and local privilege escalations. The latest batch is fixed in the October 17, 2026 releases. Run pihole -up on a native install, or docker compose pull && docker compose up -d with Docker, and apply automatic system security updates alongside. Finally, do not leave the password unset: without a fixed value, Pi-hole generates a random one and writes it to the logs on first start. For a script or an exporter, create an app password under Settings → Web interface / API instead of copying the admin password into it.
Deploy Pi-hole Step by Step
Full installation on Ubuntu/Debian
Disable the systemd-resolved stub resolver
On recent Ubuntu and Debian, systemd-resolved listens on port 53 locally. Pi-hole cannot start while this port is taken. Edit /etc/systemd/resolved.conf and add DNSStubListener=no under the [Resolve] section. Then reload: sudo systemctl reload systemd-resolved. Verify the port is free: ss -tulpn | grep ':53'.
Install Pi-hole
Run the official script:
curl -sSL https://install.pi-hole.net | bashThe installer asks for the network interface (often
eth0) and the upstream DNS (pick Custom if you will install Unbound afterwards). Keep the default listening mode, “Allow only local requests”: with WireGuard, it is the right one. Note the admin password shown at the end, or set it withpihole setpassword.Configure the firewall
Do not open port 53 to the whole Internet. Open the WireGuard port, allow DNS from the tunnel subnet, and deny the rest:
sudo ufw allow 51820/udp sudo ufw allow from 10.8.0.0/24 to any port 53 sudo ufw deny 53Adjust
10.8.0.0/24to your tunnel's subnet. If you use CSF or firewalld, translate the same rules. The web interface must not be public either: go through the tunnel or through an HTTPS reverse proxy restricted to your IPs.Check that it works
Test resolution, then blocking, from the VPS itself; then check from outside that the port cannot be reached outside the tunnel:
# from the VPS dig @127.0.0.1 google.com dig @127.0.0.1 doubleclick.net # from an outside machine, OUTSIDE the tunnel: must fail nmap -sU -p 53 IP_DE_VOTRE_VPSgoogle.commust return an address,doubleclick.netmust answer0.0.0.0, andnmapmust show the port asclosedorfiltered.Point your devices at the VPS
The simplest path is WireGuard: in each client's configuration, set Pi-hole's tunnel address as the DNS server, following the Pi-hole WireGuard guide:
[Interface] Address = 10.8.0.2/32 DNS = 10.8.0.1On Android, “Private DNS” does not fit: it expects the hostname of a DNS-over-TLS provider, not an IP address (see Android help), and Pi-hole does not serve encrypted DNS. On iOS and on desktops, the WireGuard app applies the same
DNSline. The tunnel also protects you on 4G and public Wi-Fi.Open the web interface
In v6, the interface is served by FTL itself at
http://IP/admin(and over HTTPS with a self-signed certificate). Open it through the tunnel, for examplehttp://10.8.0.1/admin, and log in with the password set during install. The dashboard shows live query counts, block rate, most requested domains and most active clients.
Adding Recommended Blocklists
Pi-hole ships with a basic list. For effective blocking, add proven community lists from the Adlists menu in the web interface (Group Management → Adlists).
Recommended lists
- StevenBlack Unified (ads + trackers): https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts — the reference, ~100,000 domains
- OISD Big: https://big.oisd.nl — aggressive list but few false positives, ~500,000 domains
- Firebog Ticked (green checkmark only): visit firebog.net and only add green-ticked lists to minimize false positives
- HaGeZi Multi PRO: https://raw.githubusercontent.com/hagezi/dns-blocklists/main/adblock/pro.txt — excellent malware coverage
- Perflyst Smart-TV: to block telemetry from smart TVs
After adding lists, click Tools → Update Gravity (or run pihole -g over SSH) to download and compile all lists. It takes one to three minutes depending on the number of lists, and Pi-hole keeps working meanwhile. After that, Gravity updates itself once a week, in the early hours of Sunday: there is no need to add your own cron job.
Managing False Positives and Whitelisting
Overly aggressive blocking eventually breaks legitimate services. Pi-hole provides precise diagnostic tools to identify and fix false positives without weakening your protection.
Diagnosing and fixing a false positive
Identify the blocked domain
In the interface, open Query Log (real-time) or Long-term data → Query Log. Filter by client or by Blocked status. Each entry shows the exact domain and the list that blocked it. In v6, you can click on an entry to see full details.
Add to the allowlist
Click the domain and then Allow, or go to Domains and add it as type Allow. To allow a domain and all its subdomains, use the Wildcard option. An allowed entry always wins over blocklists. If the same false positive keeps coming back across many domains, subscribe instead to a community-maintained allowlist under Lists, as type Allow.
Allow from the command line
v6 renamed the commands:
pihole -wand--white-wildno longer exist. Over SSH:pihole allow example.com pihole --allow-wild example.com pihole allow -lThe first allows an exact domain, the second the domain and its subdomains, the third lists the allowlist.
Common false positive cases
Microsoft Teams and Office 365 sometimes block on aggressive lists (live.com, microsoft.com domains). Apple uses CDNs shared with trackers. Windows automatic updates may be blocked by some lists. The firebog.net green list exists precisely to reduce these issues.
Pi-hole in Docker, and port 53 conflicts
If your VPS already runs containers, Pi-hole can be a container itself and act as the resolver for all the others. Two traps keep coming back: another process already holds the host's port 53, and containers cannot reach the address Pi-hole listens on. Docker's embedded resolver (127.0.0.11) lives inside each container: it does not take the host's port 53.
Deploy Pi-hole with Docker Compose and plug your containers into it
Write the docker-compose.yml
The file follows the Pi-hole Docker documentation, with a dated tag rather than
latestso you control updates. DNS is only published on loopback and on the Docker bridge address172.17.0.1, never on the public IP; the web interface only on127.0.0.1:8080, for a reverse proxy.mkdir -p /opt/pihole/etc-pihole && cd /opt/pihole cat > docker-compose.yml << 'EOF' services: pihole: container_name: pihole image: pihole/pihole:2026.10.1 ports: - "127.0.0.1:53:53/tcp" - "127.0.0.1:53:53/udp" - "172.17.0.1:53:53/tcp" - "172.17.0.1:53:53/udp" - "127.0.0.1:8080:80/tcp" environment: TZ: 'Europe/Paris' FTLCONF_webserver_api_password: 'a-long-password' FTLCONF_dns_listeningMode: 'ALL' FTLCONF_dns_upstreams: '9.9.9.9;1.1.1.1' volumes: - './etc-pihole:/etc/pihole' restart: unless-stopped EOF docker compose up -d && docker compose logs pihole | tail -20FTLCONF_dns_listeningMode: 'ALL'is required on the bridge network, because queries arrive from outside the container's subnet; the port mapping is what prevents public exposure. Addcap_add: NET_ADMINonly if Pi-hole also serves DHCP, which makes no sense on a VPS.Plug the other containers into Pi-hole
In each service of your other stacks, set the Docker bridge address:
services: my-app: image: my-image dns: - 172.17.0.1Do not write
127.0.0.1: Docker's documentation points out that, in a container,--dns=127.0.0.1refers to the container's own loopback, not the host's. For a global setting, the same address goes into/etc/docker/daemon.jsonunder thednskey, followed bysystemctl restart docker.Check that no other process is listening
Diagnostic command:
sudo ss -tulpn | grep ':53'. The only expected process ispihole-FTL(ordocker-proxyfor ports published by the container). If you see systemd-resolved, named or bind9, disable the competing service. For systemd-resolved,DNSStubListener=noin/etc/systemd/resolved.confis enough, as on a native install.Check the resolver a container sees
From a container, read its DNS configuration:
docker exec my-app cat /etc/resolv.conf sudo ss -tulpn | grep ':53'A container on a custom Compose network shows
127.0.0.11, Docker's embedded resolver, which forwards to the declareddns:servers: that is normal. The proof is in Pi-hole's Query Log, where the container must appear as a client. If the Gravity update fails with “Could not access the internet”, the Pi-hole container is probably resolving through itself in a loop: check thatFTLCONF_dns_upstreamstargets an external resolver or Unbound.
Pi-hole + Unbound: Full DNS Recursion Without a Third Party
By default, Pi-hole forwards unblocked queries to a public resolver (Cloudflare 1.1.1.1, Google 8.8.8.8). That resolver then sees all your DNS queries. Unbound solves this by querying root and authoritative servers directly — no intermediary.
Installing and configuring Unbound
Install Unbound
sudo apt install unbound. Unbound is available in official Ubuntu and Debian repositories. It starts automatically but we'll configure it to listen locally only.
Create the Pi-hole + Unbound configuration
Create
/etc/unbound/unbound.conf.d/pi-hole.conffrom the configuration in the Pi-hole Unbound guide: listening on127.0.0.1port5335, DNSSEC hardening, EDNS buffer at 1232 bytes.sudo tee /etc/unbound/unbound.conf.d/pi-hole.conf > /dev/null << 'EOF' server: verbosity: 0 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 edns-buffer-size: 1232 prefetch: yes EOF sudo systemctl restart unboundInstalled from the Debian or Ubuntu repositories, Unbound already knows the root servers: downloading
root.hintsfrom internic.net is only useful for a hand-compiled install.Point Pi-hole to Unbound
In the Pi-hole interface, go to Settings → DNS. Uncheck all upstream DNS servers. In the Custom DNS field, enter 127.0.0.1#5335. Save. Pi-hole will now send all unblocked queries to Unbound, which resolves them directly from root servers.
Check recursion and DNSSEC
Test Unbound directly:
dig pi-hole.net @127.0.0.1 -p 5335 dig fail01.dnssec.works @127.0.0.1 -p 5335 dig +ad dnssec.works @127.0.0.1 -p 5335The first query must return an address. The second must fail with
SERVFAILand no address: the domain's signature is deliberately broken. The third must answerNOERRORwith theadflag, which confirms DNSSEC validation. In Pi-hole's Query Log, queries must show as forwarded to127.0.0.1#5335.Neutralise openresolv on Debian
Debian installs
openresolvby default, and theunbound-resolvconfservice then writesnameserver 127.0.0.1without port5335into/etc/resolv.conf: the system queries Pi-hole, which queries Unbound, which is no longer reached. The Pi-hole Unbound guide recommends disabling that service:sudo systemctl disable --now unbound-resolvconf.service # then comment out the unbound_conf= line in /etc/resolvconf.confThen restart Unbound and rerun the tests from the previous step.
Monitoring Pi-hole Metrics
The Pi-hole v6 dashboard shows key metrics in real time. Here's how to interpret them and what to watch.
Important metrics and reference values
- Blocking rate (Ads blocked today): between 10% and 30% is normal. A rate below 5% suggests your lists are incomplete. A rate above 40% may indicate too many false positives.
- Queries/24h: gives an idea of the load. A family network typically generates 5,000 to 50,000 queries per day.
- Top Blocked Domains: if an ad domain tops the list every day, the corresponding app is particularly intrusive.
- Top Clients: identify the most talkative devices — IoT objects are often at the top.
- DNS cache size: in v6, FTL manages the internal cache. A high hit rate (>60%) reduces perceived latency.
- Query Types: an abnormal volume of PTR (reverse resolution) queries may indicate a network scan or misconfigured software.
To export metrics to Prometheus and Grafana, the v6 REST API exposes, among others, /api/stats/summary and /api/info/metrics. The community pihole-exporter connects with an app password (Settings → Web interface / API) and feeds an existing dashboard without touching Pi-hole's configuration.
Pi-hole v6 or AdGuard Home?
AdGuard Home is the other self-hosted filtering DNS, written in Go (version v0.107.80, November 3, 2026). The table lists the differences you can check in both projects' documentation; for a VPS linked to your devices through WireGuard, AdGuard Home's native DNS encryption matters less, since the tunnel already encrypts the traffic.
Pi-hole v6 and AdGuard Home, criterion by criterion
Scroll the table
| Criterion | Pi-hole v6 | AdGuard Home |
|---|---|---|
| Architecture | FTL (C) with built-in web server and API, no PHP or lighttpd | Single Go binary, cross-platform |
| Serving encrypted DNS (DoH, DoT) to clients | No, extra software needed (or a WireGuard tunnel) | Yes, natively |
| Encrypted upstream (DoH, DoT, DNSCrypt) | Not native (cloudflared or Unbound alongside) | Yes, natively |
| Per-client rules | Yes, through client groups | Yes |
| Parental control and forced SafeSearch | Through dedicated lists; no forced SafeSearch | Built in |
| Declarative configuration | `pihole.toml` file and `FTLCONF_*` variables | YAML file and web interface |
Backing Up and Restoring Pi-hole Configuration
Regular Pi-hole backups are essential: they preserve your custom lists, whitelist, groups and DNS configuration, which you won't want to manually reconfigure after a migration or reinstall.
Backup and restore
Create a Teleporter backup
In the web interface, open Settings → Teleporter and export the archive: Pi-hole generates a ZIP file with the configuration, lists, groups and allowed or blocked domains. On the command line, v6 goes through the FTL binary:
mkdir -p /root/pihole-backups && cd /root/pihole-backups sudo pihole-FTL --teleporterThe archive is created in the current directory, and its name is printed.
Automate the backup
Add these two jobs with
crontab -e: one archive every night, and cleanup of archives older than 30 days.0 2 * * * cd /root/pihole-backups && pihole-FTL --teleporter >/dev/null 2>&1 0 3 * * * find /root/pihole-backups -name '*.zip' -mtime +30 -deleteWith Docker, prefix the command with
docker exec piholeand write the archive to the mounted volume, for example/etc/pihole.Restore from a backup
After a reinstall, open Settings → Teleporter and import your ZIP file, or use the command line:
sudo pihole-FTL --teleporter /root/pihole-backups/ARCHIVE.zipPi-hole imports settings, groups and lists. In v6 the Teleporter format differs from v5: v5 backups are only partly restored (lists imported, network configuration to redo by hand).
Back up Unbound configuration
If using Unbound, add its files to your backup: tar -czf /root/unbound-config-$(date +%Y%m%d).tar.gz /etc/unbound/. Unbound configuration doesn't change between versions and can be restored directly.
Store your backups off the VPS: copy them to an S3 bucket, a remote server, or your local machine. If your VPS becomes inaccessible, you can reconfigure Pi-hole on a new server in minutes by simply importing the Teleporter file.