SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Pi-hole vs AdGuard Home, and where to run it

Pi-hole v6 or AdGuard Home? Compare encrypted DNS, blocklists, per-client rules and licences, then run your pick on a VPS without making an open resolver.

Pi-hole vs AdGuard Home: the short answer

Pi-hole vs AdGuard Home comes down to one feature: encrypted DNS. AdGuard Home serves DNS-over-HTTPS, DNS-over-TLS, DNS-over-QUIC and DNSCrypt to your devices out of the box, and it can forward to encrypted upstream servers with no extra software. In the Pi-hole v6 documentation we checked, Pi-hole does neither by itself, so it needs a local proxy for encrypted upstreams. If you only need plain DNS blocking for a home network, both do the job, and you can pick the interface you prefer.

The second half of this guide answers where to run it. On a small board at home, the risk is low. On a VPS (virtual private server), the risk is high. A resolver that answers anyone on the internet is an open resolver, and attackers use open resolvers to flood other hosts. The safe pattern keeps port 53 reachable only over Tailscale or WireGuard.

This guide does not cover the hosted AdGuard DNS service. For that question, read AdGuard DNS vs AdGuard Home, hosted or self-hosted.

What are the current versions, as of November 2026?

Pi-hole is on the v6 line. On 17 October 2026 the project released FTL v6.8, Web v6.6.1 and Core v6.5. FTL is the DNS engine. The newest Docker image tag is 2026.10.1, published on 31 October 2026. Its release note says the tag updates to the latest Alpine image, which carries fixes for high-profile CVEs (common vulnerabilities and exposures). The tag before it, 2026.10.0, is the one that brought in FTL v6.8, Web v6.6.1 and Core v6.5. Pi-hole Docker tags are dates. The docs say they "do not relate to any kind of semantic version number".

Version 6 changed how Pi-hole is built. The web server and the REST API now live inside FTL, so a Pi-hole install no longer needs lighttpd or PHP. All settings sit in one commented TOML file, /etc/pihole/pihole.toml, and v5 settings migrate to it automatically. The web interface also gained native HTTPS. FTL generates a self-signed certificate, or you can give it your own.

AdGuard Home has a latest stable release of v0.107.80, from 3 November 2026. Newer beta builds exist (most recently v1.0.0-b.2, from 5 November 2026). They are pre-releases. Do not run them on a resolver that other people depend on. AdGuard Home keeps its settings in one YAML file, AdGuardHome.yaml, which it creates on first run.

Both projects publish a latest Docker tag, and the official examples use it. Pin a real tag instead. With latest, a routine docker compose pull can download a new release on a day you did not plan for, but it does not upgrade the running container. Applying that image requires recreating the container, for example with docker compose up -d, and you cannot easily go back to the exact image you had unless you recorded its tag or digest.

Does Pi-hole need unbound or cloudflared for encrypted DNS?

There are two directions to check. Upstream means the queries Pi-hole sends out. Downstream means the queries your devices send to Pi-hole.

Upstream. Pi-hole forwards each query it cannot answer from cache to the servers in dns.upstreams. The configuration reference describes that setting as an array of IP addresses or hostnames with an optional port, such as 127.0.0.1#5335. It lists no DNS-over-TLS or DNS-over-HTTPS option, and the FTL release notes from v6.3.3 through v6.8 do not mention one. The Pi-hole docs reach an encrypted upstream through a separate local proxy:

  • The dnscrypt-proxy guide runs the proxy on 127.0.0.1 port 5053, then points Pi-hole at it with sudo pihole-FTL --config dns.upstreams '["127.0.0.1#5053"]'.
  • The cloudflared guide is deprecated. Cloudflare deprecated the proxy-dns feature it relies on in November 2025. The guide says it stops working if you update cloudflared after 2 February 2026, and that new installs by this method are not recommended.

The Pi-hole unbound setup does not encrypt queries. The Pi-hole unbound guide sets it up as a recursive resolver on the same machine. A recursive resolver asks the root servers and each domain's own name servers directly, so no single third-party resolver sees your whole query history. Those queries still travel as plain DNS. Choose unbound to remove the middleman. Choose a proxy such as dnscrypt-proxy if you want the link to your upstream encrypted. Unbound also supports TLS forwarding when configured for it.

Downstream. The HTTPS support added in v6 is for the web interface. The Pi-hole configuration reference does not describe serving DNS-over-TLS or DNS-over-HTTPS to clients. If you need that, check the current FTL release notes before you choose Pi-hole, or choose AdGuard Home.

Does AdGuard Home serve DoH, DoT and DoQ to clients?

