这是「Docker 好项目」系列的第 11 期。每期挑一个真正能在自己服务器上跑起来、并且能解决具体问题的项目,给出能直接复制的配置。

浏览器里装了 uBlock Origin,网页确实干净了。但手机 App 的开屏广告、电视盒子的贴片、智能音箱的推荐位,一个都没少——因为那些根本不是网页广告,是 App 自己从广告域名拉的资源,浏览器插件管不到。

能一次性管住所有设备的只有一条路:DNS。在所有流量出门的必经之路上,把广告域名解析成「不存在」,设备就连不上广告服务器。不管它是什么 App、什么系统、什么牌子。

AdGuard Home 就是干这件事的:37k+ stars,GPL-3.0,Go 写的单二进制,官方 Docker 镜像基于 Alpine,空载内存几十 MB,树莓派和 NAS 都能挂。写这篇时最新稳定版 v0.107.79(2026-08-18 发布),仓库到 2026-09-23 还有提交。它做的事情和 Pi-hole 重叠,但过滤器语法兼容性更好(AdGuard / Adblock Plus / hosts 三种语法都吃),界面是现代的那一套,而且自带加密 DNS 服务端。

AdGuard Home 效果图

它到底能干什么

  • 全屋广告与追踪拦截:一次配置,手机、平板、电视、IoT 设备全部生效,不用在每台设备上装任何东西
  • 过滤器订阅:AdGuard / ABP / hosts 三种语法通吃,支持自定义规则,默认每 24 小时自动更新一次
  • 查询日志与统计:哪台设备、什么时间、请求了哪个域名、被拦了几次,全都能查,历史默认保留 90 天
  • DNS 重写(rewrite):把 nas.lan 指到内网 IP,相当于顺手得了一个内网 DNS,不用再去改每台机器的 hosts
  • 加密 DNS 服务端:DoH / DoT / DoQ / DNSCrypt 全支持,手机在外面连公共 Wi-Fi 也能用你自己的 DNS 和过滤规则
  • 客户端级策略:按设备 IP / 网段 / ClientID 分组,给孩子的设备单独开家长控制,给自己的设备关掉拦截
  • SafeSearch / 家长控制 / 危险域名拦截:三个开关,按需开
  • DNS 缓存:默认就开着 4MB 的缓存,重复查询直接本地应答,家里网速体感会快一点
  • DHCP 服务器:可选,能直接替代路由器发 IP(需要 host 网络)
  • 完整的 HTTP API:查询统计、切换保护开关、清缓存都能脚本化

它不做的是:

  • 拦不掉 YouTube 广告。YouTube 的广告和视频同源,DNS 层无解——这是它被问得最多的问题,答案是「不行」而不是「你配置错了」
  • 拦不掉同域名下的推广位。微博、淘宝自己主域名下的广告位,DNS 也管不了
  • 不是防火墙。不做流量整形、不做入侵检测、不管端口
  • 不替代浏览器插件。网页广告还是 uBlock Origin 更专业,两者是互补关系

官方给的 DNS 请求过滤流程

上面这张是 AdGuard 官方仓库里的流程图,把工作方式讲得很清楚:请求进到 dnsproxy,先过「重写 → 过滤器 → 屏蔽服务 → SafeSearch → 家长控制」这条链,命中了就直接返回拦截响应,不命中的才转给上游;上游回来的响应还要再过滤一遍。

部署前准备

  • 内存:空载几十 MB,比第 8 期的 Navidrome 还轻。NAS、软路由、吃灰的小主机随便放
  • 镜像:adguard/adguardhome(Docker Hub 官方镜像)
  • tag:这个项目只发全量版本号。我逐个探测过 registry:v0.107.79、latest、beta、edge 存在,而 v0.107 这种次版本 tag 返回 404。也就是说没有「跟着补丁自动滚动」的 tag 可用——要么钉死 v0.107.79,要么接受 latest(当前它和 v0.107.79 是同一个 digest,但下次发版就漂移了)。edge 是每天推的开发版,别在生产上跑
  • 数据卷两个:/opt/adguardhome/work(数据:查询日志、统计库、过滤器缓存)、/opt/adguardhome/conf(配置:AdGuardHome.yaml)
  • 端口:见下面的速查表。核心只有一个:53
  • 一个没被占用的 53 端口。宿主机跑着 systemd-resolved、或者家里已经有 Pi-hole / 另一个 DNS 服务,都会撞

