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

有些信息你明明很想要,但对方不会主动通知你:竞品官网改了价格、某个报名页面终于放出名额、政府网站挂出新的申报通知、你关注的那个软件悄悄发了新版、某个绝版商品重新补货。

常规做法是隔三差五手动刷一遍。刷了三天没动静,第四天忘了刷,第五天它改了。

changedetection.io 就是这个「手动刷」的替代品:你把 URL 丢给它,它按你定的间隔抓取、存快照、比对,有变化就按你配的渠道通知你,还能把前后两版并排高亮给你看哪里变了。Python + Flask 写的单容器应用,Apache-2.0 协议,34k+ stars,写这篇时最新版 0.60.7(2026-09-17 发布),仓库昨天还在提交。

changedetection.io 效果图

它到底能干什么

  • 定时抓取 + 差异比对:默认每 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,变了告诉我」的工具。

changedetection.io 监控列表

部署前准备

  • 内存:单容器空载大约 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 重建。这样密码不落进数据目录,重建容器也不丢——代价是改密码必须改环境变量。

设完密码后,加第一个监控项就是最小可用:

  1. 首页输入框贴 URL,点 Watch
  2. 进编辑页,Filters 区填 CSS 选择器(如 .price)或 XPath,只盯这一块
  3. Time Between Checks 设检查间隔。默认 3 小时,盯降价改 15 分钟
  4. 点 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)要额外两步,缺一不可:

  1. 环境变量设 USE_X_SETTINGS=1
  2. 反代传 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/