Can I self-host NextDNS?
YES · ONE COMMAND— setup effort 1 of 4YES — it's called AdGuard Home. It takes one prompt, a 512 MB VPS, and about 10 minutes. That is $1.99 a month you stop paying NextDNS — $23.88 a year on the Pro plan.
Why people pay for NextDNS
Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.
NextDNS sells DNS that is already everywhere: blocklists, analytics and per-device profiles without running a resolver at home. You pay so phones on cellular and laptops in cafes keep the same policy without a VPN back to your house.
| Plan | List price | What it buys |
|---|---|---|
| Free | free | Limited monthly queries. |
| Prothe plan this page prices against | $1.99/mo | Unlimited queries on the advertised Pro tier; confirm current price on the vendor page. |
Vendor list prices in USD, read from the pricing page on 2026-08-07 · confidence: low
Replaced by AdGuard Home
One project, named before the prompt, so you know what you are about to install.
Network-wide DNS ad and tracker blocking for every device that uses this resolver.
Network-wide DNS blocking you run yourself. Best on a home LAN where you control the router; a public VPS as recursive DNS is a different and riskier shape.
The swap
You'd run
AdGuard Home
ONE COMMAND · ~10 min to running · 512 MB RAM
NextDNS Pro · vendor list price · checked 2026-08-07 · source · confidence: low
Before you start
- RAM floor
- 512 MBfloor from upstream docs — not measured by us yet
- Disk
- 5 GBthe app, its data, and room for one backup
- Domain needed
- yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
- Time budget
- ~10 minunder 10 minutes, through the first backup
The prompt
Two paths to the same AdGuard Home: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.
Where it runs
261 lines · 11,432 bytes
What this prompt will do
- Preflight
- Layout
- Secrets
- compose.yml
- Caddy and TLS
- Firewall
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.
Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.
Install AdGuard Home 0.107.78 on that server, reachable at https://<DOMAIN>, behind the existing
Caddy with automatic TLS.
## 1. Preflight
If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they answer.
Its A record must already point at this server.
Say three things before anything is installed, because they decide whether this path is the right
one. One: LAN DNS is the real product. Blocking ads for phones and laptops means those devices
send DNS to this resolver; an admin UI alone does nothing. Two: this VPS path does not open port
53 to the world. A public recursive resolver is an open-resolver risk, and this catalog will not
ship that shape. Use this install for the admin UI from the internet (or over a VPN), and put
household DNS on a LAN machine or reach the resolver through a VPN. Three: during the first-run
wizard you must keep the Admin Web Interface on port 3000. This compose maps host 8201 to
container 3000 only. If the wizard moves the UI to port 80, Caddy keeps talking to 3000 and the
dashboard disappears.
AdGuard Home needs 512 MB of RAM available and 5 GB free on /srv. The image publishes amd64 and
arm64. Measure all four:
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
If available RAM is under 512 MB or free disk is under 5 GB, print both numbers and stop. Do not
install and hope. If `dig +short` prints nothing, print that and stop: Caddy cannot certify a
hostname that does not resolve.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/adguard-home /srv/adguard-home/backups /srv/adguard-home/work /srv/adguard-home/conf
ls -la /srv/adguard-home
```
Assert: `ls -la` shows `backups`, `work` and `conf` owned by the login user. Upstream mounts
`/opt/adguardhome/work` and `/opt/adguardhome/conf`. There is no empty `data/` directory in this
install. `conf/` will hold AdGuardHome.yaml (including the admin password hash) after the wizard.
`work/` holds query logs and filter data.
## 3. Secrets
No secret is generated for this install and there is no `.env` file. That is not an oversight.
The first-run wizard creates the admin account in the browser, and the hash lands in
`conf/AdGuardHome.yaml`. There is no default password to rotate before first login, and there is
no open registration form after setup: only the accounts you create. Step 7 closes the wizard
path by asserting the dashboard requires a completed setup and a login.
Tell the user: when the wizard asks for a username and password, choose both carefully and store
them in a password manager. After setup, the only copy of that password hash on disk is under
`conf/`.
## 4. compose.yml
```bash
cat > /srv/adguard-home/compose.yml <<'EOF'
# AdGuard Home · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
# docker ............. https://github.com/AdguardTeam/AdGuardHome/wiki/Docker
# configuration ...... https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration
#
# One container. Admin UI is published only on loopback at host 8201 mapped to
# container 3000. DNS ports are deliberately NOT published on this VPS path: a
# public open resolver is a liability, and household DNS belongs on a LAN host
# or behind a VPN. During the first-run wizard the operator must keep the web
# interface on port 3000 so this publish mapping continues to work after setup.
# Volumes are work/ and conf/ (upstream's real paths), not an empty data/.
# Digest for v0.107.78 read from Docker Hub on 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
adguard-home:
image: adguard/adguardhome:v0.107.78@sha256:1ea34eafe5dc691007946e8eaab7bf46b0de9412f39213d8c06e48b53bf9a6c5
container_name: adguard-home
restart: unless-stopped
volumes:
- /srv/adguard-home/work:/opt/adguardhome/work
- /srv/adguard-home/conf:/opt/adguardhome/conf
ports:
# Loopback only: Caddy reaches the admin UI. Keep wizard web port at 3000.
- "127.0.0.1:8201:3000"
EOF
cd /srv/adguard-home && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. One service, one published port, two bind mounts, no DNS port
on the host. That missing 53 is intentional.
## 5. Caddy and TLS
Append the block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>` replaced by the
real hostname. Copy the file first: a syntax error here takes down every other site on the box.
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-adguard-home
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
DOMAIN_HOST=<DOMAIN>
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" <<'EOF' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
# AdGuard Home · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#encryption and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. Caddy runs under systemd
# on the host. There is no Caddy container anywhere in this project. This site
# block fronts the admin UI only. It does not make this host a public DNS
# resolver; port 53 stays closed on the VPS path.
<DOMAIN> {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# 8201 is the loopback port compose publishes; it is never in the firewall.
reverse_proxy 127.0.0.1:8201
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Set `DOMAIN_HOST` to the real hostname before the `sed` runs. Assert: `caddy validate` exits 0
and the reload exits 0. If validate fails, restore /etc/caddy/Caddyfile.before-adguard-home,
reload, and report what it objected to.
## 6. Firewall
Two ports open, both Caddy's. Do not open 53:
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
Assert: `ufw status verbose` prints `Status: active`, shows 80, 443/tcp and 443/udp, and no rule
mentioning 8201 or 53. If anything allows 53 from anywhere, delete that rule and stop until the
user understands why this path refuses to be a public resolver.
## 7. Start and verify
```bash
cd /srv/adguard-home
docker compose pull
docker compose up -d
for i in $(seq 1 30); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; case "$code" in 200|301|302|303|307|308) break ;; esac; sleep 5; done
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/
```
Assert: the loop ends on a success-class code and the container is running. Before the wizard,
the page is the setup flow. After the wizard (with UI kept on 3000), the page is the login or
dashboard.
STOP: tell the user to open https://<DOMAIN> and complete the wizard now if it appears. Hard
requirements during the wizard:
1. Admin Web Interface port: keep **3000** (do not switch to 80).
2. Create a strong admin username and password; store them offline.
3. Do not enable this host as a public DNS listener for the internet.
When they finish, they must confirm back to you that they can sign in and see the dashboard.
Do not continue until they confirm.
Post-wizard closure asserts (run only after they confirm the dashboard works):
```bash
# Config file exists and names a user (admin credential lives here).
test -f /srv/adguard-home/conf/AdGuardHome.yaml && echo "conf ok"
grep -E '^(users:|name:|password:)' /srv/adguard-home/conf/AdGuardHome.yaml | head -20
# UI still answers on the mapped port through Caddy.
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/
# Port 53 must not be published on non-loopback addresses.
ss -lunpt 2>/dev/null | grep -E ':53\b' || echo "no :53 listeners (good on this path)"
```
Assert: `conf ok` prints, the yaml shows a users section (do not print password hashes into the
chat summary), the UI still returns a success-class code, and there is no public :53 listener
created by this install. If the UI returns connection failures after the wizard, the web port
was moved off 3000: say that plainly and stop; recovery means editing conf to put the web port
back to 3000 or republishing the new port, which this path does not do by default.
A running container is not success; a signed-in dashboard with conf present is.
## 8. First backup and restore
One archive: `work/`, `conf/`, compose.yml, and the live Caddy site block. Take a backup after
the wizard so the admin hash and filter choices are inside it.
```bash
cd /srv/adguard-home
docker compose stop
sudo tar -czf /srv/adguard-home/backups/adguard-home-$(date +%F).tar.gz -C /srv/adguard-home work conf compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/adguard-home/backups/
```
Assert: the archive exists and is non-empty. Print its size. Downtime is a few seconds.
A backup on the same disk as the data is not a backup. From the user's machine:
```bash
mkdir -p ~/backups/adguard-home
scp vps:/srv/adguard-home/backups/*.tar.gz ~/backups/adguard-home/
```
To restore: `docker compose down`, remove `work` and `conf`, recreate them as in step 2, untar
into /srv/adguard-home, put the Caddy block back if needed, then `docker compose up -d`. Tell
the user: `conf/` is blocklists, rewrites and the admin hash; `work/` is query history and
runtime state. Losing conf loses the product.
## 9. Updating later
New versions are listed at https://github.com/AdguardTeam/AdGuardHome/releases. Take a backup
first, then edit the image line in /srv/adguard-home/compose.yml to the new tag and its digest:
```bash
cd /srv/adguard-home
docker compose pull
docker compose up -d
docker compose logs --tail 30 adguard-home
```
Watch that log until it settles, then open the dashboard and confirm login still works before
calling the update done.
## 10. What will probably go wrong
You will finish the wizard, feel proud, and then change the web port to 80 because the form
suggested it. The dashboard will vanish behind Caddy. I did that once and spent an hour on TLS
before noticing 8201 still pointed at an empty 3000. Keep the UI on 3000. The other miss is
opening 53/udp on a cloud firewall "for a quick test" and discovering the box is in open-resolver
blocklists by Monday. This path refuses that test. If the household needs DNS, install on a LAN
host with the local path, or put devices on a VPN that reaches a resolver you control.
## 11. Out of scope
- Do not publish host port 53 on this VPS. Do not `ufw allow 53`.
- Do not move the admin web port off 3000 in the wizard.
- Do not enable DHCP on a cloud VPS.
- Do not promise NextDNS-style per-device profiles on every cellular network without a VPN or
encrypted-DNS client config, which this install does not configure.
- Do not skip the post-wizard backup. Pre-wizard archives lack the admin hash you care about.No terminal agent? Use the chat fallback — slower, you paste the commands
For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.
This path is slower: you paste every command yourself, and there is nobody watching the output
but you. If you can run Claude Code, use the other tab.
You are installing AdGuard Home 0.107.78 on a VPS where Prompt Zero is done: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny. Run everything over `ssh vps`
unless a step says otherwise, and replace `<DOMAIN>` with the hostname whose A record already
points at the box.
Read these three before step 1. LAN DNS is the real product: devices must send DNS to this
resolver or nothing is blocked. This VPS path does not open port 53 to the world; a public open
resolver is a liability. During the first-run wizard you must keep the Admin Web Interface on
port 3000, because this install only maps host 8201 to container 3000.
## 1. Preflight
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
You should see: at least `512` MB available, at least `5` G free, `amd64` or `arm64`, and your
server's IP on the last line.
If you do not: an empty last line means the A record does not exist yet. Add it, wait a minute,
run `dig +short <DOMAIN>` again. Caddy cannot get a certificate for a hostname that does not
resolve.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/adguard-home /srv/adguard-home/backups /srv/adguard-home/work /srv/adguard-home/conf
ls -la /srv/adguard-home
```
You should see: `backups`, `work` and `conf` under /srv/adguard-home, owned by your login user.
If you do not: re-run the `install -d` line. Upstream mounts `work` and `conf`. There is no empty
`data/` directory. After the wizard, `conf/AdGuardHome.yaml` holds the admin password hash.
## 3. Secrets
There are none generated in a `.env` file, and that is the whole block. The first-run wizard
creates the admin account in the browser. Choose a strong username and password, store them in a
password manager, and know that the only on-disk copy of the password hash is under `conf/` after
setup. There is no open registration form for strangers once the wizard is done.
```bash
ls -la /srv/adguard-home/work /srv/adguard-home/conf
```
You should see: both directories exist and are empty (or nearly empty) before first start.
If you do not: create them with the `install -d` line from step 2.
## 4. compose.yml
Paste the whole block at once, including the last two lines.
```bash
cat > /srv/adguard-home/compose.yml <<'EOF'
# AdGuard Home · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
# docker ............. https://github.com/AdguardTeam/AdGuardHome/wiki/Docker
# configuration ...... https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration
#
# One container. Admin UI is published only on loopback at host 8201 mapped to
# container 3000. DNS ports are deliberately NOT published on this VPS path: a
# public open resolver is a liability, and household DNS belongs on a LAN host
# or behind a VPN. During the first-run wizard the operator must keep the web
# interface on port 3000 so this publish mapping continues to work after setup.
# Volumes are work/ and conf/ (upstream's real paths), not an empty data/.
# Digest for v0.107.78 read from Docker Hub on 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
adguard-home:
image: adguard/adguardhome:v0.107.78@sha256:1ea34eafe5dc691007946e8eaab7bf46b0de9412f39213d8c06e48b53bf9a6c5
container_name: adguard-home
restart: unless-stopped
volumes:
- /srv/adguard-home/work:/opt/adguardhome/work
- /srv/adguard-home/conf:/opt/adguardhome/conf
ports:
# Loopback only: Caddy reaches the admin UI. Keep wizard web port at 3000.
- "127.0.0.1:8201:3000"
EOF
cd /srv/adguard-home && docker compose config >/dev/null && echo "compose OK"
```
You should see: `compose OK` and nothing else.
If you do not: `services must be a mapping` means the indentation was lost. Run
`rm /srv/adguard-home/compose.yml` and paste again in one go. Notice there is no `53:53` line.
That absence is intentional on a public VPS.
## 5. Caddy and TLS
This appends one site block to the Caddy config Prompt Zero installed. Set `DOMAIN_HOST` to
your real hostname before you paste. The first line takes a copy, because a syntax error here
takes down every other site on the box.
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-adguard-home
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
DOMAIN_HOST=<DOMAIN>
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" <<'EOF' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
# AdGuard Home · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#encryption and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. Caddy runs under systemd
# on the host. There is no Caddy container anywhere in this project. This site
# block fronts the admin UI only. It does not make this host a public DNS
# resolver; port 53 stays closed on the VPS path.
<DOMAIN> {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# 8201 is the loopback port compose publishes; it is never in the firewall.
reverse_proxy 127.0.0.1:8201
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
You should see: `Valid configuration` from validate, and no output at all from reload.
If you do not: run `sudo cp /etc/caddy/Caddyfile.before-adguard-home /etc/caddy/Caddyfile`,
reload, and paste again. Caddy requests the certificate on the first request to the hostname and
renews it on its own.
## 6. Firewall
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8201` or `53`.
If you do not: delete anything for `53` or `8201`. This path must not be an open resolver. If a
rule already allowed 53 from anywhere, remove it and stop until you understand why.
## 7. Start and verify
```bash
cd /srv/adguard-home
docker compose pull
docker compose up -d
for i in $(seq 1 30); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; case "$code" in 200|301|302|303|307|308) break ;; esac; sleep 5; done
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/
```
You should see: the loop ending on a success-class code. Before the wizard, the page is setup.
After the wizard (with UI kept on 3000), the page is login or dashboard.
If you do not: run `docker compose logs --tail 40 adguard-home`. A 502 from Caddy with a running
container points at step 5 (wrong reverse_proxy port or a site block that never reloaded). A
container that exits on its own usually means the conf or work path is not writable; check
ownership on `/srv/adguard-home/work` and `/srv/adguard-home/conf`.
This install is local-first in spirit even on a VPS: the admin UI is what you can safely put
on the public hostname. DNS for the household still wants a LAN machine or a VPN. Do not treat
a successful Caddy certificate as proof that phones are filtering ads.
Open https://<DOMAIN> and complete the wizard if it appears. Hard requirements:
1. Admin Web Interface port: keep **3000** (do not switch to 80).
2. Create a strong admin username and password; store them offline.
3. Do not enable this host as a public DNS listener for the internet.
When the dashboard works after sign-in, run the post-wizard closure checks:
```bash
test -f /srv/adguard-home/conf/AdGuardHome.yaml && echo "conf ok"
grep -E '^(users:|name:)' /srv/adguard-home/conf/AdGuardHome.yaml | head -20
curl -sS -o /dev/null -w '%{http_code}\n' https://<DOMAIN>/
ss -lunpt 2>/dev/null | grep -E ':53\b' || echo "no :53 listeners (good on this path)"
```
You should see: `conf ok`, a users section in the yaml (do not paste password hashes into this
chat), a success-class code from the UI, and no public :53 listener from this install.
If the UI vanishes after the wizard, the web port left 3000. Say that plainly; recovery means
putting the web port back to 3000 in conf, not opening random ports.
Do not continue until they confirm the dashboard works and they kept the UI on 3000.
## 8. First backup and restore
One archive: `work/`, `conf/`, compose.yml, and the live Caddyfile. Take it after the wizard so
the admin hash and filter choices are inside it.
```bash
cd /srv/adguard-home
docker compose stop
sudo tar -czf /srv/adguard-home/backups/adguard-home-$(date +%F).tar.gz -C /srv/adguard-home work conf compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh /srv/adguard-home/backups/
```
You should see: one non-empty file. Downtime is a few seconds.
If you do not: an archive of about 100 bytes means `tar` found none of the paths. Run
`tar -tzf` on it and confirm `conf/` and `work/` are listed.
A backup on the same disk as the data is not a backup. On your own machine:
```bash
mkdir -p ~/backups/adguard-home
scp vps:/srv/adguard-home/backups/*.tar.gz ~/backups/adguard-home/
```
To restore: `docker compose down`, remove `work` and `conf`, recreate them, untar into
/srv/adguard-home, restore the Caddy block if needed, then `docker compose up -d`. `conf/` is
blocklists, rewrites and the admin hash. Losing conf loses the product. After restore, sign in
with the same admin password before you trust the install.
Prove the restore while risk is low: after `docker compose up -d`, wait for
`https://<DOMAIN>/` to answer, confirm `conf/AdGuardHome.yaml` still lists your user, and sign
in once. If login fails after a restore that included `work/` but not `conf/`, you restored the
wrong half. Always keep both directories in the same archive, and keep a copy of that archive off
the VPS.
## 9. Updating later
New versions are listed at https://github.com/AdguardTeam/AdGuardHome/releases. Take a backup
first, then edit the `image:` line in /srv/adguard-home/compose.yml to the new tag and its
digest.
```bash
cd /srv/adguard-home
docker compose pull
docker compose up -d
docker compose logs --tail 30 adguard-home
```
You should see: the server starting, no restart loop. Open the dashboard and confirm login still
works before you call the update done.
## 10. What will probably go wrong
You will finish the wizard, change the web port to 80 because the form suggested it, and lose
the dashboard behind Caddy. Keep the UI on 3000. The other miss is opening 53/udp on a cloud
firewall "for a quick test" and finding the box in open-resolver blocklists by Monday. This path
refuses that test. Household DNS belongs on a LAN host (local path) or behind a VPN, not as a
public recursive resolver on a cheap VPS.
A third miss is celebrating a green UI and assuming ads are blocked on the phone. Until a device
or the router uses this resolver for DNS, nothing changes. The admin UI is not a magic network
filter by itself.
A fourth miss is backing up only `work/` because it looks larger. The admin password hash and
every blocklist choice live in `conf/AdGuardHome.yaml`. Restore without `conf/` and you rebuild
the product from scratch even if query history comes back.
If you need DNS for a household from a cloud box, the honest shape is: devices join a VPN (WireGuard
or similar) whose DNS is this host on a private interface, not UDP/53 open on the public
internet. That VPN step is out of scope for this install, but it is the only safe way this VPS
path becomes a family resolver.
## 11. Out of scope
- Do not publish host port 53 on this VPS. Do not `ufw allow 53`.
- Do not move the admin web port off 3000 in the wizard.
- Do not enable DHCP on a cloud VPS.
- Do not promise NextDNS-style per-device profiles on every cellular network without a VPN or
encrypted-DNS client config, which this install does not configure.
- Do not skip the post-wizard backup. Pre-wizard archives lack the admin hash you care about.
- Do not leave the wizard unfinished. An incomplete setup leaves the door open and the product
half-installed.
- Do not treat this page as a drop-in for NextDNS on every cellular network. Without a VPN or
a DoH/DoT client pointing home, phones on LTE never see this resolver.245 lines · 11,133 bytes
What this prompt will do
- Preflight
- Docker
- Layout
- Secrets
- compose.yml
- Nothing is public
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.
Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.
Install AdGuard Home 0.107.78 under ~/selfhost/adguard-home, answering at http://localhost:8201.
## 1. Preflight
Say this to the user before step 2 runs. LAN DNS is the real use of AdGuard Home: every device
that should stop talking to ad trackers must use this machine as its DNS server (or the router
must). An admin UI on localhost alone blocks nothing for the household. This path publishes the
admin UI on loopback only by default, and does not bind host port 53 until the user deliberately
opts in. Binding 53 often needs elevated privileges and can fight with systemd-resolved or other
local resolvers; treat it as a careful second step.
Also say: during the first-run wizard, keep the Admin Web Interface on port 3000. This compose
maps 8201 to container 3000. Moving the UI to 80 breaks that mapping.
Detect the OS and measure the machine:
```bash
uname -s
case "$(uname -s)" in
Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```
`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux the
distribution ID and codename print next, for step 2. AdGuard Home needs 512 MB of RAM available
and 5 GB free on the home disk, and the image publishes amd64 and arm64. Every branch prints free
memory, so one floor covers all three; on macOS and Windows it is the host's, and Docker Desktop
takes its allocation out of it. If available RAM is under 512 MB or free disk is under 5 GB,
print both numbers and stop. Do not install and hope.
## 2. Docker
Check before installing anything:
```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```
If that printed `docker OK` and a compose version, skip to step 3.
Otherwise, install Docker for the OS step 1 detected:
- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
Homebrew, STOP: tell the user to download Docker Desktop from
https://www.docker.com/products/docker-desktop/ and install it, and wait until they
confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
terms, and wait for the whale icon to say it is running. Do not continue until they
confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
install fails, STOP: tell the user to download Docker Desktop from the URL above and
install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
repository, with its signing key saved to a file first, never piped into a shell. The
fence is guarded, a no-op on anything but a Linux with apt:
```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker "$USER"
fi
```
Adding the user to the docker group is root-equivalent on this machine; say that to the
user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
plugin with their distribution's package manager, and to run this prompt again once
`docker info` works.
Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.
## 3. Layout
```bash
mkdir -p ~/selfhost/adguard-home/work ~/selfhost/adguard-home/conf ~/selfhost/adguard-home/backups
ls -la ~/selfhost/adguard-home
```
Assert: `ls -la` shows `work`, `conf` and `backups`. Upstream uses those two mounts. There is
no empty `data/` directory.
## 4. Secrets
No secret is generated and there is no `.env` file. The wizard creates the admin account; the
hash lands in `conf/AdGuardHome.yaml`. Tell the user to choose a strong password in the wizard
and store it offline.
## 5. compose.yml
```bash
cat > ~/selfhost/adguard-home/compose.yml <<'EOF'
# AdGuard Home · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
# docker ............. https://github.com/AdguardTeam/AdGuardHome/wiki/Docker
#
# One container on the computer you are sitting at. Admin UI on loopback 8201
# mapped to container 3000. Keep the wizard web interface on port 3000. DNS port
# 53 is not published by default: binding host 53 needs care (and often root or
# capabilities) and is documented as an optional LAN step, not a default.
# Volumes are work/ and conf/. Digest for v0.107.78 read from Docker Hub on
# 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
adguard-home:
image: adguard/adguardhome:v0.107.78@sha256:1ea34eafe5dc691007946e8eaab7bf46b0de9412f39213d8c06e48b53bf9a6c5
container_name: adguard-home
restart: unless-stopped
volumes:
- ./work:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf
ports:
# Loopback only: no other device reaches the admin UI unless you rebind.
- "127.0.0.1:8201:3000"
EOF
cd ~/selfhost/adguard-home && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. One service, one published admin port, no DNS publish yet.
## 6. Nothing is public
No reverse proxy, no certificate, no firewall rule, and each is a decision. There is no hostname
to resolve. A certificate attests a public name and nothing here has one; browsers
treat http://localhost as a secure context anyway. Nothing is published beyond loopback, so no
port needs closing.
8201 is bound to 127.0.0.1. Confirm the binding:
```bash
grep -c '"127.0.0.1:' ~/selfhost/adguard-home/compose.yml
```
Assert: the count is `1`. Optional LAN DNS (only if the user wants this machine to answer DNS
for the network): edit compose to add `"53:53/tcp"` and `"53:53/udp"`, understand that this
exposes a resolver on every interface Docker publishes, stop anything else bound to 53, then
recreate the container. Do not do that on a laptop that joins untrusted networks without
firewall rules. Default remains admin-UI-only.
## 7. Start and verify
```bash
cd ~/selfhost/adguard-home
docker compose pull
docker compose up -d
for i in $(seq 1 30); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8201/); echo "$i $code"; case "$code" in 200|301|302|303|307|308) break ;; esac; sleep 5; done
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8201/
```
Assert: the loop ends on a success-class code. If `port is already allocated`, free 8201 and
retry.
STOP: tell the user to open http://localhost:8201, complete the wizard if it appears, keep the
Admin Web Interface on port **3000**, create the admin account, and confirm they can sign in to
the dashboard. Do not continue until they confirm.
Post-wizard closure:
```bash
test -f ~/selfhost/adguard-home/conf/AdGuardHome.yaml && echo "conf ok"
grep -E '^(users:|name:)' ~/selfhost/adguard-home/conf/AdGuardHome.yaml | head -20
curl -sS -o /dev/null -w '%{http_code}\n' http://localhost:8201/
```
Assert: `conf ok` prints, users appear in the yaml, and the UI still answers. If the UI dies
after the wizard, the web port left 3000.
## 8. First backup and restore
One archive: `work/`, `conf/` and compose.yml. Take it after the wizard so the admin hash is in
`conf/`.
```bash
cd ~/selfhost/adguard-home
docker compose stop
tar -C ~/selfhost/adguard-home -czf ~/selfhost/adguard-home/backups/adguard-home-$(date +%F).tar.gz work conf compose.yml
docker compose start
ls -lh ~/selfhost/adguard-home/backups/
```
Assert: the archive exists and is non-empty. Print its size.
That archive sits on the same disk as the data, which is not a backup, and on a laptop the disk
and the machine fail together. Ask the user for a destination that leaves this computer, a folder
their sync service watches or a USB stick, and copy it there with `cp`. In Git Bash a Windows
drive is written `/d/Backups`, not `D:\Backups`; confirm it exists before copying. Assert: the
user confirms the filename is listed there. If they have nowhere, say plainly that this install
has no backup.
To restore: `cd ~/selfhost/adguard-home`, `docker compose down`, `rm -rf work conf`, untar the
archive there, then `docker compose up -d`. `conf/` is the product (lists, rewrites, admin hash).
## 9. Updating later
New versions are listed at https://github.com/AdguardTeam/AdGuardHome/releases. Take a backup
first, then edit the image line in ~/selfhost/adguard-home/compose.yml to the new tag and its
digest:
```bash
cd ~/selfhost/adguard-home
docker compose pull
docker compose up -d
docker compose logs --tail 30 adguard-home
```
Confirm the dashboard still logs in before calling the update done.
## 10. What will probably go wrong
You will finish the wizard, move the web port to 80 because the form made it look standard, and
lose the UI on 8201. Keep 3000. You will also point the router at this laptop's LAN IP for DNS
and then close the lid: the whole house loses resolution until the machine wakes. A household
resolver wants a host that stays on. Binding 53 while systemd-resolved still owns it fails with
"address already in use"; stop or reconfigure the conflict before blaming AdGuard.
## 11. Out of scope
- Do not expose the admin UI to the internet from this laptop path.
- Do not configure port forwarding on the router for 8201 or 53 without understanding the
open-resolver risk.
- Do not add a reverse proxy or TLS on this path.
- Do not rebind 8201 to 0.0.0.0 without a reason and a network you trust.
- Do not enable DHCP unless the user understands they are replacing the router for that role.
- Do not skip the post-wizard backup.compose.local.ymlthe services, pinned · local layout24 lines
# AdGuard Home · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
# docker ............. https://github.com/AdguardTeam/AdGuardHome/wiki/Docker
#
# One container on the computer you are sitting at. Admin UI on loopback 8201
# mapped to container 3000. Keep the wizard web interface on port 3000. DNS port
# 53 is not published by default: binding host 53 needs care (and often root or
# capabilities) and is documented as an optional LAN step, not a default.
# Volumes are work/ and conf/. Digest for v0.107.78 read from Docker Hub on
# 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
adguard-home:
image: adguard/adguardhome:v0.107.78@sha256:1ea34eafe5dc691007946e8eaab7bf46b0de9412f39213d8c06e48b53bf9a6c5
container_name: adguard-home
restart: unless-stopped
volumes:
- ./work:/opt/adguardhome/work
- ./conf:/opt/adguardhome/conf
ports:
# Loopback only: no other device reaches the admin UI unless you rebind.
- "127.0.0.1:8201:3000"agent-readable mirror: /self-host/nextdns.md
The files, if you'd rather do it yourself
The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.
compose.ymlthe services, pinned26 lines
# AdGuard Home · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
# docker ............. https://github.com/AdguardTeam/AdGuardHome/wiki/Docker
# configuration ...... https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration
#
# One container. Admin UI is published only on loopback at host 8201 mapped to
# container 3000. DNS ports are deliberately NOT published on this VPS path: a
# public open resolver is a liability, and household DNS belongs on a LAN host
# or behind a VPN. During the first-run wizard the operator must keep the web
# interface on port 3000 so this publish mapping continues to work after setup.
# Volumes are work/ and conf/ (upstream's real paths), not an empty data/.
# Digest for v0.107.78 read from Docker Hub on 2026-08-07.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
adguard-home:
image: adguard/adguardhome:v0.107.78@sha256:1ea34eafe5dc691007946e8eaab7bf46b0de9412f39213d8c06e48b53bf9a6c5
container_name: adguard-home
restart: unless-stopped
volumes:
- /srv/adguard-home/work:/opt/adguardhome/work
- /srv/adguard-home/conf:/opt/adguardhome/conf
ports:
# Loopback only: Caddy reaches the admin UI. Keep wizard web port at 3000.
- "127.0.0.1:8201:3000"Caddyfilethe hostname and TLS26 lines
# AdGuard Home · the Caddy site block for this service.
#
# Authored by caniselfhostit from
# https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#encryption and
# https://caddyserver.com/docs/automatic-https
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed, with
# <DOMAIN> replaced by the hostname pointed at this box. Caddy runs under systemd
# on the host. There is no Caddy container anywhere in this project. This site
# block fronts the admin UI only. It does not make this host a public DNS
# resolver; port 53 stays closed on the VPS path.
<DOMAIN> {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# 8201 is the loopback port compose publishes; it is never in the firewall.
reverse_proxy 127.0.0.1:8201
}install.shthe same install, no agent119 lines
#!/usr/bin/env bash
# AdGuard Home · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
# DOMAIN_HOST=adguard.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
# https://github.com/AdguardTeam/AdGuardHome/wiki/Docker
# https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration
#
# This VPS path publishes the admin UI behind Caddy only. It does NOT open DNS
# port 53 to the world. Household DNS needs a LAN install or a VPN back to a
# resolver you control. During the first-run wizard, keep the web UI on port
# 3000 so the 8201:3000 mapping continues to work after setup.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail
APP_DIR="${APP_DIR:-/srv/adguard-home}"
DOMAIN_HOST="${DOMAIN_HOST:-}"
die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }
# --- 1. Refuse to start on a machine that is not ready -----------------------
[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. adguard.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 512 ] || die "only ${avail_mb} MB of RAM available; this install wants 512 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 5 ] || die "only ${avail_gb} GB free on /srv; this install wants 5 GB"
resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."
# --- 2. Lay the files out ----------------------------------------------------
#
# work/ and conf/ are the real state mounts. There is no data/ directory.
sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups" "$APP_DIR/work" "$APP_DIR/conf"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"
cd "$APP_DIR"
docker compose config >/dev/null
# --- 3. Caddy site block, on the host ----------------------------------------
if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-adguard-home"
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
# --- 4. Ports: Caddy only. Never open 53 on this VPS path. -------------------
if command -v ufw >/dev/null 2>&1; then
echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8201 and 53 stay closed"
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
fi
# --- 5. Start it -------------------------------------------------------------
docker compose pull
docker compose up -d
echo "==> waiting for wizard or UI on https://${DOMAIN_HOST}/"
for _ in $(seq 1 30); do
code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/" || true)"
case "$code" in 200|301|302|303|307|308) break ;; esac
sleep 5
done
[ -n "${code:-}" ] && [ "$code" != "000" ] || die "nothing answered at https://${DOMAIN_HOST}/. Check: docker compose logs --tail 40 adguard-home"
# Port 53 must not be listening on the public interfaces of this host.
if command -v ss >/dev/null 2>&1; then
if ss -lunpt 2>/dev/null | grep -E ':53\b' | grep -vq '127.0.0.1'; then
die "something is listening on port 53 beyond loopback. This VPS path must not be an open resolver."
fi
fi
# --- 6. First backup of whatever state exists (pre or post wizard) -----------
STAMP="$(date +%Y%m%d-%H%M%S)"
docker compose stop
sudo tar -czf "$APP_DIR/backups/adguard-home-${STAMP}.tar.gz" -C "$APP_DIR" work conf compose.yml -C /etc/caddy Caddyfile
docker compose start
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/adguard-home-${STAMP}.tar.gz" ] || die "the backup archive is empty"
cat <<-DONE
AdGuard Home admin UI should be reachable at https://${DOMAIN_HOST}/
1. Open the URL and finish the first-run wizard if it appears. When the
wizard asks for the Admin Web Interface port, keep it at 3000. Do not
move the UI to port 80: this compose only publishes 8201:3000, so a UI
on 80 is unreachable through Caddy.
2. Create the admin account in the wizard. After the wizard, sign in and
confirm the dashboard loads. The admin password hash is stored in
conf/AdGuardHome.yaml (and only there until you change it).
3. This VPS is NOT your household DNS yet. Port 53 is not published and
must not be opened to the world. For LAN DNS, install on a home box
(local path) or reach this UI over a VPN and run DNS where the LAN is.
4. First backup written to $APP_DIR/backups (work/, conf/, compose, live
Caddyfile). Copy it off this disk tonight. A later backup after the
wizard completes is what actually holds your blocklists and admin hash.
DONEWhat you're signing up for
The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel NextDNS.
- DNS for the household only works if devices or the router actually use this resolver. Installing the admin UI does not block ads until something points DNS at it.
- Port 53 on a public VPS is a liability. This catalog's cloud path exposes the admin UI behind Caddy and does not publish DNS to the world. For home LAN DNS use the local path or a VPN, not an open resolver on the public internet.
- During the first-run wizard you must keep the web UI on port 3000. Moving it to 80 breaks this install's 8201:3000 mapping and Caddy stops reaching the dashboard.
- You own the filter lists and the update cadence. Lists go stale, and a bad rule breaks a site for everyone on the network.
- You own the backups of conf/ and work/. Losing conf loses blocklists, rewrites and the admin password hash in AdGuardHome.yaml.
Where this came from
“AdGuard Home is a network-wide software for blocking ads and tracking.”
- Upstream documents Docker volumes for /opt/adguardhome/work and /opt/adguardhome/conf and an initial setup wizard reachable on port 3000. source
- DNS service listens on port 53 (TCP and UDP) once configured; the admin UI is separate from the DNS port. source
- The project is licensed under GNU GPL v3. source
- Caddy obtains and renews TLS certificates automatically for any public hostname named in the Caddyfile. source
Questions people actually ask
Answered from this page's own data — the same numbers, in sentences.
Can I self-host NextDNS?
Not NextDNS itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is AdGuard Home. Network-wide DNS ad and tracker blocking for every device that uses this resolver. The install is one command: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 10 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.
What replaces NextDNS?
AdGuard Home. Network-wide DNS ad and tracker blocking for every device that uses this resolver. Network-wide DNS blocking you run yourself. Best on a home LAN where you control the router; a public VPS as recursive DNS is a different and riskier shape. AdGuard Home is GPL-3.0-only-licensed and free; nothing on this page is a hosted service we sell you.
What does self-hosting cost compared to NextDNS?
512 MB of RAM and 5 GB of disk — the smallest tier most VPS hosts sell, about $5 a month. AdGuard Home itself is free and GPL-3.0-only-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: NextDNS Pro, $1.99/mo — $23.88 a year.
How hard is it really?
ONE COMMAND — under 10 minutes. The rule that produced that verdict: one container, no database, no outside integration, at most one secret. Nothing to negotiate with anyone else, nothing to back up separately, at most one secret to generate. This is the case where the compose file honestly is the whole install. The tier is derived from seven countable facts about the AdGuard Home install, not from anyone's impression of it, and the whole rubric is published on the methodology page.
Can I run AdGuard Home on my own computer instead of a server?
Yes — that is the second path in the prompt box above. "On my computer" installs the same AdGuard Home on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. Worth knowing: LAN DNS is the real use: point this computer or the router at the local resolver. Other devices only benefit after you change their DNS or the router, which is a separate step. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.
Content last checked 2026-08-07. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.