端口速查

容器端口 协议 干什么用的
53 TCP + UDP 普通 DNS。必映射,两个协议都不能少
3000 TCP 首次安装向导。装完之后就没用了
80 TCP 装完后面板的默认端口。就是它和 3000 的关系坑了最多人
443 TCP + UDP DoH / 面板 HTTPS
853 TCP DoT
853 UDP DoQ(当前版本的 DoQ 就在 853/udp 上)
5443 TCP + UDP DNSCrypt(可选,很少用)
6060 TCP pprof 性能调试,默认关闭,不用映射
67 / 68 UDP DHCP 服务端 / 客户端,只有要用 DHCP 才映射

docker run 一行版

先跑起来感受一下:

mkdir -p /opt/adguardhome/work /opt/adguardhome/conf

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

然后浏览器打开 http://服务器IP:3000。

注意这里同时映射了 3000 和 80(宿主机 8080) —— 不是多余。向导跑在 3000,但你在向导里按默认值走完,服务就搬到容器内的 80 了。只映射 3000 的话,点完最后一步页面立刻打不开:进程活着、在听 80、Docker 却没给 80 开门。这是这个项目最经典的一个坑,后面「踩坑清单」里排第一。

docker-compose 推荐版

正式用建议这么写:

mkdir -p /opt/adguardhome/data/work /opt/adguardhome/data/conf && cd /opt/adguardhome

新建 docker-compose.yml:

services:
  adguardhome:
    image: adguard/adguardhome:v0.107.79
    container_name: adguardhome
    restart: unless-stopped
    volumes:
      - ./data/work:/opt/adguardhome/work
      - ./data/conf:/opt/adguardhome/conf
    ports:
      # 普通 DNS:局域网里所有设备都往这儿发查询
      - "53:53/tcp"
      - "53:53/udp"
      # 首次安装向导专用。装完这行可以删掉
      - "3000:3000/tcp"
      # 管理面板:容器内 80 → 宿主机 8080
      # 局域网自用写 "8080:80";公网机器一定改成 "127.0.0.1:8080:80"
      - "8080:80/tcp"
      # 下面四个只有「给手机在外面用」才需要,不用就先注释掉
      # - "443:443/tcp"
      # - "443:443/udp"
      # - "853:853/tcp"
      # - "853:853/udp"

启动:

docker compose up -d
docker compose logs -f adguardhome

看到日志里有 serving url=http://0.0.0.0:3000 就对了——说明它在 3000 上等你走向导。

每个配置项为什么这么写

配置 说明
adguard/adguardhome:v0.107.79 钉全量版本号。我探测过镜像 tag:v0.107 返回 404,官方根本没发次版本 tag;latest 只是「当前最新」的别名(此刻和 v0.107.79 同 digest),下次发版自动漂移。想控制升级节奏就钉死版本号
./data/conf:/opt/adguardhome/conf AdGuardHome.yaml 在这。改这个文件必须在容器停止的时候改——官方文档明确写了:运行中改会被进程内存里的值覆盖回去,白改
./data/work:/opt/adguardhome/work 查询日志、统计数据库、过滤器缓存。备份时这两个目录要一起
"53:53/tcp" + "53:53/udp" 两个都不能省。DNS 默认走 UDP,但响应包超过 512 字节(DNSSEC、多条 A 记录、大 TXT)时会回退到 TCP。只映射 UDP 的典型症状是「大部分网站正常,个别网站偶尔打不开」——这种间歇性故障最难查
"3000:3000/tcp" 只在第一次启动时有意义。已经装好之后这行可以从 compose 删掉,少一个暴露面
"8080:80/tcp" 容器内 80 是装完以后面板的默认端口。宿主机用 8080 是为了不和别的服务抢,也方便交给反代
不写 network_mode: host 官方文档原话:不写 host 网络时,AdGuard Home 看到的客户端 IP 会是 172.17.0.1 这类 Docker 网关地址,而不是真实设备 IP。后果是所有设备在日志里挤成同一个 IP,按设备分策略、按设备看统计全部失效。要真实 IP 就得用 host 网络,见下
(可选)network_mode: host 加了它之后,ports: 整段全部失效——DNS 直接监听宿主机 53,面板直接监听宿主机 80。所以用 host 网络时,面板端口要在安装向导里直接改成 8080,而不是靠端口映射。好处是每台设备都有独立 IP 可识别,DHCP 服务也只有在这种模式下才能用
restart: unless-stopped 这台机器是全家人的 DNS,重启后必须自己起来。掉了全家断网,别用 no 也别用 on-failure

