文件
workbuddy-portal/docs/FAQ.md
T
wangchuanli 10db94c162 fix(docker): 数据改用 Docker 命名卷,修容器打不开数据库的问题
## 现象
容器跑着跑着页面全部 500,应用日志:
  sqlite3.OperationalError: unable to open database file
  (db.py:33, conn.execute("PRAGMA journal_mode=WAL"))

## 根因(已最小复现)
数据原本用绑定挂载(./data:/app/data)。Windows + Docker Desktop 的绑定挂载走 9p
(mount 里是 type 9p, aname=drvfs;path=C:\)。9p 本身支持 WAL(新建库能开 WAL),
但**宿主的 Windows 进程打开过这个 WAL 库之后**,容器侧缓存的 -shm 映射就失效,
下一次连接无法重建共享内存文件 → 打不开数据库,且**不会自愈**。

复现:
  docker compose -f docker-compose.yml -f docker-compose.hostdir.yml up -d
  docker compose exec portal python -c "..."     # OK, 1665 条
  python manage.py stats                        # 宿主侧纯读一次
  docker compose exec portal python -c "..."     # ERR unable to open database file
  # 只有 docker compose restart portal 才恢复

## 处理
- docker-compose.yml 改用命名卷 wb_data / wb_logs(容器独占 /app/data)
- 新增 docker-compose.hostdir.yml 叠加层:需要宿主目录时用
  docker compose -f docker-compose.yml -f docker-compose.hostdir.yml up -d
  (注明**只建议 Linux**;Linux 的 bind mount 与容器同一文件系统,无此问题)
- 数据迁移:docker run --rm -v workbuddy-portal_wb_data:/to -v "$PWD/data":/from:ro \
    alpine:3.20 sh -c 'cp -a /from/. /to/'
- 文档同步:DEPLOYMENT 2.4/5.4/第六节全部改为命名卷 + 备份恢复用 docker run;
  新增第九节「Windows 绑定挂载的坑」(含复现步骤);FAQ、USER-GUIDE、README、CHANGELOG 同步

## 验证
- 宿主跑 manage.py stats 与 smoke.py 之后,容器侧仍能正常读写(此前会立刻失效)
- 容器实例 check_live 56/56;离线 smoke 99/99;hostdir 叠加层 config 校验通过
2026-09-14 15:09:24 +08:00

12 KiB
原始文件 Blame 文件历史

常见问题(FAQ)

按「现象」分类。每条都给出原因和动作,不用从头排查。


一、部署

Q:docker compose up 报端口被占用

Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:8848

改 .env 里的 WB_PORT,比如 WB_PORT=18848,然后 docker compose up -d。 容器内始终监听 8848,只改宿主映射即可。

Q:容器起来了但局域网访问不了

  1. .env 的 WB_BIND 是不是被设成了 127.0.0.1(那只允许本机);
  2. 服务器防火墙有没有放行该端口;
  3. docker compose ps 的 PORTS 是不是 0.0.0.0:8848->8848/tcp。

Q:容器 unhealthy 但 Up

docker compose exec portal python /app/docker/healthcheck.py

健康检查打的是 /login(唯一免登录页),拿到 200 才算健康。 若失败,看 docker compose logs 有没有 Python traceback。

Q:unable to open database file

分三种情况,先判断你用的是哪种挂载:

场景 原因 处理
默认(命名卷) 极少见;多半是卷被误删或磁盘满 docker compose exec portal ls -la /app/data;docker compose exec portal df -h /app/data
用了 docker-compose.hostdir.yml(Linux) 宿主目录属主与容器内 uid 1000 不一致 sudo chown -R 1000:1000 ./data ./logs
用了 docker-compose.hostdir.yml(Windows / Docker Desktop) 9p 挂载的固有缺陷,见下条 换成默认的命名卷

Q:容器一开始好的,跑着跑着页面全 500,日志里 unable to open database file

这是 Windows + Docker Desktop 用绑定挂载(hostdir 叠加层)时的典型症状。

宿主机的 Windows 进程只要访问过这个 WAL 库——哪怕只是 manage.py stats 这种纯读—— 容器侧下一次打开数据库就会失败,而且不会自愈。

docker compose restart portal          # 临时恢复(但宿主再碰一次还会坏)

根治:不要用 hostdir 叠加层,直接用默认的命名卷;需要跑 CLI 就:

docker compose exec portal python manage.py stats

完整复现步骤与原理见 部署与运维指南。

Q:database is locked / disk I/O error

