站群做数据分析,第一反应是接 GA,但境内访问 Google 出口的不确定性、以及”数据必须在自己手里”的诉求,让自托管 Matomo 成了更稳的选择。听起来就是
docker compose up的事,实际用起来会连续撞上四堵墙:CLI 命令不存在、API token 没法通过 UI 生成、所有访客 IP 都显示成 172.18.0.1、报表里混进自己的测试流量。这篇文章按踩坑顺序完整记录这四面墙和出路,环境 = Matomo 5.14 + matomo:5-apache + MariaDB 10.11 双容器 + nginx 反代。

一、先接受一个现实:容器版 CLI 是残废的
习惯 wp-cli 那套的人,第一反应是找 Matomo 的命令行:
docker exec matomo-app php console.php
跑 list 才发现,可用命令只有孤零零几个:core:version、plugin:list、featureflags:list、usercountry:attribute(加 GA importer 的)。没有 user:list,没有 site: 命名空间,建 token、改密码这类高频操作在 CLI 里根本不存在。
结论先行:Docker 版 Matomo 的运维主通道是直连它的 MariaDB,一切 UI 缺失功能都在库里做。前提是先摸清表名——这套 schema 有三处和直觉相反:
| 直觉 | 实际 |
|---|---|
matomo_options |
matomo_option(单数!) |
超管列 super_user |
superuser_access |
| token 存 user 表 | 独立表 matomo_user_token_auth |
DB 访问姿势一个细节:命令行带密码必然刷 insecure 警告,污染后续管道解析,统一 2>/dev/null 压掉:
docker exec matomo-db mariadb -umatomo -p"$PW" matomo -e "..." 2>/dev/null
二、API token 的 DB 直插:先逆向哈希链
要在报表自动化里用 Matomo API,需要个人 token。UI 进不去(见下一节密码问题),只能查源码逆向存储格式。读 UsersManager/Model.php 的 generateTokenAuth 得到答案:
- token 原文 = 32 位 hex(内部是 md5 随机串);
- 库里存的是
hash('sha512', token原文 + config.ini.php 里的 salt)。
照这个算法,token 就能纯 DB 造出来:
SALT=$(grep -m1 '^\s*salt' config/config.ini.php | sed 's/.*"\(.*\)".*/\1/')
TOKEN=*** rand -hex 16)
HASH=$(python3 -c "import hashlib,sys;print(hashlib.sha512((sys.argv[1]+sys.argv[2]).encode()).hexdigest())" "$TOKEN" "$SALT")
docker exec matomo-db mariadb -umatomo -p"$PW" matomo -e \
"INSERT INTO matomo_user_token_auth (login,description,password,hash_algo,system_token,date_created,secure_only)
VALUES ('automation','报表脚本','$HASH','sha512',0,NOW(),0);"
两个键值别写错:hash_algo='sha512' 对应上面的算法;secure_only=0 才允许 token 出现在 URL GET 参数里(=1 只能 POST body)。给的是超管账号的 token,即可通吃全部 API(仅 widgetize 嵌入模式被禁)。
密码哈希也是两层
顺带记录网页登录 100% 失败时的排查路径。Matomo 存储格式是 bcrypt(md5(明文), cost=12)——先 md5 再 bcrypt,两层:
docker exec matomo-app php -r 'echo password_hash(md5("<pw>"), PASSWORD_BCRYPT, ["cost"=>12]);'
库里若发现存的是字面量短串(比如某个两位数),那就是非法哈希,登录永远失败且没有任何报错线索,发现即重写。另外网页登录 POST 的字段名是 form_login/form_password/form_nonce,用 login/password 会被静默弹回登录页、零提示——排查时很容易被误导成”密码错了”。