1Panel 怎么搞

AdGuard Home 在 1Panel 应用商店里,搜到直接装。我翻了它应用仓库里的清单,面板用的镜像就是 adguard/adguardhome:v0.107.79,目录结构是 ./data/work + ./data/conf,和上面那份 compose 一致。

面板会让你填一堆端口,默认值是这么对应的:

面板字段 默认值 对应容器端口
HTTP 网页端口 23000 80(装完之后的面板)
初始设置网页端口 23001 3000(安装向导)
普通 DNS 端口 20053 53
DOH 服务端口 20443 443
DOT 服务端口 853 853
DNS Crypt 服务端口 5443 5443
QUIC 服务端口 20784 / 8853 784 / 8853

最后一行可以直接忽略。 784 和 8853 是早年 DoQ 的实验端口,现在 DoQ 已经统一到 853/udp 了——官方 Dockerfile 里 EXPOSE 的清单早就没有这两个。这是 1Panel 商店清单没跟上的一处,多映射两个 UDP 端口属于纯粹的暴露面,不影响使用。

华为云主机上的实操提示:

  • 安全组不要放行 53。公网上开一个开放递归 DNS,轻则被人拿去做 DNS 放大攻击的跳板、把出网带宽刷满,重则被云厂商判定滥用直接停机。云主机上 AdGuard Home 只该做两件事:管好你自己的域名解析(只给本机或白名单用),以及对外提供 DoH
  • 面板端口别裸奔:8080:80 改成 127.0.0.1:8080:80,公网访问走反代。安装阶段想临时访问,用 SSH 隧道:ssh -L 3000:127.0.0.1:3000 -L 8080:127.0.0.1:8080 root@你的服务器,然后本地浏览器开 http://127.0.0.1:3000。装完就把端口收回去
  • 它真正的主场是家里的常开设备。AdGuard Home 的定位是「局域网的 DNS 服务器」——放在 NAS 或软路由上,路由器 DHCP 里把 DNS 指向它,全屋设备立刻生效。云主机上跑它更多是为了 DoH 和给几台固定机器用

首次访问与最小可用配置

浏览器打开 http://服务器IP:3000,四步向导:

  1. 开始配置 —— 选监听网卡,一般保持默认(所有网卡),或者指定内网 IP
  2. 管理员账号 —— 用户名 + 密码。这步千万不要跳过。源码里有一行日志:no users in the configuration file; authentication is disabled —— 配置文件里没有用户时,鉴权会被整个关掉。也就是说只要面板端口能访问到,谁进来都是管理员。公网机器上跳过这一步等于把面板送人
  3. 管理面板端口 —— 默认就是 80。这里的 80 是容器内的端口,跟你映射到宿主机的 8080 没关系,保持 80 不动
  4. DNS 服务器端口 —— 默认 53,别改(改了就要同步改 -p 映射)

装完页面会自动跳到 http://服务器IP:8080(因为容器内 80 映射在宿主机 8080 上)。如果这里打不开,八成是漏了 80 的映射,回去看「踩坑清单」第一条。

然后按这三步把它配到能用:

① 上游 DNS(设置 → DNS 设置)

国内直连环境建议填带加密的:

https://dns.alidns.com/dns-query
https://doh.pub/dns-query

Bootstrap DNS 必须填纯 IP,不能填域名:

223.5.5.5
119.29.29.29