报错 原因
database is locked 有另一个写者(另一个容器实例?宿主机上同时在跑 manage.py collect?)。等它结束——文件锁会串行化,但 SQLite 层面的写冲突仍会短暂报错
disk I/O error 挂载的文件系统不支持 SQLite 需要的锁语义(某些 NFS / 网络盘)。改用本地盘或 Docker 命名卷

Q:exec format error 或 no such file or directory(关于 entrypoint.sh)

docker/entrypoint.sh 被 CRLF 污染了。仓库 .gitattributes 已强制 *.sh 为 LF; 若手工传过文件:

python -c "p='docker/entrypoint.sh';d=open(p,'rb').read();open(p,'wb').write(d.replace(b'\r\n',b'\n'))"

Q:能不能跑多个副本做高可用

不要。 三个理由:SQLite 是单写者;调度线程在 Web 进程内;文件锁只在本机有效。 真要跑多副本,除第一份外全部设 WB_DISABLE_SCHEDULER=1,但写冲突依然存在。 这个服务的正确扩展方式是「升级到更强的单机」,不是横向加副本。


二、采集

Cookie 过期。重新获取(见 用户手册 3.2), 填进「配置管理 → 凭证」,保存后按区间补采。

Cookie 通常是浏览器会话级,关掉浏览器可能就失效。从已登录浏览器复制时勾选「保持登录」。

Q:采集成功但「新增 0 条」

大概率正常。看那一次的 抓取 条数:

抓取 新增 判断
> 0 0 云端返回的都是库里已有的(断点回退窗口重叠)——正常
0 0 该时段云端确实没有记录——正常
> 0 > 0 正常采集到新数据

Q:返回 409 busy

已有采集在跑。文件锁 data/collect.lock 保证同时只有一个采集。 到「任务管理 → 运行历史」看它是否还在 running,等结束再操作。

Q:TLS / SSLError

企业代理或自签证书场景。两种处理:

  1. 推荐:把企业根证书装进系统信任链;
  2. 临时:「配置管理」把 ssl_verify 设为 0。

Cookie 就是账号凭证,关掉证书校验等于把它暴露在中间人面前。只在受控内网临时用。

Q:日志里出现「云端时间比本地早」告警

云端记录的 cloud_ts 比本地 ts 早超过 drift_tolerance_minutes(默认 5 分钟)。 影响不大,但可能意味着:

  • 服务端时钟不准 → 校时;
  • 云端写入有延迟 → 适当调大 rewind_minutes(比如 5)。

Q:想让采集更频繁 / 更稀疏

「任务管理 → 每日时刻」改成任意逗号分隔的时刻,如 08:30,12:30,18:00,22:00。 保存即生效,不用重启。时刻按本地时区解释。


三、数据与日期

Q:日期差一天 / 跨日数据落到相邻日期

时区问题。所有日期按服务端本地时区计算。

docker compose exec portal date        # 期望:CST / +0800
docker compose exec portal python -c "from workbuddy_portal import db; print(db.now_str())"

若不是,检查 .env 的 TZ=Asia/Shanghai,然后 docker compose up -d 重建 (TZ 是环境变量,restart 不生效)。

Q:导出的 CSV 在 Excel 里乱码

本系统导出的文件带 UTF-8 BOM,双击不会乱码。若乱码,先确认你打开的是 从「导出 CSV」拿到的文件,而不是用记事本另存过的版本。

Q:大屏的「单笔 TOP」为什么切了日期区间也不变

设计如此。 top 是全局的:如果跟着窗口变,排名会随筛选跳动,反而看不出长期最贵的那几条。 想要窗口内的排行,用「数据明细」按积分降序 + 日期筛选。

Q:大屏的 daily(日历/趋势)为什么不受区间影响

也是设计如此:daily 是全量(约 200 B/天),日历与日期轴需要完整日期序列。 真正跟随窗口的是 dims / totals / 明细表。

Q:明细表提示「只显示最近 N 条」

大屏下发的明细有上限(recordsCap = 20000)。超过时只发最新 N 条并置 recordsTruncated=true,页面据此提示。完整数据请到「数据明细」页筛选或导出。


四、界面

Q:页面能开但图表全白

  1. Ctrl+F5 强刷清缓存;
  2. 开浏览器控制台看有没有资源 404;
  3. 「日志管理 → 应用日志」看有没有异常栈。

历史上有过 /dashboard 下相对路径把 echarts.min.js 解析成 /vendor/... 导致 整页全白的问题,已在 tools/smoke.py 里加了「页面所有 src/href 资源逐个断言 200」防回归。

Q:某块样式突然失效 / 文字发虚