三、最隐蔽的坑:报表里全是 172.18.0.1
上线后打开 Real Time 一看,所有访客 IP 齐刷刷是 172.18.0.1——Docker 网桥地址。反代没有把真实客户端 IP 透传给 Matomo,地域报告、独立访客、防刷全部失真。
修复在 config/config.ini.php,[General] 段头后插三行:
proxy_client_headers[] = "HTTP_X_FORWARDED_FOR"
proxy_client_headers[] = "HTTP_CF_CONNECTING_IP"
proxy_host_headers[] = "HTTP_X_FORWARDED_HOST"
然后 docker restart matomo-app 生效。但注意前置条件:这些头得真的有人传进来。逐层核对链路——
- 自建 nginx 的 vhost:
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;(多数默认配置本来就有); - 走 Cloudflare 隧道的站:CF 自动加
CF-Connecting-IP,隧道配置文件里补 XFF 透传。
改完验收:新访客的 visitIp 应出现真实家庭宽带出口地址。同时接受一条不可逆事实——历史数据的 visitIp 无法追改,172.18.0.1 的旧记录将永远留在库里。这也是一个提醒:统计系统的转发头配置要在上线第一天就做对,晚一天多一天脏数据。
四、token 别放进 curl 命令行
用 API 拉数据,顺手写:
curl "https://stat.example.com/?module=API&method=...&token_auth=29c4a1..."
实际发生的事:很多执行环境/日志链路会对命令行里的长随机串做自动脱敏(缩写成 29c4…65bb),于是真正发出去的 token 是被截断的。API 返回 Unable to authenticate with the provided token——看起来像 token 失效,其实是你发出的命令已经不是你以为的那条了。
正解是让 token 永远不出现在 shell 命令行:用脚本语言直连 HTTP 库,token 从 600 权限的文件读。同款坑在支付 API、面板密钥场景反复出现,规律统一:凭据进命令行 = 交给不可控的中间层改写,凭据进文件 = 只有自己读得到。

五、API 返回值的防御式解析
对接 API 时三个反复咬人的小坑:
- 参数名大小写敏感:
idSite的大写 S 是硬要求,写成idsite直接报错;Matomo 5 移除了SitesManager.getSitesIdWithVisits,老教程里的方法别照抄。 - 鉴权失败返回的是 dict 不是 list:
{"result":"error","message":...},脚本里直接for r in resp会抛'str' object has no attribute 'get'。解析前统一先判错误。 - 空数据的形态不统一:
VisitsSummary.get无数据时值是裸整数而非{value:...}字典,MultiSites.getAll空时返回[]。所有读取写双兼容:
v = data.get("visits")
v = v["value"] if isinstance(v, dict) else v
六、测试数据隔离:报表被自己的 E2E 污染了
最后一个管理教训,和软件无关。做完付费链路联调,第二天看板发现转化数据异常漂亮——一查,是自家 E2E 测试流量打进了生产统计库,测试的 payment_success 事件混进了真实转化报表。
两步处置 + 一条纪律:
- 处置:DB 交叉验证确认对应订单不存在 → 按测试机 IP 精确定位 visit → 删 visit 记录并作废归档重建报表;
- 纪律:做自动化测试时给流量打上可识别标记(专属 IP / user_id 前缀),让事后清除变成一条 DELETE 而不是考古;测试完当场清干净,这属于交付的一部分,不是可选善后。
统计库是只进不出的账本,写进去的每一行测试数据,都会在未来某次决策里被当成”证据”读出来。

小结
| 坑 | 本质 | 出路 |
|---|---|---|
| CLI 命令缺失 | 容器版功能裁剪 | DB 直写 |
| token 无法 UI 生成 | 同上 | 逆向 sha512(token+salt) 直插 |
| 登录静默失败 | 库里存了非法哈希 | bcrypt(md5(pw),12) 重写 |
| IP 全是网桥地址 | 反代头未透传 | proxy_client_headers 三行 + 逐层核对 |
| token 报错像失效 | 命令行被脱敏改写 | 凭据走文件不走命令行 |
| 报表混入假数据 | E2E 流量入生产库 | 测试打标 + 当场清除 |
自托管统计的自由度和运维量是成正比的——把这几张坑图存下来,能省下后续每一次深夜救火。



