Jump to content

AdGuard Home on PMSS

From Pulsed Media Wiki


AdGuard Home on a Pulsed Media seedbox gives your devices an ad- and tracker-blocking DNS resolver of your own, reached over DNS-over-HTTPS at your account's address and answering only devices that know your secret ClientID. In the test its process used about 80 MB of memory, while Docker counted 490 MiB for the whole container. It runs in rootless Docker, which every PMSS seedbox includes, and works as DNS-over-HTTPS only (why plain DNS is not possible). Tested on a Pulsed Media seedbox on 24 October 2026 with adguard/adguardhome version v0.107.79.

AdGuard Home login form with user name and password fields
The AdGuard Home login page served under a test account's address, 24 October 2026.

Privacy

Privacy level: MEDIUM (the levels used on these guides are HIGH, MEDIUM, LOW and NONE). Using AdGuard Home here reduces your privacy:

  • AdGuard Home keeps a query log, a list of the names your devices looked up, and statistics of those lookups, in your account (~/docker/adguard/work) on a server shared with other customers.
  • Every query it forwards goes to the upstream DNS servers listed in its DNS settings, and their operator sees those names. With the tested version, the setup left one upstream in that list, Quad9's https://dns10.quad9.net/dns-query. Open that list before you point a device here.
  • Each request your devices send carries the ClientID in its address, and requests to your address are written to your account's web server log (~/.lighttpd/access.log, with the time and the request address) and to the server's own web server log (with the time, the request address and the device's IP address). How long the server's log is kept is not covered here. Devices that send lookups as GET requests, as the test commands below do, put the encoded lookup in that address too, so the looked-up name lands in both logs. AdGuard Home's own retention setting does not reach those logs.

AdGuard Home itself does not collect usage statistics or use web services unless you configure it to (AdGuard Home README); by default it does fetch filter-list and version updates, which carry none of your queries (AdGuard Home privacy policy).

To limit this, shorten the query log retention or turn the log off in AdGuard Home's settings, and pick an upstream DNS server whose privacy policy you accept. Your ClientID works like a password: anyone who has it can use your resolver.

Requirements

  • Memory: about 80 MB for the AdGuard Home process in the test; Docker's own counter showed 490 MiB for the container, and the tester noted that figure may include cache. A second test the same day counted 230 MiB. Seedbox and Storage Box Resource Tiers lists each plan's memory: 1 GB on the smallest current seedbox plan, shared with the torrent client, and 250 MB or 500 MB on storage boxes.
  • Own login: yes, an admin user created through AdGuard Home's setup before its port is published.
  • Address: https://SERVER.pulsedmedia.com/user-USERNAME/adguard/ for the web interface, and https://SERVER.pulsedmedia.com/user-USERNAME/adguard/dns-query/CLIENTID for your devices. AdGuard Home has no base-path setting; the proxy strips the path (map-urlpath).
  • Devices: each needs a DNS-over-HTTPS setting that takes a custom address. Android's Private DNS setting speaks DNS-over-TLS only, so it cannot use this.
  • Disk: the image size was not measured in the test, and the query log grows with use. Your file quota is 500 files per GiB of disk quota (Pikkumies on PMSS gives the rule); check what you have left with quota -s.
  • Other apps: none.

Running and updating AdGuard Home is yours to do; this page pins one build.

Install

In the commands below, replace USERNAME with your account name, SERVER with your server's name as it appears in your panel address, and PORT with a free port between 2000 and 38000: run ss -tln and pick a number that does not appear in its output. Run everything over SSH as your own account.

The setup, sed, docker run and proxy lines below are the ones used in the test, except that the test ran the image by its tag and these lines pin the digest that tag had. The lines that generate the password, the setup request and the ClientID (step 2), the ls and grep checks and the removal of install.json were written for this page and not run as written. The admin login is created while the container has no port at all. Other accounts on the server can connect to 127.0.0.1, and AdGuard Home's first-run setup is open to whoever reaches it first, so the port is published only after setup is done.

1. Create the folders and close them to other accounts:

mkdir -p ~/docker/adguard/work ~/docker/adguard/conf
chmod 700 ~/docker/adguard

2. Generate the admin password, the setup request and your ClientID, all in files only you can read:

head -c 24 /dev/urandom | base64 | tr -d '\n' > ~/docker/adguard/admin.pw
chmod 600 ~/docker/adguard/admin.pw
printf '{"web":{"ip":"0.0.0.0","port":3000},"dns":{"ip":"0.0.0.0","port":53},"username":"admin","password":"%s"}' "$(cat ~/docker/adguard/admin.pw)" > ~/docker/adguard/install.json
chmod 600 ~/docker/adguard/install.json
head -c 8 /dev/urandom | od -An -tx1 | tr -d ' \n' > ~/docker/adguard/clientid
chmod 600 ~/docker/adguard/clientid

Port 53 in the setup request exists only inside the container's own network; nothing outside can reach it. Read the admin password later with cat ~/docker/adguard/admin.pw and the ClientID with cat ~/docker/adguard/clientid.

3. Start a setup container with no published port. The image is pinned to the digest the tested version had:

docker run -d --name adguard-setup \
  -v ~/docker/adguard/work:/opt/adguardhome/work \
  -v ~/docker/adguard/conf:/opt/adguardhome/conf \
  adguard/adguardhome@sha256:aba9e3bf0613be3ba3755e1fc311b126e2c24bec25e18b6483894a88283074f0

4. Run the setup from inside the container:

docker exec -i adguard-setup wget -qO- --header='Content-Type: application/json' \
  --post-file=- http://127.0.0.1:3000/control/install/configure < ~/docker/adguard/install.json
ls ~/docker/adguard/conf/AdGuardHome.yaml

Continue when ls lists the file. If wget reports an error or the file is missing, see Troubleshooting. Then remove the setup container and the request file:

docker rm -f adguard-setup
rm ~/docker/adguard/install.json

5. Allow DNS-over-HTTPS without TLS inside the container. Your web server provides HTTPS to your devices and talks to the container over plain HTTP on the loopback address, so AdGuard Home must accept that:

sed -i 's/^    insecure_enabled: false$/    insecure_enabled: true/' ~/docker/adguard/conf/AdGuardHome.yaml
grep -n 'insecure_enabled' ~/docker/adguard/conf/AdGuardHome.yaml

The grep line must show insecure_enabled: true.

Do steps 6 to 8 in one sitting. From step 6, AdGuard Home answers DNS-over-HTTPS lookups from any account on the server. From the moment your web server picks up the fragment in step 7, a minute or two after you save it, it also answers anyone on the internet who finds the address. Both stop when step 8 limits it to your ClientID, which takes a browser sign-in and one settings change. The admin interface stays behind its login throughout.

6. Start AdGuard Home on the loopback address:

docker run -d --name adguard --restart unless-stopped -p 127.0.0.1:PORT:3000 \
  -v ~/docker/adguard/work:/opt/adguardhome/work \
  -v ~/docker/adguard/conf:/opt/adguardhome/conf \
  adguard/adguardhome@sha256:aba9e3bf0613be3ba3755e1fc311b126e2c24bec25e18b6483894a88283074f0

7. Add the proxy fragment. auth.require = () is required: DNS-over-HTTPS clients cannot sign in to your panel, and the web interface has its own login. setenv.set-request-header keeps your panel login, which a browser logged into your panel sends with every request, away from that web interface; lookups carry the ClientID in their address, so they do not need the header. map-urlpath strips your path before the request reaches AdGuard Home:

cat > ~/.lighttpd/custom.d/adguard.conf << 'EOF'
$HTTP["url"] =~ "^/user-USERNAME/adguard/" {
  auth.require = ()
  setenv.set-request-header = ( "Authorization" => "" )
  proxy.server = ( "" => ( ( "host" => "127.0.0.1", "port" => PORT ) ) )
  proxy.header = ( "upgrade" => "enable",
    "map-urlpath" => ( "/user-USERNAME/adguard/" => "/", "/user-USERNAME/adguard" => "" ) )
}
EOF
/usr/sbin/lighttpd -t -f ~/.lighttpd.conf

The last command checks the syntax and prints Syntax OK. If it prints an error instead, fix or delete adguard.conf straight away: a broken fragment keeps your web panel down at the next restart. The panel and proxied apps are unavailable for a moment while the web server restarts.

8. Limit the resolver to your ClientID. Open https://SERVER.pulsedmedia.com/user-USERNAME/adguard/, with the trailing slash, sign in as admin with the password from admin.pw, and open Settings → DNS settings. Allowed clients is in the Access settings section at the bottom of that page: paste the ClientID from ~/docker/adguard/clientid there and press Save configuration. The first test set the list through AdGuard Home's own API (POST /control/access/set), and the effect was immediate; a second test the same day did it in the web interface as described here. Only a ClientID works there: in the test AdGuard Home saw every device as the same internal Docker address, so allowing clients by IP address did not work.

9. Point your devices at https://SERVER.pulsedmedia.com/user-USERNAME/adguard/dns-query/CLIENTID, with your ClientID in place of CLIENTID.

Configure

  • Upstream DNS server: AdGuard Home forwards the queries it does not block to the upstream servers in its DNS settings. That server sees your queries, so choose it on purpose (see Privacy).
  • Query log and statistics: shorten their retention, or turn them off, if you do not need the history.
  • More devices: add more ClientIDs to the allowed clients list, for example one per device, so you can remove one without changing the others.
  • Updating: the digest in steps 3 and 6 keeps you on the build tested on 24 October 2026. To move to a newer build, run docker rm -f adguard and step 6 again with a newer digest; your settings stay in ~/docker/adguard. Updating was not tested.

Integrate

AdGuard Home does not connect to the seedbox apps. A client that accepts a custom DNS-over-HTTPS address should be able to use it; in the test, lookups were sent with curl, and setting up a particular browser, phone or operating system was not tested.

Test

  • docker ps lists adguard as Up.
  • https://SERVER.pulsedmedia.com/user-USERNAME/adguard/ shows the AdGuard Home login page, styled as in the picture above.
  • A request with no login straight to the port is refused, which is what another account on the server would see: curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:PORT/control/status prints 401.
  • stat -c '%a %n' ~/docker/adguard ~/docker/adguard/admin.pw ~/docker/adguard/clientid shows 700, 600 and 600 right after you create them. Later the folder shows 750, not the 700 set in step 1, because PMSS resets the folders in your home to 750 with your account's own group, so other accounts still cannot open it.

Lookups need the ClientID. These two commands look up example.com and print the DNS result code, 0 for answered and 5 for refused. The first reads your ClientID from its file, so it never appears on a command line:

printf 'url = "https://SERVER.pulsedmedia.com/user-USERNAME/adguard/dns-query/%s?dns=AAABAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE"\n' "$(cat ~/docker/adguard/clientid)" \
  | curl -sf -H 'accept: application/dns-message' -K - | od -An -tu1 -j3 -N1 | awk '{print $1 % 16}'
curl -sf -H 'accept: application/dns-message' \
  'https://SERVER.pulsedmedia.com/user-USERNAME/adguard/dns-query?dns=AAABAAABAAAAAAAAB2V4YW1wbGUDY29tAAABAAE' \
  | od -An -tu1 -j3 -N1 | awk '{print $1 % 16}'

After step 8 the first prints 0 and the second, with no ClientID, prints 5. Before step 8 both print 0. With -f, any HTTP error (a 401, 404 or 502, for example) prints no number, only an od error; a refused lookup is still a normal HTTP answer, so it prints 5. The first test made these same lookups with its own commands; the second test ran these two lines as written, before and after step 8, and got the results above.

Uninstall

Switch your devices to another DNS setting first: they stop resolving through AdGuard Home when it is removed. This removes the containers and the web address and leaves your files alone:

docker rm -f adguard adguard-setup
rm ~/.lighttpd/custom.d/adguard.conf
touch ~/.lighttpd/custom

If adguard-setup was already removed in step 4, Docker may report that it does not exist; that is harmless. The touch makes your web server restart without the removed fragment (deleting or disabling a fragment explains why).

Removing the data is your choice. It deletes your settings, the allowed ClientIDs, the admin password and ClientID files, the query log and the statistics:

rm -r ~/docker/adguard
docker rmi adguard/adguardhome@sha256:aba9e3bf0613be3ba3755e1fc311b126e2c24bec25e18b6483894a88283074f0

Troubleshooting

  • wget in step 4 reports an error, or ls finds no AdGuardHome.yaml: check that the setup container is running (docker ps) and give it a few seconds after starting, then run the docker exec line again. The setup container and install.json are still there at that point.
  • Every lookup is refused: the address your device uses is missing the ClientID, or has a different one from the allowed clients list.
  • DNS-over-HTTPS requests get 401: the fragment is missing auth.require = (), so your panel login is asking for a password the device cannot give. The test hit exactly this with a fragment that lacked the line.
  • Older guides mention allow_unencrypted_doh: in v0.107.79 the setting is insecure_enabled under http → doh in AdGuardHome.yaml.
  • The query log shows one IP address for every device: in the test every request reached AdGuard Home from the Docker network, so addresses cannot tell devices apart. Give each device its own ClientID so you can remove one without the others.
  • 502 Bad Gateway: the container is not running or the port in the fragment is wrong; see the 502 section of Lighttpd Custom Configuration. A 502 for a few seconds right after you save a fragment is the web server restarting; the test saw one.

Not on PMSS shared hosting

Plain DNS (port 53) and DNS-over-TLS (port 853) are not available from outside. Devices expect them on those fixed ports, and both are below 1024, which a seedbox account cannot listen on, in a container or outside one (Rootless Limitations). DNS-over-HTTPS travels inside your account's existing HTTPS address instead.

See also