这是「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 服务端。

它到底能干什么
- 全屋广告与追踪拦截:一次配置,手机、平板、电视、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 更专业,两者是互补关系

上面这张是 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,四步向导:
- 开始配置 —— 选监听网卡,一般保持默认(所有网卡),或者指定内网 IP
- 管理员账号 —— 用户名 + 密码。这步千万不要跳过。源码里有一行日志:
no users in the configuration file; authentication is disabled—— 配置文件里没有用户时,鉴权会被整个关掉。也就是说只要面板端口能访问到,谁进来都是管理员。公网机器上跳过这一步等于把面板送人 - 管理面板端口 —— 默认就是 80。这里的 80 是容器内的端口,跟你映射到宿主机的 8080 没关系,保持 80 不动
- 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):
- 先在
设置 → 加密设置里打开 DNS-over-HTTPS,监听端口 443,证书可以选「让 AdGuard Home 自己签」或者直接复用反正已经签好的域名证书 - 客户端 DoH 地址填
https://dns.你的域名/dns-query - 想让每台设备在日志里可区分,用
/dns-query/{ClientID}这个路径形式(源码里注册了GET/POST /dns-query/{ClientID}两条路由),ClientID 在「设置 → 客户端」里给每台设备单独设
手机系统层面配好之后,在外面连任何 Wi-Fi 都走你自己的 DNS 和过滤规则,顺带把公共 Wi-Fi 的 DNS 劫持也绕过去了。
踩坑清单
- 只映射了 3000,装完面板打不开。 头号坑。向导在容器内 3000,但装好后服务搬到容器内 80。Docker 的端口映射是启动时定死的,容器起来了就改不了——所以启动前必须同时映射 3000 和 80。补救办法:停容器、删容器、加上
-p 8080:80重建(数据卷不动,配置不丢),或者用docker exec进去看 / 改配置 - 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-resolvedDNS=127.0.0.1这一行不能省:关掉 stub listener 之后,/etc/resolv.conf里原来那个127.0.0.53就没人应答了,必须换成真正在监听的地址 - 别把宿主机的
/etc/resolv.conf直接指向127.0.0.1。 Docker 的内置 DNS 在给容器做上游转发时处理不了回环地址,改完容易出现「宿主机解析正常、容器里全部解析失败」。填宿主机的内网 IP(比如192.168.31.193)比填127.0.0.1稳得多;也可以在需要上网的容器 compose 里显式加dns:字段指定一个能用解析器 - 所有客户端 IP 都是
172.17.0.1。 官方文档原话,bridge 网络下 AdGuard Home 看到的是 Docker 网关地址。想让每台设备有自己的身份,用network_mode: host - 切到 host 网络后端口映射全部失效。 这是配套的坑:
ports:里的8080:80不再起作用,DNS 直接听宿主机 53,面板直接听宿主机 80。所以切 host 之前,先在向导里把「管理面板端口」写成8080,或者装好后停容器改AdGuardHome.yaml里的http.address - 运行中改
AdGuardHome.yaml不生效。 官方文档明确说必须在容器停止时改,运行中改会被进程覆盖。改之前先cp AdGuardHome.yaml AdGuardHome.yaml.bak - 忘了面板密码。 停容器 → 把
data/conf/AdGuardHome.yaml里的users:整段删掉 → 启动。这时鉴权就是关的(源码里那条authentication is disabled日志会打出来),进去之后立刻在「设置 → 常规设置」里重建管理员账号 - 登录失败 5 次被锁 15 分钟。 源码默认
auth_attempts: 5/block_auth_min: 15。连错几次后不是服务挂了,是它在锁你,等 15 分钟或者重启容器 latest会跟着官方发版漂移。 这个项目升级是「停容器 → 删容器 → 拉新 tag → 重建」,没有热升级。想控制节奏就钉v0.107.79,看到新版本再手动改。改之前先备份- 没有 Docker HEALTHCHECK。 官方在 v0.107.27 到 v0.107.33 之间试过容器健康检查,因为问题太多在 v0.107.34 移除了。
docker ps里的 healthy 状态别指望,探针要自己写(见「告警/通知」那一节) - 改了监听端口,客户端还是老的解析结果。 AdGuard Home 侧改完配置,别忘了路由器 DHCP 里的 DNS 指向和客户端缓存。Windows 上
ipconfig /flushdns,Linux 上resolvectl flush-caches,macOS 上sudo dscacheutil -flushcache; sudo killall -HUP mDNSResponder - 别把它变成开放递归 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/