这次 Part-DB 的问题并不是一次单纯的软件 Bug,
而是一次 Docker 虚拟机崩溃后的隐性权限问题。表面看是数据库损坏、版本不兼容、迁移失败,
实际根因却非常底层:群晖 ACL 权限在崩溃修复后发生变化,导致 SQLite 实际不可写。
一、背景说明
Part-DB 是我长期使用的元器件管理系统,
运行环境为 群晖 NAS + Docker。
系统原本稳定运行,数据库规模不大,但数据非常重要:
- 元器件信息
- 分类、封装、库存
- 附件图片与文档
这些数据都是手动录入库存的,非常繁琐。
问题的起点并不是 Part-DB 本身,而是NAS迁移带来的后遗症:
- Part-DB 页面可以访问、数据可以读取
但从这次崩溃之后,一个致命问题开始出现:
所有涉及“写入”的操作全部失败
—— 新增、编辑、出入库,统一 500 Internal Server Error
而读取操作(浏览、搜索、查看详情)完全正常。
二、表象问题与误导性症状
从表面来看,这可能是数据库损毁导致的。
主要表现为:
- 编辑元器件时报
500 Internal Server Error - 出入库操作直接失败
- 登录后部分操作触发 500,但页面仍可正常浏览
- 没有明显的前端错误提示
更具迷惑性的是后台现象:
- Doctrine 报错堆栈混杂在 Symfony 事件中
- 有时提示数据库异常
- 有时迁移命令可以执行,但功能仍然不可用
doctrine:schema:validate显示 Schema 正常- SQLite 文件 可以被读取
这些症状非常容易把人带到错误方向:
- 怀疑数据库损坏
- 怀疑备份不完整
- 怀疑 Docker 镜像问题
- 怀疑 Part-DB 版本不兼容
- 怀疑 Doctrine / Symfony Bug
实际上,这些判断 都“看起来合理”,但全部偏离了真正原因。
三、错误排查过程(走过的弯路)
在真正找到原因之前,我几乎把所有“看起来合理”的方向都排查了一遍。
1. 怀疑数据库损坏
第一反应是 SQLite 数据库在崩溃中被破坏:
- 手动用 PDO 连接
app.db - 能正常读取表结构
- 表数据完整存在
- 简单
SELECT、INSERT测试可执行
这一步已经很诡异:
“程序写不了,但我手动能写。”
2. 怀疑版本不兼容
随后发现一个关键信息:
- 旧环境:Part-DB 1.16.1
- 当前镜像:Part-DB 2.2.1
于是开始怀疑:
是否数据库结构跨大版本不兼容?
为此我做了大量尝试:
- 回退 Docker 镜像到旧版本
- 尝试执行
doctrine:migrations:migrate - 尝试新建实例再导入数据库
- 对比 migration 表与 schema 状态
结果依然是:
- 迁移命令有时成功
- 页面依旧无法写入
3. 怀疑缓存与权限污染
期间还反复出现一些“烟雾弹”问题:
WebProfilerBundle not foundcache:clear在 dev 环境直接报错- prod 环境缓存清了又生
var/log/prod.log有时甚至不存在
这些问题看起来像:
- 缓存目录权限异常
- 之前用 root 执行过 console 命令
但即便彻底清空缓存、重建,也无法解决根本问题。
4. 尝试备份恢复
我还做了最后的“重锤测试”:
- 使用旧版本生成的完整备份
- 解压后手动替换
app.db - 校验表结构、数据完整性
- 重启容器
结果:读取正常,写入依旧 500。
到这里,几乎所有“软件层面”的可能性都已经被否定。
四、真正的根因:新版本群晖系统的ACL权限管理
最终的原因,说出来甚至有点让人哭笑不得。
问题不在 Part-DB,不在 Docker,也不在数据库结构。
而是出在 群晖文件系统的 ACL 权限 上。
发生了什么
在那次 Docker 虚拟机崩溃并修复之后
我把群晖DSM系统从7.0更新到了7.2,群晖的ACL权限管理机制发生了变化:
- Part-DB 的数据目录仍然“看起来正常”
- 文件属主是
www-data - 权限位是
664 / 775 - SQLite 文件本身可以被 PHP 读取
但实际上:
群晖在底层 ACL 中,悄悄移除了 everyone 的写权限这是一个非常难以察觉的状态:
Docker 容器内:
- 文件属主、权限位看起来完全没问题
- 甚至我还给了这个文件夹docker的权限,也无济于事
群晖宿主机层面:
- 目录对容器挂载点是“只读式可写”
为什么症状如此迷惑
这也解释了之前所有“反直觉”的现象:
- ✅
SELECT正常(读取不受影响) - ❌ 所有业务写操作失败
- ❌ Doctrine 抛出各种抽象异常
- ❌ SQLite 报
unable to open database file - ❌ 迁移、出入库、编辑统一 500
- ❌ 手动测试偶尔成功,业务流程失败
因为业务写入并不只是写一个文件:
- SQLite 会创建临时文件
- 会写 WAL / journal
- 会修改目录元数据
而这些操作,全部被 ACL 在宿主层面拦截了。
五、最终解决方式
真正的解决方式,并不在容器内。
一切在容器里反复折腾的行为,本质上都是在绕开根因。
正确操作位置
👉 群晖 DSM 文件管理器
定位到 Part-DB 的数据目录(Docker 挂载目录),例如:
docker/Part-DB/- 或你实际映射到
/var/www/html/var的宿主路径
关键一步
在 DSM 中:
- 右键 Part-DB 数据目录
- 进入 属性 / 权限
- 切换到 ACL 权限
- 找到
everyone 勾选:
- 读取
- 写入
- 遍历文件夹 / 执行
应用到:
- 当前文件夹
- 子文件夹
- 所有文件

不可思议的是做到这一步,几个小时已经过去了......
修复效果
权限修改完成后:
- 所有 500 错误立刻消失
- 元器件可正常编辑
- 出入库恢复正常
- Doctrine / Symfony 不再报错
- SQLite 行为恢复完全正常
数据库从来没有坏过。
六、经验教训与总结
这次事故最大的教训,并不在技术难度,而在“认知盲区”。
1. Docker ≠ 权限隔离的终点
很多时候我们会潜意识认为:
只要容器内权限正确,一切就没问题。甚至说DSM给了docker权限就没有问题。
但现实是:
- Docker 的文件系统权限,最终由宿主机决定
- ACL 权限可以绕过传统的
chmod / chown - 容器内看到的“可写”,并不等于真正可写
2. SQLite 对权限异常极其敏感
SQLite 的特点决定了:
- 业务写入 ≠ 单纯写一个文件
- 会频繁创建 / 删除临时文件
- 会操作目录本身
因此:
- ACL 少一个权限位,症状就会非常诡异
- 错误往往被“包装”成 ORM / 框架异常
3. 虚拟机崩溃后的“隐性后遗症”
这次问题的真正诱因是:
Docker 虚拟机崩溃 → 群晖修复 → ACL 被悄然修改
这种情况:
- 不会有任何明显提示
- 不会影响读取
- 只在写入时集中爆发
4. 排错顺序的重要性
如果再来一次,我会按以下顺序排查:
- 宿主机挂载目录是否真的可写(ACL)
- 再看容器内权限
- 再看数据库结构
- 最后才是应用版本和迁移
5. 一句话总结
**当数据库“看起来坏了”,但一切又说不通时,
请第一时间检查宿主机权限,而不是数据库本身。**
这是一次浪费了大量时间的事故,但也换来了一次非常扎实的经验。
