GuidesNetwork and VPNAdGuard Home ad blocking

Block ads on every device with AdGuard Home and rootless Podman

AdGuard Home on Debian 13 in rootless Podman behind Caddy, blocking ads and trackers for your phones and laptops over DNS-over-TLS and DNS-over-HTTPS, with plain DNS only for your VPN and never open to the internet.

Tested on AdGuard Home 0.107.79 on Debian 13 (trixie) on a Melonslab server Updated September 27, 2026

Recommended server for this guide

VC-S Micro · 2 vCPU · 8 GB Memory · 250 GB Storage

Month to month, no lock-in 7-day money-back guarantee

€7.99/mo

Deploy now
On this page

What you will set up

AdGuard Home is a DNS server that blocks ads and trackers. Every device that asks it for a name like doubleclick.net gets 0.0.0.0 back, so the ad never loads, in every app and browser, with nothing installed on the device. A web interface shows what each device looks up and lets you change the blocklists.

Here it runs in Podman under a user of its own called adguard, behind Caddy from the Podman guide. If you want a private resolver without ad blocking, Unbound is the simpler choice.

The design matters more than usual, because a DNS server on the internet is a target:

  • Plain DNS answers only your VPN. Plain DNS runs over UDP, where the sender's address can be forged. A resolver that answers anyone is used within hours to flood other networks: attackers send small questions with the victim's address as the sender, and your server sends the much larger answers to the victim. So port 53 listens only on the address of your WireGuard VPN, never on the server's public addresses.
  • Encrypted DNS answers everyone. DNS-over-TLS and DNS-over-HTTPS run over TCP with TLS, and a forged sender cannot complete the connection, so they cannot be used for such attacks. They are what phones use: Android's Private DNS speaks DNS-over-TLS, and an iPhone takes a profile with DNS-over-HTTPS. Both need a real certificate, which Caddy gets from Let's Encrypt.
  • The web interface answers only your VPN. AdGuard Home has its own login, but behind a proxy it counts every visitor as one: five wrong passwords from a stranger would lock you out for 15 minutes. Caddy keeps the interface for devices on the VPN, and lets DNS-over-HTTPS through for everyone.

Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:

  • AdGuard Home 0.107.79 answered DNS-over-TLS and DNS-over-HTTPS from the internet with a valid Let's Encrypt certificate, and returned 0.0.0.0 for doubleclick.net. DNS-over-TLS and DNS-over-HTTPS over IPv6 were tested from the server itself.
  • A WireGuard device used plain DNS on the VPN address. From the internet, port 53 did not answer at all, both with ufw on and with ufw briefly off, and a check from check-host.net nodes in several countries timed out.
  • The query log showed each device's real address, for plain DNS, DNS-over-TLS and DNS-over-HTTPS.
  • When the certificate file changed, AdGuard Home loaded it by itself, without a restart.
  • A backup was restored, and everything came back by itself after a reboot.

AdGuard Home used about 60 MB of memory, with the default blocklist of about 183,000 rules.

Before you start

You need:

  • a server set up as in the Podman guide, with Caddy running;
  • WireGuard set up as in the WireGuard guide, with the tunnel address 10.8.0.1 and your devices sending all their traffic through it;
  • ufw as in the security guide, steps 1 to 4;
  • an A record and an AAAA record for adguard.example.com pointing at your server;
  • dig on your own computer, to test: on Debian and Ubuntu it is in bind9-dnsutils.

The examples use adguard.example.com for AdGuard Home, 203.0.113.10 for the server's IPv4 address and 2001:db8:1f::/64 for its IPv6 range. Replace them throughout.

If you run Unbound on 10.8.0.1, it already holds port 53 there. Switch it off first with systemctl disable --now unbound, or use one or the other.

1. Let apps use port 53

As root, let ordinary users use ports from 53 upwards, and bind to the VPN address even before WireGuard has created it at boot:

cat > /etc/sysctl.d/50-unprivileged-ports.conf <<EOF
net.ipv4.ip_unprivileged_port_start = 53
net.ipv4.ip_nonlocal_bind = 1
EOF
sysctl --system

This replaces the file from the Podman guide, and 53 covers ports 80, 443 and 853 as well. The alternative, putting Caddy in front of DNS too, would need a Caddy with extra modules: this keeps AdGuard Home in charge of its own ports.

2. Create the user