多半是类名撞了全局样式。本项目约定:新组件用带前缀的独有类名 (如 .calcell .cbar,而不是含糊的 .bar)。 smoke.py 里有「页面 class ∩ app.css 选择器」差集断言,跑一遍就能发现异常类名。

Q:登录后跳转目标丢了

历史 bug,已修。现在 next 参数在 GET/POST 两条路径上都正确回填。 注意开放重定向防护会拒绝站外目标://evil.com、/\evil.com、https://evil.com 一律回落到 /——这是预期行为。


五、账号与权限

Q:忘记管理员密码

# 裸机
python manage.py passwd admin 新密码

# Docker
docker compose exec portal python manage.py passwd admin 新密码

不传新密码时会用默认的 admin123——别这么干。

Q:怎么给同事开只读账号

「用户管理 → 新建账号」,不要勾「管理员」。 普通用户能看所有页面、能导出、能触发采集,但看不到「用户管理」且访问 /users 返回 403。

Q:不小心把自己降级 / 删掉自己了

做不到。服务端有三条护栏:不能取消自己的管理员身份、不能删除自己、至少保留一个账号。

Q:所有人被踢下线了

SECRET_KEY 变了。它存在 data/instance.json。这个文件丢了/被删了就会重新生成, 所有会话失效(数据不受影响)。恢复办法:从备份里找回 instance.json,或让大家重新登录。


六、运维

Q:备份怎么做最稳

数据在命名卷 workbuddy-portal_wb_data 里。先 checkpoint 再拷出单个 .sqlite:

docker compose exec portal python -c "
from workbuddy_portal import db
db.connect().execute('PRAGMA wal_checkpoint(TRUNCATE)')"

docker run --rm \
  -v workbuddy-portal_wb_data:/data:ro \
  -v "$PWD/backup":/backup \
  alpine:3.20 cp /data/usage.sqlite /backup/usage-$(date +%F).sqlite

整体打包(含 instance.json、exports/):

docker run --rm -v workbuddy-portal_wb_data:/data:ro -v "$PWD/backup":/backup \
  alpine:3.20 tar czf /backup/wb-data-$(date +%F).tar.gz -C /data .

恢复见 部署指南 6.3。 别把新库配旧 WAL 用——会损坏数据(用上面已 checkpoint 的单文件备份最省心)。

Q:数据库文件越来越大

docker compose exec portal python manage.py vacuum

或在「配置管理 → 维护动作 → 整理数据库」点一下。 作用是 wal_checkpoint(TRUNCATE) + VACUUM,回收删除后的空闲页并压缩 WAL。

Q:升级会不会丢数据

不会。数据在 Docker 命名卷 workbuddy-portal_wb_data 里(对应容器内 /app/data), docker compose up -d --build 只重建容器,不碰卷。 但升级前依然要备份(见上一条):schema.sql 用 CREATE TABLE IF NOT EXISTS, 加表加索引安全,改列需要手工迁移。

Q:日志在哪、怎么滚动

位置 内容
docker compose logs portal 容器 stdout(entrypoint + waitress)
命名卷 workbuddy-portal_wb_logs 里的 app.log 应用日志,滚动 2 MB × 3
页面「日志管理」 采集逐行日志 + 应用日志尾部 + 操作审计

Q:想改采集的接口地址(走镜像/代理)

「配置管理」里改 api_base 与 api_path。改了之后记得同步确认 Cookie 是该域下的有效凭证。


七、开发

Q:改完代码怎么验证

五层,前两层必须跑绿:

python tools/smoke.py                                        # 离线回归 99 项
python manage.py serve --port 8849 --no-scheduler            # 另开终端
python tools/check_live.py --base http://127.0.0.1:8849      # 真实 HTTP 56 项
python tools/shots.py    --base http://127.0.0.1:8849 --full # 界面截图 + JS 报错

详见 架构说明 · 验证体系。

Q:ModuleNotFoundError: No module named 'flask'

选错解释器了。依赖装在项目的 venv 或托管环境里:

python -c "import flask, sys; print(sys.executable, flask.__version__)"

报这个错说明当前 python 不是装了依赖的那个。

Q:改模板后页面没变

本项目 TEMPLATES_AUTO_RELOAD=True,模板改动通常立即生效。 若是静态资源(CSS/JS)被浏览器缓存,Ctrl+F5。

Q:写了个自定义接口结果 500,但日志只看到异常栈

先看是不是流式响应(Response(gen()) / stream_with_context): Flask 在返回 app_iter 之后就关掉了请求上下文里的连接, 生成器里若复用 db.get_db() 会报 Cannot operate on a closed database。 正确做法是在生成器内部 db.connect() 自建连接并 finally 关闭。 详见 架构说明 · 已知坑。