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
父节点 dcd420e9c0
当前提交 10db94c162
共修改 8 个文件,包含 281 行新增和 63 行删除
+45 -11
查看文件
@@ -32,13 +32,34 @@ docker compose exec portal python /app/docker/healthcheck.py
### Q:`unable to open database file`
Linux 宿主机上宿主目录属主与容器内 uid 1000 不一致:
分三种情况,先判断你用的是哪种挂载:
| 场景 | 原因 | 处理 |
|---|---|---|
| 默认(命名卷) | 极少见;多半是卷被误删或磁盘满 | `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` 这种纯读**——
容器侧下一次打开数据库就会失败,**而且不会自愈**。
```bash
sudo chown -R 1000:1000 ./data ./logs
docker compose restart
docker compose restart portal # 临时恢复(但宿主再碰一次还会坏)
```
**根治**:不要用 `hostdir` 叠加层,直接用默认的命名卷;需要跑 CLI 就:
```bash
docker compose exec portal python manage.py stats
```
完整复现步骤与原理见
[部署与运维指南](DEPLOYMENT.md#windows-绑定挂载的坑容器打不开数据库)。
### Q:`database is locked` / `disk I/O error`
| 报错 | 原因 |
@@ -206,16 +227,28 @@ docker compose exec portal python manage.py passwd admin 新密码
### Q:备份怎么做最稳
数据在命名卷 `workbuddy-portal_wb_data` 里。先 checkpoint 再拷出单个 `.sqlite`:
```bash
# 1) 先 checkpoint,把 WAL 落进主库
docker compose exec portal python -c "
from workbuddy_portal import db
db.connect().execute('PRAGMA wal_checkpoint(TRUNCATE)')"
# 2) 拷走
cp data/usage.sqlite ~/backup/usage-$(date +%F).sqlite
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
```
或者整体 `tar` 掉 `data/`(含 `-wal` / `-shm`)——**别把新库配旧 WAL 用**,那会损坏数据。
整体打包(含 `instance.json`、`exports/`):
```bash
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](DEPLOYMENT.md#63-恢复)。
**别把新库配旧 WAL 用**——会损坏数据(用上面已 checkpoint 的单文件备份最省心)。
### Q:数据库文件越来越大
@@ -228,16 +261,17 @@ docker compose exec portal python manage.py vacuum
### Q:升级会不会丢数据
不会。数据在宿主机的 `data/`(绑定挂载),`docker compose up -d --build` 只重建容器。
但**升级前依然要备份**:`schema.sql` 用 `CREATE TABLE IF NOT EXISTS`,
不会。数据在 Docker 命名卷 `workbuddy-portal_wb_data` 里(对应容器内 `/app/data`),
`docker compose up -d --build` 只重建容器,不碰卷。
但**升级前依然要备份**(见上一条):`schema.sql` 用 `CREATE TABLE IF NOT EXISTS`,
加表加索引安全,**改列需要手工迁移**。
### Q:日志在哪、怎么滚动
| 位置 | 内容 |
|---|---|
| `logs/app.log`(挂载到宿主机) | 应用日志,滚动 2 MB × 3 |
| `docker compose logs` | 容器 stdout(entrypoint + waitress) |
| `docker compose logs portal` | 容器 stdout(entrypoint + waitress) |
| 命名卷 `workbuddy-portal_wb_logs` 里的 `app.log` | 应用日志,滚动 2 MB × 3 |
| 页面「日志管理」 | 采集逐行日志 + 应用日志尾部 + 操作审计 |
### Q:想改采集的接口地址(走镜像/代理)