这是「Docker 好项目」系列的第 9 期。每期挑一个真正能在自己服务器上跑起来、并且能解决具体问题的项目,给出能直接复制的配置。
有些信息你明明很想要,但对方不会主动通知你:竞品官网改了价格、某个报名页面终于放出名额、政府网站挂出新的申报通知、你关注的那个软件悄悄发了新版、某个绝版商品重新补货。
常规做法是隔三差五手动刷一遍。刷了三天没动静,第四天忘了刷,第五天它改了。
changedetection.io 就是这个「手动刷」的替代品:你把 URL 丢给它,它按你定的间隔抓取、存快照、比对,有变化就按你配的渠道通知你,还能把前后两版并排高亮给你看哪里变了。Python + Flask 写的单容器应用,Apache-2.0 协议,34k+ stars,写这篇时最新版 0.60.7(2026-09-17 发布),仓库昨天还在提交。

它到底能干什么
- 定时抓取 + 差异比对:默认每 3 小时查一次,最短可以压到分钟级。抓下来的每一版都存快照,任意两版都能做 diff,变更处高亮
- 只盯页面的一部分:整页比对噪音太大(页脚时间戳一变就报)。用 CSS 选择器或 XPath 圈出真正关心的那块,价格变了才算变
- 可视化选择器:配了浏览器抓取器之后,直接在渲染好的页面上点选元素,不用自己写选择器
- 浏览器步骤:抓之前先执行一串动作——填搜索框、点按钮、登录、加购物车、勾选日期。适合「必须先登录才能看到的价格」这类场景
- 降价 / 补货识别:内置 restock 与价格检测,页面上「已售罄」变成「加入购物车」就报警
- 通知:底层用 Apprise,官方口径 85+ 种渠道。Telegram、Discord、Slack、邮件、Ntfy、Gotify、Bark、Matrix 都是原生的;企业微信 / 钉钉 / 飞书机器人用
json://自定义 Webhook 转发即可 - RSS 与 API:每个监控项都能导出,也提供 REST API 和 Chrome 扩展,可以跟其它系统串起来
它不做的是:不是监控服务可用性的(那是第 1 期 Uptime Kuma 的活儿,它只在你配的端口挂了才管)、不是爬虫框架(不做翻页遍历、不做数据入库)、不会帮你绕过登录验证码。它就是一个「盯着这几个 URL,变了告诉我」的工具。