原因是 DoH 上游本身是个域名(dns.alidns.com),要连它得先解析它——如果 Bootstrap 也填域名,就成了循环依赖,AdGuard Home 启动时会卡在这里报错。Bootstrap 的作用就是「用不加密的 DNS 去查加密 DNS 的地址」。

上游模式保持「负载均衡」就行(这是源码里的默认值)。想更快可以试「最快 IP」,但那个模式会额外发探测请求测延迟,查询日志里会多出一堆噪声记录。

② 过滤器(设置 → 过滤器 → DNS 黑名单)

默认已经开了一条 AdGuard DNS filter(源码里 ID=1,默认 enabled),够用。想再加中文环境的:

https://anti-ad.net/easylist.txt
https://adrules.top/dns.txt

这两条加起来大概 30 万行规则,是默认那条的一倍多。规则越多内存占用越高、首次加载越慢,内存紧张的小机器先只加一条,跑起来用 docker stats adguardhome 看一眼实际占用再加第二条。

过滤器更新间隔是 24 小时,这个值在源码里写死的(filters_update_interval: 24),UI 上改不了。想立刻生效,过滤器列表右上角有个「立即更新」按钮。

③ 验证拦截是否生效

从别的机器直接问 AdGuard Home:

# 广告域名:期望返回 0.0.0.0
dig @192.168.31.193 doubleclick.net +short

# 正常域名:期望返回真实 IP
dig @192.168.31.193 www.baidu.com +short

第一条应该返回 0.0.0.0(AdGuard Home 默认拦截模式就是回零 IP,不是返回 NXDOMAIN,也不是 REFUSED——这点和很多人想的不一样,别看到 0.0.0.0 以为出错了)。

④ 把设备指过来

到这一步 AdGuard Home 自己能干活了,但流量还没过来。两条路:

  • 路由器 DHCP 里改 DNS(推荐):登录路由器后台,把 DHCP 下发的 DNS 服务器改成 AdGuard Home 的内网 IP。全屋设备下次续租就自动生效,不用一台台配
  • 单台设备改:手机 Wi-Fi 设置里把 DNS 改成手动、填 AdGuard Home 的 IP;或者干脆用 DoH 地址(见下面「反向代理与 HTTPS」)

告警/通知配置

先说清楚:AdGuard Home 没有邮件通知,也没有 webhook。 我把 v0.107.79 的源码过了一遍,全库搜不到 SMTP 相关代码。它所谓的「通知」只有网页右上角那个铃铛——过滤器更新失败、有新版本可用这类消息,你不打开面板就永远看不到。

所以真正的告警必须靠外部。最省事的组合是第 1 期那套 Uptime Kuma,三个监控就够:

  • 端到端探测(最重要):监控类型选 DNS,解析服务器填 AdGuard Home 的地址和端口(比如 192.168.31.193:53),查询 www.baidu.com,期望拿到 A 记录。DNS 服务挂了、上游全死了、容器被 OOM 杀了,这一个监控全能抓到
  • 面板存活:HTTP(s) 监控 http://内网IP:8080,期望 200
  • 容器存活:如果有 Docker 监控插件,直接盯 adguardhome 容器状态

想要更细的判据,用 AdGuard Home 自己的 HTTP API。它的 API 支持 HTTP Basic Auth(源码里是标准的 r.BasicAuth() 实现),一条 cron 就能接进任何告警渠道:

#!/bin/sh
# /opt/adguardhome/check.sh —— 每分钟跑一次,异常就推给 Uptime Kuma 的 Push 监控
set -u
AGH="http://127.0.0.1:8080"
AUTH="admin:你的面板密码"
PUSH="https://uptime.你的域名/api/push/你的Token"

if curl -fsS --max-time 5 -u "$AUTH" "$AGH/control/status" \
   | grep -q '"protection_enabled":true'; then
  curl -fsS --max-time 5 "$PUSH?status=up&msg=AGH-OK" >/dev/null
else
  curl -fsS --max-time 5 "$PUSH?status=down&msg=AGH-FAIL" >/dev/null
fi
chmod +x /opt/adguardhome/check.sh
# 加进 crontab,每分钟一次
echo '* * * * * /opt/adguardhome/check.sh' | crontab -