useradd -m -s /bin/bash adguard
loginctl enable-linger adguard
machinectl shell adguard@

As adguard, create the directories for its configuration, its data and its certificate:

mkdir -p ~/.config/containers/systemd ~/conf ~/work ~/certs
chmod 700 ~/conf ~/work

Everything up to step 4 runs as adguard.

3. Describe the container

Create ~/.config/containers/systemd/adguard.container:

[Unit]
Description=AdGuard Home

[Container]
ContainerName=adguard
Image=docker.io/adguard/adguardhome:latest
Volume=%h/conf:/opt/adguardhome/conf
Volume=%h/work:/opt/adguardhome/work
Volume=%h/certs:/opt/adguardhome/certs:ro
# The web interface and DNS-over-HTTPS, for Caddy only
PublishPort=127.0.0.1:3000:3000
# Plain DNS, on the VPN address only
PublishPort=10.8.0.1:53:53/udp
PublishPort=10.8.0.1:53:53/tcp
# DNS-over-TLS, for everyone
PublishPort=853:853/tcp
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target

Start it, and switch on Podman's daily updates:

systemctl --user daemon-reload
systemctl --user start adguard
systemctl --user enable --now podman-auto-update.timer

Only the ports in this file can be reached. Port 53 is published on 10.8.0.1 alone, so even with the firewall off, nothing answers DNS on your public addresses.

4. Run the setup

Until you finish the setup, AdGuard Home has no password, and whoever opens it first chooses one. That is why its port is on 127.0.0.1 only, and why you reach it through SSH for now. From your own computer:

ssh -L 3000:127.0.0.1:3000 root@203.0.113.10

Keep that session open, and open http://localhost:3000 in your browser. Choose Get Started, then:

  • Under Admin Web Interface, change Port to 3000, the port in the container file. Leave Listen interface at All interfaces, and the DNS server at port 53. The addresses listed below the fields are what AdGuard Home sees inside its container; only the ports from step 3 can really be reached.
  • Choose Next. Under Authentication, enter a Username, a Password and Confirm password, and choose Next.
  • Choose Next once more. "Congratulations!" means the setup is done.

AdGuard Home now asks for this login on every visit. You can close the SSH session.

5. Put Caddy in front

As root, switch to machinectl shell caddy@, and add this block at the end of ~/Caddyfile:

adguard.example.com {
    # DNS-over-HTTPS and the Apple profiles are for everyone, the web interface only for the VPN
    @admin {
        not path /dns-query /dns-query/* /apple/*
        not remote_ip 10.8.0.0/24 2001:db8:1f::/64
    }
    respond @admin 403
    reverse_proxy 127.0.0.1:3000
}

Restart Caddy with systemctl --user restart caddy. It gets a certificate for adguard.example.com within a few seconds.

remote_ip lists the VPN's IPv4 range and your server's /64, from which WireGuard gives each device its IPv6 address. A device on the VPN reaches the interface at https://adguard.example.com; anyone else gets "403".

6. Give AdGuard Home the certificate

DNS-over-TLS on port 853 does not go through Caddy, so AdGuard Home needs the certificate itself. Caddy keeps it in its own volume, readable only by the caddy user. As root, create a service that copies it, /etc/systemd/system/adguard-cert.service:

[Unit]
Description=Copy the certificate for adguard.example.com from Caddy to AdGuard Home

[Service]
Type=oneshot
Environment=D=/home/caddy/.local/share/containers/storage/volumes/caddy-data/_data/caddy/certificates/acme-v02.api.letsencrypt.org-directory/adguard.example.com
ExecStart=install -C -o adguard -g adguard -m 600 ${D}/adguard.example.com.crt /home/adguard/certs/cert.pem
ExecStart=install -C -o adguard -g adguard -m 600 ${D}/adguard.example.com.key /home/adguard/certs/key.pem

And a timer that runs it every day, /etc/systemd/system/adguard-cert.timer:

[Unit]
Description=Copy the certificate for AdGuard Home every day

[Timer]
OnCalendar=daily
Persistent=true

[Install]
WantedBy=timers.target

Switch on the timer, and copy the certificate now:

systemctl daemon-reload
systemctl enable --now adguard-cert.timer
systemctl start adguard-cert.service
ls -l /home/adguard/certs

ls should list cert.pem and key.pem, owned by adguard. Caddy renews the certificate about a month before it expires, and install -C only copies a file that has changed. AdGuard Home watches the files and loads a new certificate by itself: its log shows certificate file is modified.

7. Turn on encrypted DNS

As adguard again, stop AdGuard Home while you edit its configuration:

systemctl --user stop adguard
nano ~/conf/AdGuardHome.yaml

Change these lines to the values below, each in its own section of the file, and leave everything else as it is:

http:
  doh:
    insecure_enabled: true
dns:
  ratelimit: 0
  trusted_proxies:
    - 127.0.0.0/8
    - ::1/128
    - 203.0.113.10/32
tls:
  enabled: true
  server_name: adguard.example.com
  port_https: 0
  port_dns_over_tls: 853
  port_dns_over_quic: 0
  certificate_path: /opt/adguardhome/certs/cert.pem
  private_key_path: /opt/adguardhome/certs/key.pem
querylog:
  interval: 24h
clients:
  runtime_sources:
    whois: false
    rdns: false

Then start it again:

systemctl --user start adguard

What the changes do:

  • insecure_enabled lets AdGuard Home answer DNS-over-HTTPS without TLS of its own, because Caddy handles the TLS. port_https: 0 keeps it from opening an HTTPS port that Caddy already has, and port_dns_over_quic: 0 leaves DNS-over-QUIC off.
  • trusted_proxies with your server's IPv4 address lets AdGuard Home trust the visitor's address that Caddy passes on. Caddy's own connections reach the container from that address, as the Podman guide explains, so without it every DNS-over-HTTPS device would appear as the server.
  • ratelimit: 0 switches off the limit of 20 lookups a second. It only applies to plain DNS, which only your own devices can reach, and it counts the whole VPN as one device. In testing, one device looking up 40 names at once already went over it, and some lookups timed out.
  • querylog keeps the log of every lookup for 24 hours instead of 90 days. Query logs rotation, under Settings and General settings, changes it later, next to Statistics retention for the counts on the dashboard, which is 24 hours by default.
  • whois: false and rdns: false stop AdGuard Home from looking up the owner and the name of every public address that uses it, on public WHOIS servers and through Quad9.

Last, as root, open DNS-over-TLS in the firewall, and let VPN devices reach the DNS server:

ufw allow 853/tcp
ufw allow in on wg0

Port 443, for DNS-over-HTTPS, is already open for Caddy.

8. Check it from outside

From your own computer, not on the VPN. DNS-over-TLS:

dig +tls +tls-ca @adguard.example.com doubleclick.net

DNS-over-HTTPS:

dig +https +tls-ca @adguard.example.com doubleclick.net

Both should answer 0.0.0.0: the ad domain is blocked. +tls-ca checks the certificate, so an answer also means it is valid. A name that is not blocked, such as example.com, gets its real addresses.

Plain DNS, on the server's public address:

dig @203.0.113.10 example.com

This must not answer: dig should report "timed out". To check from several places at once, https://check-host.net/check-tcp?host=203.0.113.10:53 should show "Connection timed out" from every node, while the same check for port 853 connects.

On a device connected to the VPN, dig @10.8.0.1 doubleclick.net should answer 0.0.0.0 too.

9. Point your devices at it

  • Devices on the VPN. In each device's WireGuard configuration, from step 7 of the WireGuard guide, set DNS = 10.8.0.1, and load the configuration on the device again.
  • Android, on any network: open Settings, Network & internet, Private DNS, choose Private DNS provider hostname, and enter adguard.example.com. The names of these settings vary a little between phone makers.
  • iPhone and Mac, on any network: open https://adguard.example.com/apple/doh.mobileconfig?host=adguard.example.com&client_id=anna-phone in Safari, and install the profile under Settings, General, VPN & Device Management.
  • Browsers, such as Firefox or Chrome: set a custom DNS-over-HTTPS provider of https://adguard.example.com/dns-query/anna-laptop.

The last part of the address, anna-phone or anna-laptop, is a name you choose for each device, and the query log shows it instead of the device's address. Android cannot send one here, because that would need a separate certificate for every device name, so it appears by its address.

The iPhone profile and the phone steps were not tested on real phones for this guide; DNS-over-TLS and DNS-over-HTTPS were tested with dig as in step 8, and the profile was downloaded and checked.

10. Blocklists, privacy and updates

In the web interface, Filters and DNS blocklists lists the blocklists. AdGuard DNS filter is on from the start, with about 183,000 rules; Add blocklist adds others, and Check for updates fetches them now. AdGuard Home fetches every list again once a day, which Filter update interval under Settings and General settings changes. To let one blocked site through, add a rule such as @@||example.com^ under Filters and Custom filtering rules.

A blocked name gets 0.0.0.0, or :: for IPv6. Blocking mode, under Settings and DNS settings, can answer NXDOMAIN or REFUSED instead.

What AdGuard Home sends out, and what it does not:

  • There is no telemetry. The container image also switches off AdGuard Home's own update check, so it never contacts AdGuard for updates.
  • It sends your lookups to Quad9 over DNS-over-HTTPS, https://dns10.quad9.net/dns-query, which Upstream DNS servers under DNS settings changes. Quad9 sees the names, coming from your server's address, not from your devices.
  • It fetches the blocklists from their addresses, on GitHub for the default ones.
  • Use AdGuard browsing security web service and Use AdGuard parental control web service are off. If you turn them on, AdGuard Home sends the start of a hash of each name to AdGuard.

AdGuard Home itself is updated by Podman, from step 3: once a day it checks for a new image and restarts the container on it. To see what it would update now, as adguard:

podman auto-update --dry-run

11. Back up

As adguard:

mkdir -p ~/backup
systemctl --user stop adguard
tar -czf ~/backup/adguard.tar.gz -C ~ conf work
systemctl --user start adguard

conf holds all the settings, with your login's password hash, and work the query log, the statistics and the downloaded blocklists; the file was about 1.5 MB here. Copy ~/backup to another machine, and keep it private. To restore, stop AdGuard Home, remove ~/conf and ~/work, unpack the file with tar -xzf ~/backup/adguard.tar.gz -C ~, and start it again. The certificate is not in the backup: step 6 copies it again.

Plain DNS for a home router instead

A home router cannot use DNS-over-TLS in most cases, and may not run WireGuard. If your home has a fixed IPv4 address, you can open plain DNS to that one address instead. Make sure ufw is on first, with ufw status: this depends on it.

In adguard.container, as adguard, change the two plain DNS lines to all addresses:

PublishPort=53:53/udp
PublishPort=53:53/tcp

Restart with systemctl --user daemon-reload and systemctl --user restart adguard. Then, as root, allow your home's address, 198.51.100.20 here, and nobody else:

ufw allow from 198.51.100.20 to any port 53

Set the router's DNS server to 203.0.113.10. Check again from outside as in step 8: check-tcp for port 53 should still time out from every node. If your home's address changes, delete the rule with ufw delete allow from 198.51.100.20 to any port 53 and add the new one; never allow port 53 from everyone.

Troubleshooting

AdGuard Home does not start, and its log shows "Failed to bind port 53 (Permission denied)". Step 1 is missing or has not been loaded; sysctl net.ipv4.ip_unprivileged_port_start should print 53. Then run systemctl --user reset-failed adguard and start it again.

The web interface shows "403". Your device is not on the VPN, or does not send all its traffic through it. Connect WireGuard with AllowedIPs = 0.0.0.0/0, ::/0, as in the WireGuard guide.

The login answers "auth: blocked for" and a time. Five wrong passwords block the login for 15 minutes. Wait, or restart AdGuard Home with systemctl --user restart adguard, which clears the block.

DNS-over-TLS times out from your phone, while DNS-over-HTTPS works. Port 853 is not open: ufw allow 853/tcp from step 7.

Devices on the VPN lose DNS. ufw allow in on wg0 from step 7 is missing. Check also that ss -lnup | grep :53 lists 10.8.0.1:53.

The log shows "certificates has no IP addresses; DNS-over-TLS won't be advertised via DDR". This is harmless. Devices set up as in step 9 use the name, not an address.

A site you need stops working. Open Query Log, find the blocked name from that device, open the menu at the end of its row, and choose Unblock. Or add a @@|| rule as in step 10.

Run it on your own server

VC-S Micro

€7.99/mo

vCPU
2
Memory
8 GB
Storage
250 GB
Transfer
10 TB
Standard
HDD · RAID 10
  • Full root access
  • Native /64 IPv6
  • RAID-protected storage
  • Malmö, Sweden
  • Month to month, no lock-in
  • 7-day money-back guarantee
All guides