Tutorial

Self-hosting

Self-hosting Pi-hole on Your Own VPS

Pi-hole turns any Linux server into a network-wide ad and tracker blocker. Hosted on a VPS, it protects all your devices and all your containers at once, with no extension to install in each browser. This guide covers version 6, released on February 18, 2025 with a rewritten architecture, both as a native install and with Docker Compose. It goes beyond a basic install: never running an open resolver, port 53 taken by systemd-resolved, Unbound for full DNS recursion, false positives, metrics and backups.

17 min read23 steps
Contents· Why Self-host Pi-hole on a VPS1/23
  1. 01Why Self-host Pi-hole on a VPS
  2. 02Concrete benefits of Pi-hole on a VPS
  3. 03Pi-hole v5 vs Pi-hole v6: What Changed
  4. 04Pi-hole v5 vs v6
  5. 05Technical Requirements
  6. 06What you need
  7. 07Security first: no open resolver, and tracked updates
  8. 08Deploy Pi-hole Step by Step
  9. 09Full installation on Ubuntu/Debian
  10. 10Adding Recommended Blocklists
  11. 11Recommended lists
  12. 12Managing False Positives and Whitelisting
  13. 13Diagnosing and fixing a false positive
  14. 14Pi-hole in Docker, and port 53 conflicts
  15. 15Deploy Pi-hole with Docker Compose and plug your containers into it
  16. 16Pi-hole + Unbound: Full DNS Recursion Without a Third Party
  17. 17Installing and configuring Unbound
  18. 18Monitoring Pi-hole Metrics
  19. 19Important metrics and reference values
  20. 20Pi-hole v6 or AdGuard Home?
  21. 21Pi-hole v6 and AdGuard Home, criterion by criterion
  22. 22Backing Up and Restoring Pi-hole Configuration
  23. 23Backup and restore

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

FeaturePi-hole v5Pi-hole v6 (2025)
Web interfacePHP + lighttpdBuilt into FTL, no lighttpd needed
DNS engineSeparate dnsmasqUnified FTL (Faster Than Light) embeds DNS
APILimited API, key in URLFull REST API with token authentication
ConfigurationFiles in /etc/pihole/*Single TOML file pihole.toml
Installationcurl | bash scriptcurl | bash script (identical, same URL)
Dockerpihole/pihole imagepihole/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

  1. 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'.

  2. Install Pi-hole

    Run the official script:

    curl -sSL https://install.pi-hole.net | bash

    The 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 with pihole setpassword.

  3. 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 53

    Adjust 10.8.0.0/24 to 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.

  4. 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_VPS

    google.com must return an address, doubleclick.net must answer 0.0.0.0, and nmap must show the port as closed or filtered.

  5. 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.1

    On 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 DNS line. The tunnel also protects you on 4G and public Wi-Fi.

  6. 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 example http://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

  1. 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.

  2. 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.

  3. Allow from the command line

    v6 renamed the commands: pihole -w and --white-wild no longer exist. Over SSH:

    pihole allow example.com
    pihole --allow-wild example.com
    pihole allow -l

    The first allows an exact domain, the second the domain and its subdomains, the third lists the allowlist.

  4. 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

  1. Write the docker-compose.yml

    The file follows the Pi-hole Docker documentation, with a dated tag rather than latest so you control updates. DNS is only published on loopback and on the Docker bridge address 172.17.0.1, never on the public IP; the web interface only on 127.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 -20

    FTLCONF_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. Add cap_add: NET_ADMIN only if Pi-hole also serves DHCP, which makes no sense on a VPS.

  2. 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.1

    Do not write 127.0.0.1: Docker's documentation points out that, in a container, --dns=127.0.0.1 refers to the container's own loopback, not the host's. For a global setting, the same address goes into /etc/docker/daemon.json under the dns key, followed by systemctl restart docker.

  3. Check that no other process is listening

    Diagnostic command: sudo ss -tulpn | grep ':53'. The only expected process is pihole-FTL (or docker-proxy for ports published by the container). If you see systemd-resolved, named or bind9, disable the competing service. For systemd-resolved, DNSStubListener=no in /etc/systemd/resolved.conf is enough, as on a native install.

  4. 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 declared dns: 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 that FTLCONF_dns_upstreams targets 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

  1. 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.

  2. Create the Pi-hole + Unbound configuration

    Create /etc/unbound/unbound.conf.d/pi-hole.conf from the configuration in the Pi-hole Unbound guide: listening on 127.0.0.1 port 5335, 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 unbound

    Installed from the Debian or Ubuntu repositories, Unbound already knows the root servers: downloading root.hints from internic.net is only useful for a hand-compiled install.

  3. 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.

  4. 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 5335

    The first query must return an address. The second must fail with SERVFAIL and no address: the domain's signature is deliberately broken. The third must answer NOERROR with the ad flag, which confirms DNSSEC validation. In Pi-hole's Query Log, queries must show as forwarded to 127.0.0.1#5335.

  5. Neutralise openresolv on Debian

    Debian installs openresolv by default, and the unbound-resolvconf service then writes nameserver 127.0.0.1 without port 5335 into /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.conf

    Then 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

CriterionPi-hole v6AdGuard Home
ArchitectureFTL (C) with built-in web server and API, no PHP or lighttpdSingle Go binary, cross-platform
Serving encrypted DNS (DoH, DoT) to clientsNo, extra software needed (or a WireGuard tunnel)Yes, natively
Encrypted upstream (DoH, DoT, DNSCrypt)Not native (cloudflared or Unbound alongside)Yes, natively
Per-client rulesYes, through client groupsYes
Parental control and forced SafeSearchThrough dedicated lists; no forced SafeSearchBuilt in
Declarative configuration`pihole.toml` file and `FTLCONF_*` variablesYAML 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

  1. 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 --teleporter

    The archive is created in the current directory, and its name is printed.

  2. 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 -delete

    With Docker, prefix the command with docker exec pihole and write the archive to the mounted volume, for example /etc/pihole.

  3. 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.zip

    Pi-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).

  4. 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.

A filtering DNS that follows you everywhere

The ServOrbit Cloud VPS, with its stable public IP, hosts your own Pi-hole + WireGuard install, ready to lock down: block ads and trackers on all your devices, even on the go, without ever exposing an open resolver.