判据用 protection_enabled 而不是 running:running 只说明进程活着,protection_enabled 还能覆盖「有人手滑点了暂停过滤」这种情况——面板上一个「暂停保护」按钮点下去,全家就裸奔了,但你从进程状态上看不出来。

其它几个值得知道的 API:

接口 用途
GET /control/status 版本、DNS 监听地址、running、protection_enabled
GET /control/stats 查询总量、拦截量、Top 域名、Top 客户端
GET /control/querylog 最近的查询明细
GET /control/dns_info 当前上游、Bootstrap、缓存设置
POST /control/cache_clear 清 DNS 缓存
POST /control/protection 开关过滤保护(做维护窗口用)

反向代理与 HTTPS

面板不应该裸奔在公网。用第 2 期的 Nginx Proxy Manager:新建 Proxy Host,域名 dns.你的域名,Forward 到 127.0.0.1:8080,SSL 页签一键签证书。

两个必须注意的点:

  • 不要给 DNS 的 53 端口做反向代理。DNS 是裸的 UDP/TCP 协议,nginx 处理不了,客户端必须直连
  • 反代要带 X-Forwarded-For,并且把反代 IP 加进白名单。AdGuard Home 的「可信代理」配置项 trusted_proxies 源码默认只有 127.0.0.0/8 和 ::1/128,也就是只信任本机。走反代之后要把它加上,否则查询日志里所有客户端都会显示成反代的 IP。NPM 默认带 X-Forwarded-For,1Panel 的反代也带,基本不用改

给手机在外面用(DoH / DoT):

  1. 先在 设置 → 加密设置 里打开 DNS-over-HTTPS,监听端口 443,证书可以选「让 AdGuard Home 自己签」或者直接复用反正已经签好的域名证书
  2. 客户端 DoH 地址填 https://dns.你的域名/dns-query
  3. 想让每台设备在日志里可区分,用 /dns-query/{ClientID} 这个路径形式(源码里注册了 GET/POST /dns-query/{ClientID} 两条路由),ClientID 在「设置 → 客户端」里给每台设备单独设

手机系统层面配好之后,在外面连任何 Wi-Fi 都走你自己的 DNS 和过滤规则,顺带把公共 Wi-Fi 的 DNS 劫持也绕过去了。