部署前准备
- 内存:单容器空载大约 200–300MB,1 核 1G 就够。如果要抓 JS 渲染的页面,得再加一个 Chrome 容器,那部分至少再留 1G
- 端口:默认 5000(源码里写死
int(os.environ.get('PORT', 5000))) - 数据目录:容器内
/datastore,启动命令是python ./changedetection.py -d /datastore - 数据库:没有,全部配置就一个
url-watches.json,快照是普通文件,备份极其简单 - 镜像:
ghcr.io/dgtlmoon/changedetection.io。Docker Hub 上也有同名dgtlmoon/changedetection.io,但国内拉 Docker Hub 经常超时,直接用 ghcr 更省心
docker run 一行版
先跑起来感受一下:
docker run -d \
--name changedetection \
--restart unless-stopped \
-p 5000:5000 \
-v /opt/changedetection/datastore:/datastore \
ghcr.io/dgtlmoon/changedetection.io:0.60.7
浏览器打开 http://服务器IP:5000,直接进入,没有登录页——这点后面「首次访问」要重点说。
docker-compose 推荐版
正式用建议这样部署:
mkdir -p /opt/changedetection/datastore && cd /opt/changedetection
新建 docker-compose.yml:
services:
changedetection:
image: ghcr.io/dgtlmoon/changedetection.io:0.60.7
container_name: changedetection
hostname: changedetection
restart: unless-stopped
ports:
- "127.0.0.1:5000:5000"
volumes:
- ./datastore:/datastore
environment:
PORT: 5000
TZ: Asia/Shanghai
BASE_URL: https://cd.你的域名
USE_X_SETTINGS: "1"
FETCH_WORKERS: "10"
MINIMUM_SECONDS_RECHECK_TIME: "60"
PAGE_WATCH_LIMIT: "200"
HIDE_REFERER: "true"
DISABLE_VERSION_CHECK: "true"
# SALTED_PASS: "用下面命令生成后填这里"
启动:
docker compose up -d
docker compose logs -f changedetection
看到 Listening on ... 5000 就成了。
每个配置项为什么这么写
| 配置 | 说明 |
|---|---|
ghcr.io/dgtlmoon/changedetection.io:0.60.7 |
钉全量版本号,别用 :latest,也别用 :0.60。我逐个查过 registry 的 manifest 摘要:0.55 确实等于 0.55.8、0.53 等于 0.53.7,但 0.54 至今仍停在 0.54.0 的镜像上——这个项目的次版本 tag 并不保证随补丁滚动。既然如此,唯一稳的写法就是写全 0.60.7,升级时手动改一行 |
./datastore:/datastore |
全部家当都在这:url-watches.json 是你的监控项配置,其余是每次抓取的文本快照。这是唯一需要备份的目录 |
"127.0.0.1:5000:5000" |
只监听本机,公网访问交给反向代理。想让局域网手机也能直连就改回 "5000:5000",但那样必须先把密码设上 |
PORT: 5000 |
容器内监听端口,默认就是 5000,写出来是为了让你知道能改。改了它别忘了同步改 ports 右边那个 |
TZ: Asia/Shanghai |
调度默认按 UTC 跑。你要设「每天上午 9 点检查」,不写这个就会变成北京时间 17 点 |
BASE_URL |
通知消息里附带的回链地址。不设的话告警里那行是 ("Base URL" not set - see settings - notifications),点不开,等于告警废一半。反代到子路径就填到子路径,如 https://你的域名/cd |
USE_X_SETTINGS: "1" |
让程序尊重反代传来的 Host、X-Forwarded-Prefix、X-Forwarded-Proto。子路径反代必须开,否则页面里的链接全指向错误路径。用子域名反代时开了也无害 |
FETCH_WORKERS: "10" |
并发抓取线程数,默认 10。监控项上了几百个还嫌慢就往上加,但同时开的 Chrome 会话也会变多,内存吃不消就往回调 |
MINIMUM_SECONDS_RECHECK_TIME: "60" |
全局最短重查间隔(秒),默认 3。它的作用是盖掉单个监控项里设的更激进的值——防止手滑设成 0 把对方网站刷到封 IP。个人自用设 60 秒很宽裕,盯秒杀/抢购的才需要压到 10 秒以内 |
PAGE_WATCH_LIMIT: "200" |
实例最多能有多少个监控项,超出后 UI 和 API 都拒绝新增。不设就是不限。设一个能防止配置失控,也防止被别人拿去当免费监控平台 |
HIDE_REFERER: "true" |
抓页面时不带 Referer 头,被监控的网站就看不到你的 changedetection 主机名。降低被针对性拦截的概率 |
DISABLE_VERSION_CHECK: "true" |
关掉「检查新版本」的上报。自托管图的就是数据不出门 |
SALTED_PASS |
用环境变量锁死登录密码,见下节。注意:一旦设了它,网页设置里的密码项会被锁定,改不了,只能改这个环境变量再重启 |
1Panel 怎么搞
changedetection.io 不在 1Panel 应用商店里,走容器 → 编排 → 创建编排:把上面的 docker-compose.yml 原样贴进去,目录选 /opt/changedetection,启动即可。之后升级镜像、看日志、改环境变量都在面板里点。
两个华为云主机上的实操提示:
- 安全组别放行 5000。compose 里已经绑了
127.0.0.1,公网本来也进不来;面板的反代走本机回环,不需要额外开端口 - iptables / firewalld 同理。这系列反复强调的一点:反代在容器里,流量从 80/443 进来转给
127.0.0.1:5000,容器间回环不走公网网卡
首次访问与最小可用配置
浏览器打开 http://服务器IP:5000(或你的反代域名)。
第一件事:设密码。 默认安装是完全开放的,任何拿到地址的人都能看你的监控列表、改配置、加监控项。两条路:
- 网页里:Settings → Password,填两遍保存,之后会要求登录
- 环境变量:
SALTED_PASS。先在本机生成:
python3 -c "
import base64, hashlib, os
password = '换成你的强密码'
salt = os.urandom(32)
key = hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 100000)
print(base64.b64encode(salt + key).decode())
"
把输出填进 compose 的 SALTED_PASS,docker compose up -d 重建。这样密码不落进数据目录,重建容器也不丢——代价是改密码必须改环境变量。
设完密码后,加第一个监控项就是最小可用:
- 首页输入框贴 URL,点 Watch
- 进编辑页,Filters 区填 CSS 选择器(如
.price)或 XPath,只盯这一块 - Time Between Checks 设检查间隔。默认 3 小时,盯降价改 15 分钟
- 点 Recheck 立刻跑一次,先把「初始版本」存下来——没有基线就无所谓「变化」
之后等它变,变化会在列表里标出来,点进去看 diff:

