时间:2026-09-20 20:00 → 2026-09-21 00:30 主机:NAS451DHU(QNAP TS-451D · QTS 5.2.10 · Docker 27.1.2-qnap8) 起因:Farmer 提问「web-calibre 容器新版本为什么不用?升级后服务起不来,也导致本地 Calibre 客户端不能升 9.x」
一、结论先行
| 问题 | 结论 |
|---|---|
| 新版本为什么不能用 | Calibre 9 删除了 books.isbn/lccn/flags,而 Calibre-Web ≤0.6.25 在 ORM 里硬编码映射了这些列 → 页面 500 |
| 是"服务起不来"吗 | 不是。容器正常启动,是页面报错 |
| 修好了吗 | ✅ 已升到 0.6.27-ls402,实测全绿 |
| 阻塞 Calibre 桌面 9.x 吗 | ✅ 已解锁,实测 Calibre 9.15 转换格式成功 |
| 自定义列要处理吗 | ❌ 不用(是 composite,在 Calibre-Web 忽略名单里) |
二、技术根因(源码级)
Calibre 9 做了什么
kovidgoyal/calibre → src/calibre/db/schema_upgrades.py → upgrade_version_26:
for x in {'flags', 'isbn', 'lccn'} & columns:
statements.append(f'ALTER TABLE books DROP COLUMN {x};')
同时新建 books_pages_link、重建 meta 视图、PRAGMA application_id = 0x63616c69、user_version 26 → 27。
关键特性:Calibre 打开书库时在 BEGIN EXCLUSIVE TRANSACTION 里原地升级,不可逆。
Calibre-Web 为什么会崩
- ≤0.6.25:
cps/db.py的Books模型有isbn = Column(...)/flags = Column(...)→ SQLAlchemy 生成SELECT ... books.isbn ...→no such column: books.isbn→ HTTP 500 - 0.6.26 起:commit
7f7fc884c2(2026-01-31)把这两列注释掉 → 不再查询 → 修复 - ⚠️ Calibre-Web 从不读
PRAGMA user_version(源码 5 处文件零命中)→ 无版本判断,只在查询时炸
为什么极难自查
错误只写进
/config/calibre-web.log(应用日志),不进docker logs(容器 stdout)。 所以看容器日志只会看到"一切正常启动",真正原因藏在 config 目录的日志文件里。
三、自定义列排查(虚惊一场)
custom_columns 表(1 行)
label = formats
name = 格式
datatype = composite ← 关键
display = {"composite_template": "{:'re(approximate_formats(), ',', ', ')'}", ...}
custom_column_1 表:0 行 ← composite 是计算列,不存数据
Calibre-Web cps/db.py:
cc_exceptions = ['composite', 'series']
if row.datatype not in cc_exceptions: # ← composite 直接跳过
→ composite 落在忽略名单内,不会触发 books_custom_column_N_link 查询
→ issue #3643(Calibre 9 + 自定义列 → 书详情页/OPDS 失败)对本库不适用
注:
cc_exceptions里之所以有composite,正是源于 2016 年的 cbw #20 —— 那位报告人的列也叫 formats、也是 composite,janeczku 据此把它加进了忽略名单。
四、决定性复现实验
在库副本上施加 upgrade_version_26 的等效迁移(删三列 + 重建 meta 视图 + application_id/user_version),然后同一份库分别跑新旧两个 Calibre-Web:
| URL | 0.6.20(旧) | 0.6.26(新) |
|---|---|---|
/ 首页 | 500 | 200 |
/login | 200 | 200 |
/opds | 200 | 200 |
/opds/books/letter/00 | 500 | 200 |
→ 症状精确复现,修复确认。(/login 正常是因为它不查书)
五、升级执行(三步走,每步都带回滚)
① 备份 一致快照(SQLite backup API,非裸拷贝)
→ /share/CACHEDEV1_DATA/Public/openclaw-backups/
② 沙箱验证 库副本 + config 副本 + 独立端口(8093/8094/8095)
不碰生产容器/配置/书库
③ 生产切换 docker rename 旧容器保留为回滚锚点 → 同规格起新容器
→ 等待 → 验证 → 失败自动回滚
升级链:0.6.20-ls225 → 0.6.26-ls388 → 0.6.27-ls402(当日发布,含 2026-09 安全批次)
最终验证(生产 8083):
/ 200 /login 200 /opds 200
/book/1 200 ← 旧版必崩的页面
/opds/books/letter/00 200
restarts=0 stdout 错误签名=0 app.db 21 表 / 3 用户完整保留
终局实测(最有说服力的一条)
Farmer 升 Calibre 桌面 9.15 并成功转换一本书的格式 → 书库已升到 schema 27:
user_version : 27
application_id : 0x63616c69
books_pages_link : PRESENT(1065 行)
books.isbn/lccn/flags : 全部已删除
books 行数 : 1065(一本书没丢)
而 Calibre-Web 0.6.27 完全扛住:/book/1 与 /opds/* 全 200、零错误签名。
→ 如果当初没升级,此刻这两个页面必定 500。整条链路验证通过。
六、镜像加速的真相(重要可复用结论)
registry-mirrors 只对 Docker Hub(docker.io)生效。
✅ linuxserver/calibre-web:0.6.27-ls402 → 命中加速,实测 5 秒
❌ lscr.io/linuxserver/calibre-web:0.6.27-ls402 → 绕过加速,直连境外 → 卡死
- 本机 daemon 配置:
/share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json"registry-mirrors": ["https://docker.1ms.run", "https://docker.xuanyuan.me"] - 实测
docker pull hello-world5 秒完成 → 加速本身完全正常 - 规则:拉 LinuxServer 镜像一律用 Docker Hub 路径,不要用
lscr.io(其镜像同时发布在 Hub 上) - 不需要改
daemon.json—— 改它要重启 dockerd,会导致所有容器重启,代价大而无必要 - ⚠️ 容器 Station UI 走的是 Hub + 加速,所以 UI 能拉、命令行用
lscr.io不能拉 —— 差异就在仓库路径
七、顺带完成的安全审计(8083 暴露问题)
修正我自己的错误说法
曾把这些扫描称为"境外扫描器" —— 核对 IP 后发现是错的:
MGLNDD_116.237.***.*** / 116.237.***.*** / 116.237.***.*** ← 中国电信·上海(国内)
防火墙实际状态
QuFirewall 已启用(firewall_status = 1,profile = Basic protection),规则:
IPv4 ① Allow 192.168.***.***/24 ② Allow src_country=["CN"] ③ Deny Any
IPv6 ① Allow 240e:389:8a18:c100::/64 (eth0)
② Allow fe80::/64 (eth0) ③ Allow src_country=["CN"] ④ Deny Any
🎯 答案:策略是「允许整个中国」→ 国内扫描器被合法放行,不是防火墙失效。
但发现 3 个真问题
- geo-IP 库过期:
geoip_date = 20251104(约 10.5 个月前)→ 国家判定可能失准 - 丢弃日志已死:
ulogd于 2024-09-13 退出(Terminal signal received, exiting)→ 看不到防火墙拦了什么,这正是"感觉拦不住"的根源 - 规则面过宽:允许任何中国 IP 访问任意端口(含 8083)
1 个未验证的怀疑
Docker 映射端口可能绕过主机防火墙(经典 DOCKER-USER 问题:DNAT 在
nat PREROUTING,不走 filter INPUT 常规路径)。claw无 root、可用镜像均无iptables→ 未能证实,不能当结论。
建议(按有效性)
- ⭐ 路由器层面不转发 8083(从入口掐掉最可靠)
- ⭐ 收紧"允许中国"规则(家庭 NAS 通常不需要全国可访问)
- 更新 geo-IP 库
- 恢复 ulogd 丢弃日志(否则永远无法自查)
八、我在本次工作中犯的错误(教训)
- 备份脚本路径用错:把宿主机路径当容器内路径 → 备份写进
--rm容器内部、随容器蒸发(宿主零副作用,但白做一次) - 探测方式有缺陷:
docker pull | tail会把输出缓冲到结束才显示 → 把「慢」误判成「挂死」,过早 kill - 误判镜像为假:把「拉取中途 tag 指向旧镜像」的瞬时状态判成「镜像站返回假镜像」→ 被 Farmer 纠正
- 未核对就下结论:把国内扫描 IP 说成"境外扫描器"
共同教训:下判断前先确认「状态是否已经稳定」(有没有进行中的任务),再判断数据本身。 辅助教训:
| tail这类缓冲会掩盖进度;长任务探测要有可见进度或分层确认;不要用未经核对的形容词下结论。
九、可复用的技术要点
NAS 接入
claw已在administrators组;SSH 用 SSH_ASKPASS 免交互(tmp/askpass.cmd+askpass.py,从.env的QNAP_SSH_CREDENTIAL读,密码不进模型上下文)- 脚本传输最可靠方式:把脚本 pipe 给远端
sh的 stdin(base64 会截断、sh -c引号易错) - 容器路径 vs 宿主路径必须分清(见教训 1)
- PowerShell 脚本必须纯 ASCII(PS 5.1 无 BOM 时按 GBK 读 UTF-8 中文会破坏语法)
关键路径
书库 /share/Multimedia/book/calibre-db/books/library/metadata.db
config /share/Container/calibre-svr/config/
备份 /share/CACHEDEV1_DATA/Public/openclaw-backups/
镜像配置 /share/CACHEDEV1_DATA/.qpkg/container-station/etc/docker.json
防火墙配置 /etc/config/QuFirewall.conf · QuFirewall_Profile.conf
容器规格(calibre-web-1)
image linuxserver/calibre-web:<tag>
env PUID=1000 PGID=1000
ports 8083:8083
volumes /share/Container/calibre-svr/config:/config
/share/Multimedia/book/calibre-db/books:/books
restart always
log json-file, max-file=10, max-size=10m
labels com.qnap.qcs.gpu=False · com.qnap.qcs.network.mode=nat
Container Station 不单独存容器定义(
ctstation.sqlite只有backgroundtask历史记录与 registry/keyvalue),UI 实时反映 Docker 状态 → 用docker run重建不会产生"UI 管不到的野容器"。
十、遗留事项
| 优先级 | 事项 |
|---|---|
| 🔴 | 8083 暴露 —— 路由器是否转发待确认;Docker 是否绕过防火墙待验证(需 root) |
| 🟡 | calibre 转换引擎未装 —— 镜像内无 calibre 二进制;Web 端转换不可用(需 DOCKER_MODS=linuxserver/mods:universal-calibre) |
| 🟡 | Docker 另有约 3.28 GB 可回收 —— 死容器遗留镜像(wizserver 2.37G / movie-robot 1.59G / openjdk / portainer) |
| 🟡 | movie-robot 死因未查 —— Exited (137),疑 OOM |
| 🟡 | QuFirewall 三项整改 —— geo-IP 更新 / ulogd 恢复 / 规则收紧 |
十一、最终状态
calibre-web-1 linuxserver/calibre-web:0.6.27-ls402 Up restarts=0
回滚锚点 已按 Farmer 要求清理
旧镜像 0.6.20 / 0.6.26 已删除(释放 1.47 GB)
备份 两份完整快照保留于 openclaw-backups/