Yes. The AdGuard Home knowledge base says it "supports all modern DNS encryption protocols out-of-the-box": DNS-over-HTTPS (DoH), DNS-over-TLS (DoT), DNS-over-QUIC (DoQ) and DNSCrypt. DoH and DoT are built on TLS (transport layer security), so you need a certificate for a hostname that points at your server. You paste the certificate chain and the private key into the encryption settings in the web interface. If an HTTP challenge cannot reach your resolver, issuing the certificate with the DNS-01 challenge avoids opening port 80 to the world.

The Docker image documents a port for each protocol: 443 for DoH, 853/tcp for DoT, 853/udp for DoQ, and 5443 for DNSCrypt. Publish only the ones you use.

Upstream encryption needs no helper either. Each line of upstream_dns takes a scheme prefix:

dns:
  upstream_dns:
    - tls://dns-unfiltered.adguard.com
    - https://dns-unfiltered.adguard.com/dns-query
    - quic://dns-unfiltered.adguard.com

A bare IP address such as 10.0.0.53 means plain DNS over UDP. The configuration docs say you can edit AdGuardHome.yaml only while the service is stopped. Stop the container first, or make the change in the web interface.

This is also the main reason phones push people to AdGuard Home. If you want an Android phone to use your resolver through its system encrypted DNS setting, read what Private DNS does on Android first. A resolver that only speaks plain DNS on port 53 cannot fill that setting.

Which blocklist formats does each one accept?

AdGuard Home documents three syntaxes for filter lists:

  • Adblock-style rules (a subset of the browser syntax), such as ||example.org^ to block and @@||example.org^ to unblock.
  • /etc/hosts-style lines, such as 127.0.0.1 example.com.
  • Domains only, one name per line.

Two parsing rules matter in practice. A line that is not a valid domain is read as an Adblock-style rule. A rule with a modifier AdGuard Home does not know is ignored as a whole, so a browser-only rule in a mixed list is skipped, not half-applied.

Pi-hole adds lists by URL. The Pi-hole docs we checked do not state which file formats it accepts. They do show how to check a list after you add it. The adlist table in Pi-hole's domain database stores number, the number of domains on the list, and invalid_domains, the number of invalid domains it found. A high invalid count means Pi-hole is not reading that list the way you expected, so check that number before you trust a new list. For single entries, Pi-hole keeps four kinds of domain rule: exact allow, exact deny, regex allow and regex deny.

How do per-client rules differ?

Pi-hole uses groups. You assign clients to groups, then link adlists and domain rules to groups. A client can be an IPv4 or IPv6 address, a subnet in CIDR (classless inter-domain routing) notation such as 192.168.2.0/24, a MAC address, a hostname, or an interface written with a leading colon, such as :eth0. The Default group applies to every client you have not assigned. The docs say it "has a special meaning and cannot be deleted".

AdGuard Home lists per-client (device) configuration among its features. It can also put the client into the rule itself, with the $client modifier:

  • ||example.org^$client=192.168.0.0/24 blocks one domain for one subnet.
  • @@||*^$client=127.0.0.1 unblocks everything for one address.

The modifier also accepts a client name in quotes. The docs say $client works only in AdGuard Home and AdGuard DNS, so a list that uses it will not behave the same in other blockers. AdGuard Home can also identify a device by a ClientID, which matters for a public resolver (covered near the end).

What licences do they use?

Pi-hole Core is licensed under the European Union Public Licence, version 1.2 (EUPL-1.2). The FTL repository also states the EUPL. AdGuard Home is licensed under the GNU General Public License, version 3 (GPL-3.0). The licence matters mainly if you plan to change the code and share your version with others. In that case, read the full text of the one that applies.

How much hardware does each one need?

Pi-hole states a minimum of 512MB of RAM and 2GB of free disk space, with 4GB recommended. Its docs call it "very lightweight". The AdGuard Home getting-started page gives no RAM or CPU figure. We have not measured either one, so this guide gives no numbers of its own. Test with your own query volume before you size a server around either.

Where should you run it: a home board or a VPS?

On a home network, a small board is the classic setup. Pi-hole supports Raspberry Pi OS, Debian, Ubuntu and several other distributions, and only actively maintained versions of them. The server needs a fixed address. Pi-hole's docs say a DHCP reservation on your router is fine. Then set that address as the DNS server in your router's DHCP settings, so every device on the network picks it up. A typical home router does not forward inbound port 53 unless you tell it to, so a mistake in listening mode does not expose the resolver to the world.

On a VPS, the server has a public IP address and nothing in front of it except the firewall you build. That changes the rules. Pi-hole's docs warn about this in their interface settings. The "Allow only local requests" mode keeps you safe from DNS amplification attacks if the server is accidentally public, and "Permit all origins" could make your Pi-hole an open resolver. AdGuard Home's guide to running securely says a public instance "may expose your server to DDoS attacks" and recommends that you "disable plain DNS or make it inaccessible from the outside".