告警与通知配置
通知在 Settings → Notifications,底层是 Apprise,一行一个 URL:
tgram://<bot_token>/<chat_id>
discord://<webhook_id>/<webhook_token>
mailto://user:pass@gmail.com?to=you@qq.com
ntfy://ntfy.sh/你的主题
bark://你的key
gotify://gotify.example.com/token
国内几个平台用自定义 Webhook 转发,以企业微信群机器人为例:
json://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxxxxxx
然后在 URL 参数或通知体的自定义模板里按对方要求的 JSON 拼。钉钉、飞书同理,都是 json:// + 机器人地址。
两个容易忽略的点:
- 配好之后一定要在编辑页点 Send test notification。Apprise 的 URL 拼错是静默失败的,你不测就以为配好了
BASE_URL没设的话,通知里的回链是那句("Base URL" not set...)。所以上面 compose 里那行不是可选项
通知也可以在 Notification settings 里按监控项单独覆盖(Override),比如「降价提醒发 Telegram,别的只发邮件」。
反向代理与 HTTPS
用第 2 期的 Nginx Proxy Manager:新建 Proxy Host,域名 cd.你的域名,Forward 到 127.0.0.1:5000,SSL 页签一键签证书。用子域名的话 USE_X_SETTINGS 开不开都行,但建议开着,改配置时省事。
用 1Panel:网站 → 反向代理 → 新建,代理地址 http://127.0.0.1:5000,然后在网站设置里签 Let's Encrypt。
子路径(如 https://域名/cd)要额外两步,缺一不可:
- 环境变量设
USE_X_SETTINGS=1 - 反代传
X-Forwarded-Prefix /cd(NPM 里在 Advanced 加自定义 location 配置,1Panel 的自定义配置段里加proxy_set_header X-Forwarded-Prefix /cd;)
另外它有一个轻量 WebSocket 通道用于页面上的实时状态刷新(不是核心功能)。NPM 默认开启 WebSocket Support,一般不用管;自写 nginx 规则的话记得带上 Upgrade / Connection 两个头,否则只是实时刷新不灵,其它功能不受影响。
踩坑清单
PUID/PGID完全无效:和上一期 Navidrome 是同一个坑。官方镜像的 Dockerfile 里没有USER指令,容器就是以 root 跑的,源码里也搜不到任何PUID的处理逻辑。网上那些带- PUID=1000的 compose 是从 linuxserver 版抄来的。真要降权就自己加user: "1000:1000"并把./datastore属主改对齐0.60这类次版本 tag 不一定滚动:0.54至今还停在0.54.0。别指望写:0.60能自动吃到0.60.7,钉全量版本号- 抓 JS 渲染的页面一直是空壳:默认抓取器是纯 HTTP 请求,拿不到 JS 生成的内容。要在 Settings → Fetching 里把后端切到 Playwright,并另起一个
dgtlmoon/sockpuppetbrowser容器,主容器配PLAYWRIGHT_DRIVER_URL=ws://sockpuppetbrowser:3000。老的 Selenium 后端已标注废弃,别再走那条路 - 装了 Playwright 但内存爆了:每个 Chrome 会话几百 MB。
FETCH_WORKERS和 sockpuppetbrowser 的MAX_CONCURRENT_CHROME_PROCESSES一起调小,1G 内存的小鸡建议各设 2–3 - 默认无密码:重复一遍,这是最容易被忽略的安全问题。公网暴露又没密码,等于把监控列表和通知配置送人
MINIMUM_SECONDS_RECHECK_TIME默认只有 3 秒:这是全局下限,它不会主动加快你的检查,但如果你不小心把某个监控项设得太激进,它能兜住。反过来,想做秒级监控时也会被它卡住,记得调小- 时区不对导致调度错 8 小时:Scheduler 默认 UTC,必须设
TZ=Asia/Shanghai - 一加监控就报「变化」:八成是没设选择器,整页比对撞上了轮播图、时间戳、CSRF token。用 CSS/XPath 圈定区域,或者启用「Ignore text」把噪声词排除掉
- 对方网站返回 403:加大检查间隔、设
HIDE_REFERER=true、在 compose 里给主容器配一个HTTP_PROXY/HTTPS_PROXY走代理。不要把间隔压到几十秒去硬刷,被封 IP 得不偿失 - 升级务必跨过 0.60.1:0.60.1 修了三个安全公告(添加监控接口的 CSRF、通过
tag_colour字段的存储型 CSS 注入、/diff/download-patch的反射型 XSS)。还在 0.5x 的建议直接上 0.60.7
备份
所有状态都在 ./datastore,备份就是打包这个目录:
cd /opt/changedetection
docker compose stop changedetection
tar czf cd-backup-$(date +%F).tar.gz datastore/
docker compose start changedetection
恢复:
cd /opt/changedetection && docker compose stop changedetection
tar xzf cd-backup-2026-09-22.tar.gz
docker compose start changedetection
只想要配置、不要历史快照的话,单独拷 datastore/url-watches.json 就够了。
在 1Panel 里可以做成计划任务:每天凌晨执行一次上面的 tar,输出到备份目录,配合面板的备份策略一起走。
小结
changedetection.io 是这系列里「装完五分钟就能用上」程度最高的一期:一个容器、一个端口、一个数据目录,加个 URL 就开始工作,不需要数据库、不需要外部依赖。它解决的是一件很小但很烦的事——替你盯着那些不会主动通知你的页面。
适合:盯竞品价格与库存、盯报名/放号页面、盯公告与政策更新、盯软件发布页、盯自己网站上不该被改动的内容(挂马/篡改监测)。
不适合:想知道服务是否宕机(用第 1 期 Uptime Kuma)、想大规模抓取并结构化入库(那是爬虫框架的活儿)。
下期继续聊另一个 Docker 项目。
项目地址:https://github.com/dgtlmoon/changedetection.io 官方文档:https://changedetection.io/docs/