## 现象
容器跑着跑着页面全部 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 校验通过
62 行
2.7 KiB
YAML
62 行
2.7 KiB
YAML
# =============================================================================
|
||
# workbuddy-portal —— 单服务部署(采集 / 存储 / 呈现都在同一个进程里)
|
||
#
|
||
# docker compose up -d --build 本机构建并启动
|
||
# docker compose logs -f 跟踪日志
|
||
# docker compose down 停止(数据在命名卷里,不会丢)
|
||
#
|
||
# 设计取舍:
|
||
# * 刻意只有**一个**服务:SQLite 是单写者,调度线程也在 Web 进程内,
|
||
# 多副本只会带来锁竞争与重复采集,所以不做横向扩展。
|
||
# * 数据用**命名卷**而不是绑定挂载。这不是随手选的:
|
||
# Windows + Docker Desktop 走 9p 挂载,宿主的 Windows 进程一旦访问过
|
||
# 这个 WAL 库(哪怕只是 `manage.py stats` 这种纯读),容器侧下一次打开就会
|
||
# `sqlite3.OperationalError: unable to open database file`,且**不会自愈**,
|
||
# 必须重启容器。命名卷住在 Linux VM 的本地文件系统里,不存在这个问题。
|
||
# 需要在宿主机直接看到数据/日志时,叠加 docker-compose.hostdir.yml
|
||
# (只建议 Linux 宿主机用)。
|
||
# =============================================================================
|
||
name: workbuddy-portal
|
||
|
||
services:
|
||
portal:
|
||
# 默认就用 Gitea 注册表里的名字,构建完即可直接 push,不用再补 tag
|
||
image: ${WB_IMAGE:-git.iwali.top/wangchuanli/workbuddy-portal:latest}
|
||
build:
|
||
context: .
|
||
dockerfile: Dockerfile
|
||
container_name: workbuddy-portal
|
||
restart: unless-stopped
|
||
init: true # tini 接管 PID 1:docker stop 能干净地传到 python
|
||
ports:
|
||
- "${WB_BIND:-0.0.0.0}:${WB_PORT:-8848}:8848"
|
||
environment:
|
||
TZ: ${TZ:-Asia/Shanghai} # 决定「每日 09:00 / 17:00」调度与日志时间戳
|
||
WB_HOST: 0.0.0.0
|
||
WB_PORT: "8848"
|
||
WB_ADMIN_USER: ${WB_ADMIN_USER:-admin}
|
||
WB_ADMIN_PASSWORD: ${WB_ADMIN_PASSWORD:-}
|
||
WB_DISABLE_SCHEDULER: ${WB_DISABLE_SCHEDULER:-0}
|
||
WB_IMPORT_CREDS: ${WB_IMPORT_CREDS:-0}
|
||
WB_IMPORT_XLSX: ${WB_IMPORT_XLSX:-}
|
||
volumes:
|
||
- wb_data:/app/data # 数据正本 + 导出 + secret_key
|
||
- wb_logs:/app/logs # 应用日志(滚动 2 MB × 3)
|
||
# 可选:把编辑器配置挂进来,配合 WB_IMPORT_CREDS=1 自动接管 cookie
|
||
# - ${WB_EDITOR_SETTINGS:-./nonexistent.json}:/mnt/editor-settings.json:ro
|
||
healthcheck:
|
||
test: ["CMD", "python", "/app/docker/healthcheck.py"]
|
||
interval: 30s
|
||
timeout: 6s
|
||
start_period: 20s
|
||
retries: 3
|
||
logging:
|
||
driver: json-file
|
||
options:
|
||
max-size: "10m"
|
||
max-file: "3"
|
||
|
||
volumes:
|
||
wb_data:
|
||
wb_logs:
|