A VPS resolver does have one real benefit. It is reachable from anywhere, so your phone can use it on mobile data. The safe way to get that benefit is a private network.

The mistake: Permit all origins plus a public port 53

Pi-hole v6 sets this with dns.listeningMode. The default is LOCAL, "Allow only local requests". SINGLE and ALL both carry the label "Permit all origins". The docs say ALL "truly allows any traffic to be replied to and is a dangerous thing to do". For SINGLE they add: "Make sure your Pi-hole is properly firewalled!"

The trap is that the official Docker example sets FTLCONF_dns_listeningMode: 'ALL'. The Docker docs say that with Docker's default bridge network, the listening mode should be ALL. The same example publishes "53:53/udp" with no host address. Docker's own docs say that without a host address, "the Docker daemon publishes ports to all host addresses". On a home board behind a router, that is harmless. On a VPS, those two lines together make an open resolver.

A host firewall does not save you here. Docker's packet-filtering docs say that for a published port, traffic "gets diverted before it goes through the ufw firewall settings". So a ufw deny 53 rule does not block a port that Docker published. The ufw basics for a VPS still apply to everything else on the box. For containers, the fix is to publish on the right address.

The safe pattern: publish port 53 on your Tailscale address only

Tailscale gives the VPS a private address in your tailnet, from the 100.x.y.z range. Publish every port on that address and nothing listens on the public IP. Install Tailscale with the official script, then read the address:

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up
tailscale ip -4

If the install script fails on your release, the common Tailscale install errors on Ubuntu each have a known cause. The examples below use 100.101.102.103. Replace it with your own address.

Pi-hole, in compose.yaml, adapted from the official example with a pinned tag and every port bound to the tailnet address:

services:
  pihole:
    container_name: pihole
    image: pihole/pihole:2026.10.1
    ports:
      - "100.101.102.103:53:53/tcp"
      - "100.101.102.103:53:53/udp"
      - "100.101.102.103:80:80/tcp"
      - "100.101.102.103:443:443/tcp"
    environment:
      TZ: 'Europe/London'
      FTLCONF_webserver_api_password: 'use-a-long-random-password'
      FTLCONF_dns_listeningMode: 'ALL'
    volumes:
      - './etc-pihole:/etc/pihole'
    restart: unless-stopped

ALL stays, as the Docker docs ask for bridge networking. It is safe here because Docker forwards to the container only the traffic that arrives at the tailnet address. If you leave out the password, the comment in the official example says Pi-hole assigns a random one. The example's cap_add lines are needed only for DHCP, for setting the host clock, or for extra CPU priority, so this file leaves them out. If you want to know how the container network itself works, see how Docker Compose networks work.

sudo docker compose up -d
sudo docker compose ps
sudo ss -lntup | grep ':53 '

The ss output should show port 53 bound to 100.101.102.103 and never to 0.0.0.0. Tailscale must be up before Docker starts the container, because a program can only listen on an address the host already has. If docker compose up instead reports that port 53 is already in use, read the process name that sudo ss -lntup | grep ':53 ' prints. That is the program holding the port.

AdGuard Home, with the official docker run command cut down to the ports a tailnet resolver needs:

sudo docker run --name adguardhome \
  --restart unless-stopped \
  -v /opt/adguardhome/work:/opt/adguardhome/work \
  -v /opt/adguardhome/conf:/opt/adguardhome/conf \
  -p 100.101.102.103:53:53/tcp -p 100.101.102.103:53:53/udp \
  -p 100.101.102.103:80:80/tcp \
  -p 100.101.102.103:3000:3000/tcp \
  -d adguard/adguardhome:v0.107.80

On first run, AdGuard Home serves its setup wizard on port 3000, listening on 0.0.0.0:3000 inside the container. Because the port is published on the tailnet address only, open http://100.101.102.103:3000 from a device on your tailnet to finish setup. The wizard is not reachable from the public internet during that time. Run only one of the two resolvers on a host, because both want port 53 on the same address.

How do you check that the resolver is not open?

Run two tests. From a device on your tailnet, query the tailnet address:

dig @100.101.102.103 example.com

You should get status: NOERROR and an answer section. Then, from a machine that is not on your tailnet, query the public IP of the VPS (here 203.0.113.10):

dig @203.0.113.10 example.com

This must not return a DNS response. A timeout ending with no servers could be reached or an immediate network error, such as a port-unreachable error, is consistent with an inaccessible DNS port. If you get a DNS response instead, port 53 is reachable from the internet. Stop the container and look for a ports line with no host address in front of it.