踩坑清单

  1. 只映射了 3000,装完面板打不开。 头号坑。向导在容器内 3000,但装好后服务搬到容器内 80。Docker 的端口映射是启动时定死的,容器起来了就改不了——所以启动前必须同时映射 3000 和 80。补救办法:停容器、删容器、加上 -p 8080:80 重建(数据卷不动,配置不丢),或者用 docker exec 进去看 / 改配置
  2. 53 端口被 systemd-resolved 占了。 Ubuntu / Debian 上 systemd-resolved 在 127.0.0.53:53 监听,容器启动会报绑定失败。官方给的标准三步: bash sudo mkdir -p /etc/systemd/resolved.conf.d sudo tee /etc/systemd/resolved.conf.d/adguardhome.conf >/dev/null <<'EOF' [Resolve] DNS=127.0.0.1 DNSStubListener=no EOF sudo mv /etc/resolv.conf /etc/resolv.conf.backup sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf sudo systemctl reload-or-restart systemd-resolved DNS=127.0.0.1 这一行不能省:关掉 stub listener 之后,/etc/resolv.conf 里原来那个 127.0.0.53 就没人应答了,必须换成真正在监听的地址
  3. 别把宿主机的 /etc/resolv.conf 直接指向 127.0.0.1。 Docker 的内置 DNS 在给容器做上游转发时处理不了回环地址,改完容易出现「宿主机解析正常、容器里全部解析失败」。填宿主机的内网 IP(比如 192.168.31.193)比填 127.0.0.1 稳得多;也可以在需要上网的容器 compose 里显式加 dns: 字段指定一个能用解析器
  4. 所有客户端 IP 都是 172.17.0.1。 官方文档原话,bridge 网络下 AdGuard Home 看到的是 Docker 网关地址。想让每台设备有自己的身份,用 network_mode: host
  5. 切到 host 网络后端口映射全部失效。 这是配套的坑:ports: 里的 8080:80 不再起作用,DNS 直接听宿主机 53,面板直接听宿主机 80。所以切 host 之前,先在向导里把「管理面板端口」写成 8080,或者装好后停容器改 AdGuardHome.yaml 里的 http.address
  6. 运行中改 AdGuardHome.yaml 不生效。 官方文档明确说必须在容器停止时改,运行中改会被进程覆盖。改之前先 cp AdGuardHome.yaml AdGuardHome.yaml.bak
  7. 忘了面板密码。 停容器 → 把 data/conf/AdGuardHome.yaml 里的 users: 整段删掉 → 启动。这时鉴权就是关的(源码里那条 authentication is disabled 日志会打出来),进去之后立刻在「设置 → 常规设置」里重建管理员账号
  8. 登录失败 5 次被锁 15 分钟。 源码默认 auth_attempts: 5 / block_auth_min: 15。连错几次后不是服务挂了,是它在锁你,等 15 分钟或者重启容器
  9. latest 会跟着官方发版漂移。 这个项目升级是「停容器 → 删容器 → 拉新 tag → 重建」,没有热升级。想控制节奏就钉 v0.107.79,看到新版本再手动改。改之前先备份
  10. 没有 Docker HEALTHCHECK。 官方在 v0.107.27 到 v0.107.33 之间试过容器健康检查,因为问题太多在 v0.107.34 移除了。docker ps 里的 healthy 状态别指望,探针要自己写(见「告警/通知」那一节)
  11. 改了监听端口,客户端还是老的解析结果。 AdGuard Home 侧改完配置,别忘了路由器 DHCP 里的 DNS 指向和客户端缓存。Windows 上 ipconfig /flushdns,Linux 上 resolvectl flush-caches,macOS 上 sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder
  12. 别把它变成开放递归 DNS。 公网机器上把 53 暴露出去,等于给全世界的攻击者提供免费放大服务。要么只用内网 53,要么在「设置 → DNS 设置 → 访问设置」用 allowed_clients / disallowed_clients 卡死白名单

备份

配置和历史都在两个目录里,停容器直接打包最省事:

cd /opt/adguardhome
docker compose stop adguardhome
tar czf adguardhome-$(date +%F).tar.gz data/
docker compose start adguardhome

必须先停容器——运行中它一直在写统计和查询日志库,热拷贝可能拷到半个事务。停的这几秒全家 DNS 会断,挑个半夜跑。

只想要配置、不要历史的话,单独拷 data/conf/AdGuardHome.yaml 就够了,里面包含管理员密码哈希、过滤器订阅列表、重写规则、客户端配置。恢复到新机器:把目录按原路径放回去,起容器,完事。

在 1Panel 里做成计划任务:每周停容器 → tar → 启容器 → 输出到备份目录(第 4 期 Immich 那一期是同样的做法)。

小结

AdGuard Home 是这个系列里性价比最高的一期:一台常年开着的 NAS 或小主机,几十 MB 内存,一次配置,全家所有设备的广告一起少一大截。它不解决全部广告问题,但它是唯一一个「配一次、管全部设备、还不用在每台设备上装东西」的方案。

适合:家里有常开设备(NAS / 软路由 / 小主机)、被手机开屏广告烦、想看清楚家里每个设备在偷偷连什么域名、需要内网域名解析和加密 DNS。

不适合:想拦 YouTube 广告(DNS 层无解);只在乎浏览器网页广告(装个 uBlock Origin 更省事);没有常开设备也不想维护第二台 DNS——DNS 挂了全家断网,这是它唯一的代价。

最后一句实话:路由器里主 DNS 指向 AdGuard Home 之后,副 DNS 留空最干净(但挂了就全家断网),填个公网 IP 最保险(但挂了之后一部分流量会绕过过滤,日志也会不全)。选哪个取决于你更怕「断网」还是更怕「漏拦」。

下期继续聊另一个 Docker 项目。


项目地址:https://github.com/AdguardTeam/AdGuardHome 官方文档:https://adguard-dns.io/kb/adguard-home/