Point every tailnet device at it

In the Tailscale admin console, open the DNS page. Under Global nameservers, add your resolver's tailnet address and enable Override DNS servers. Devices accepting Tailscale DNS settings now use Pi-hole or AdGuard Home as their global nameserver, on home Wi-Fi or on mobile data. Devices that disable Tailscale DNS settings ignore this configuration, and exit nodes use their own DNS unless you configure the nameserver for use with exit nodes.

Tailscale's DNS docs say you probably need subnet routing unless your nameservers are public or use Tailscale IP addresses. A resolver on its own tailnet address therefore needs no subnet route. You need one only if the resolver sits on a private LAN address behind the VPS. In that case, advertise the range from a subnet router and approve it on the Machines page. The same docs warn that if an access rule or grant stops a device from reaching the nameserver, that device cannot resolve DNS. So when you write tailnet access rules, let every device reach port 53 on the resolver.

The WireGuard-only version

If you already run your own tunnel, the pattern is the same with a different address. In a self-hosted WireGuard VPN on your VPS, the server's tunnel address is 10.8.0.1. Publish the resolver's ports on that address instead of the tailnet one, for example "10.8.0.1:53:53/udp", and put DNS = 10.8.0.1 in the [Interface] section of each client config. The wg0 interface must exist before the container starts, so enable wg-quick@wg0 at boot. Run the same two dig tests, with 10.8.0.1 from a connected client and the public IP from outside the tunnel.

For a Pi-hole installed directly on the VPS without Docker, do not reach for ALL. Use SINGLE, which the docs describe as "accept only on the specified interface", and set dns.interface to the tunnel interface (wg0 or tailscale0). The public interface then never answers.

What if you really need a public resolver?

Sometimes a device cannot join a tailnet or a WireGuard tunnel. AdGuard Home can still serve it, but follow its secure-use guidance. Disable plain DNS or make it unreachable from outside. For plain DNS that only the server itself uses, the docs describe setting dns.bind_hosts to loopback. Turn on the allowlist mode, which the docs recommend "for public instances where the number of clients is known", and list clients by ClientID, which the docs recommend over IP addresses. Keep the default plain-DNS rate limit of 20 queries per second if plain DNS remains enabled. The docs say it "should generally be sufficient". Expect a public resolver to draw DDoS (distributed denial of service) traffic anyway. Pi-hole's docs describe no way to serve encrypted DNS to the public, so this option is AdGuard Home only.

FAQ

Is AdGuard Home better than Pi-hole?

It is better if you need encrypted DNS. AdGuard Home serves DNS-over-HTTPS, DNS-over-TLS, DNS-over-QUIC and DNSCrypt to clients, and forwards to encrypted upstreams with no extra software. Pi-hole v6 needs a local proxy such as dnscrypt-proxy for encrypted upstreams, and its docs do not describe serving encrypted DNS to clients. For plain DNS blocking on a home network, both work, so choose the interface and the group or rule model you prefer.

Does Pi-hole need unbound to encrypt DNS?

No, because the recursive setup in Pi-hole's unbound guide does not encrypt your queries. Pi-hole's unbound guide sets it up as a local recursive resolver, which asks each domain's name servers directly so no single third-party resolver sees your history. Those queries are still plain DNS. For an encrypted upstream, Pi-hole's docs use dnscrypt-proxy on 127.0.0.1#5053. The cloudflared guide is deprecated and not recommended for new installs.

Is it safe to run Pi-hole or AdGuard Home on a VPS?

It is safe if port 53 is not reachable from the public internet. The common mistake is Pi-hole's "Permit all origins" listening mode together with a Docker port mapping such as 53:53/udp. Docker publishes that mapping on every host address, and ufw rules do not block it. Publish port 53 only on your Tailscale or WireGuard address, then run dig against the public IP from outside. It must return no DNS response; a timeout or an immediate network error is consistent with an inaccessible port.

Is the ALL listening mode in the Pi-hole Docker example safe?

Pi-hole's Docker docs say that with Docker's default bridge network, FTLCONF_dns_listeningMode should be ALL. The configuration docs call ALL dangerous because it replies to any traffic. It is safe only when the published ports are limited. Bind each published port to a private address, such as your tailnet IP, and check with sudo ss -lntup that nothing listens on 0.0.0.0:53.

Should I pin the Pi-hole and AdGuard Home Docker images?

Yes. Both official examples use latest, which can download a different release on the next pull; the running container upgrades when you recreate it with that image. Pi-hole tags are dates, such as pihole/pihole:2026.10.1. AdGuard Home tags are versions, such as adguard/adguardhome:v0.107.80. Check each project's releases page before you upgrade, and change the tag on purpose.

Was this article useful?