## 现象
容器跑着跑着页面全部 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 校验通过
633 行
22 KiB
Markdown
633 行
22 KiB
Markdown
# 部署与运维指南
|
||
|
||
> 面向**运维 / 部署者**。从零到跑起来,以及跑起来之后的备份、升级、排错。
|
||
|
||
**目录**
|
||
|
||
- [一、部署方式怎么选](#一部署方式怎么选)
|
||
- [二、Docker Compose 部署](#二docker-compose-部署)
|
||
- [三、裸机部署](#三裸机部署)
|
||
- [四、反向代理与 HTTPS](#四反向代理与-https)
|
||
- [五、把镜像推到 Gitea 注册表](#五把镜像推到-gitea-注册表)
|
||
- [六、备份与恢复](#六备份与恢复)
|
||
- [七、升级与回滚](#七升级与回滚)
|
||
- [八、日常巡检](#八日常巡检)
|
||
- [九、排错](#九排错)
|
||
- [十、配置项速查](#十配置项速查)
|
||
|
||
---
|
||
|
||
## 一、部署方式怎么选
|
||
|
||
| 场景 | 建议 |
|
||
|---|---|
|
||
| 有 Docker(NAS / 服务器 / 本机 Docker Desktop) | **Docker Compose**,最省事,升级只需换镜像 |
|
||
| 不想装 Docker,或要跑在 Windows 上用系统计划任务兜底 | 裸机 Python + `waitress` |
|
||
| 想给多人访问 | 任一种方式 + 反向代理(加 HTTPS 更稳) |
|
||
|
||
> **不要横向扩展**。SQLite 是单写者,调度线程也在 Web 进程内,
|
||
> 多副本只会带来锁竞争和重复采集。这个服务天然是单实例的。
|
||
|
||
---
|
||
|
||
## 二、Docker Compose 部署
|
||
|
||
### 2.1 前置
|
||
|
||
- Docker Engine 20.10+ / Docker Desktop(含 Compose v2)
|
||
- 至少 200 MB 磁盘(镜像 152 MB + 数据)
|
||
|
||
### 2.2 步骤
|
||
|
||
```bash
|
||
git clone https://git.iwali.top/wangchuanli/workbuddy-portal.git
|
||
cd workbuddy-portal
|
||
|
||
cp .env.example .env
|
||
vi .env # 至少设置 WB_ADMIN_PASSWORD
|
||
|
||
docker compose up -d --build
|
||
docker compose ps # 等 STATUS 变成 (healthy)
|
||
docker compose logs -f
|
||
```
|
||
|
||
浏览器打开 `http://<服务器IP>:8848`。
|
||
|
||
### 2.3 `.env` 主要变量
|
||
|
||
| 变量 | 默认 | 说明 |
|
||
|---|---|---|
|
||
| `WB_BIND` | `0.0.0.0` | 宿主机绑定地址。只想本机访问就设 `127.0.0.1` |
|
||
| `WB_PORT` | `8848` | 宿主机端口 |
|
||
| `TZ` | `Asia/Shanghai` | **影响「每日 09:00/17:00」与所有日期口径** |
|
||
| `WB_ADMIN_USER` | `admin` | 首个管理员用户名(只在库为空时生效) |
|
||
| `WB_ADMIN_PASSWORD` | 空 | 首个管理员密码。**留空会用 `admin123`**,务必显式设置 |
|
||
| `WB_DISABLE_SCHEDULER` | `0` | `1` = 不启动调度线程(只跑手动采集) |
|
||
| `WB_IMPORT_CREDS` | `0` | `1` = 启动时尝试从挂载的编辑器配置导入 Cookie |
|
||
|
||
### 2.4 数据落点:用命名卷,不用绑定挂载
|
||
|
||
| 容器内 | 存放位置 | 内容 |
|
||
|---|---|---|
|
||
| `/app/data` | Docker 命名卷 `workbuddy-portal_wb_data` | `usage.sqlite`(正本)、`instance.json`(secret_key)、`exports/` |
|
||
| `/app/logs` | Docker 命名卷 `workbuddy-portal_wb_logs` | `app.log`(滚动 2 MB × 3) |
|
||
|
||
**为什么是命名卷而不是脚本目录里的 `./data`**(这不是随手选的):
|
||
|
||
> Windows + Docker Desktop 的绑定挂载走 **9p**(`aname=drvfs;path=C:\`)。
|
||
> 宿主的 Windows 进程只要访问过这个 WAL 库——**哪怕只是 `manage.py stats` 这种纯读**——
|
||
> 容器侧下一次打开就会 `sqlite3.OperationalError: unable to open database file`,
|
||
> 而且**不会自愈**,必须 `docker compose restart portal`。实测复现见
|
||
> [第九节「Windows 绑定挂载的坑」](#windows-绑定挂载的坑容器打不开数据库)。
|
||
|
||
命名卷住在 Linux VM 的本地文件系统里,不存在跨文件系统翻译的问题,
|
||
所以容器独占数据目录是**唯一在所有平台上都正确**的做法。
|
||
|
||
**代价**:宿主机的 `manage.py` 不能直接读容器里的库了。随之而来的三件事都有一行命令替代:
|
||
|
||
```bash
|
||
# 在容器里跑 CLI(推荐,始终打到同一份数据)
|
||
docker compose exec portal python manage.py stats
|
||
docker compose exec portal python manage.py vacuum
|
||
docker compose exec portal python manage.py passwd admin 新密码
|
||
|
||
# 看日志
|
||
docker compose logs -f portal
|
||
|
||
# 备份 / 恢复:见第六节
|
||
```
|
||
|
||
#### 想要「宿主机直接看到数据/日志」怎么办
|
||
|
||
叠加 `docker-compose.hostdir.yml` 即可:
|
||
|
||
```bash
|
||
docker compose -f docker-compose.yml -f docker-compose.hostdir.yml up -d
|
||
# 可用 WB_HOST_DATA_DIR / WB_HOST_LOG_DIR 指定目标目录
|
||
```
|
||
|
||
> ⚠️ **只建议 Linux 宿主机使用**(bind mount 与容器是同一个文件系统,
|
||
> SQLite 的 WAL 与文件锁语义正常)。Windows + Docker Desktop 下会踩上面那个坑。
|
||
>
|
||
> Linux 首次运行若报 `unable to open database file`,是宿主目录属主与容器内 uid 1000 不一致:
|
||
> ```bash
|
||
> sudo chown -R 1000:1000 ./data ./logs
|
||
> ```
|
||
|
||
### 2.5 常用命令
|
||
|
||
```bash
|
||
docker compose ps
|
||
docker compose logs -f --tail=100
|
||
docker compose restart
|
||
docker compose down # 停并删容器,数据保留
|
||
docker compose up -d --build # 改完代码重新构建
|
||
|
||
# 在容器里跑 CLI(同一个数据卷)
|
||
docker compose exec portal python manage.py stats
|
||
docker compose exec portal python manage.py status
|
||
docker compose exec portal python manage.py passwd admin 新密码
|
||
docker compose exec portal python manage.py vacuum
|
||
docker compose exec portal python manage.py collect # 手动采集一次
|
||
```
|
||
|
||
> **本机调试**(Docker Desktop on Windows)已实测:命名卷上的 SQLite(WAL)读写正常,
|
||
> 启动补采、采集、导出、CSV 流式下载、健康检查全部可用;
|
||
> 且**宿主侧再跑 `manage.py` / `tools/smoke.py` 都不会影响容器**(这正是改用命名卷的原因)。
|
||
|
||
---
|
||
|
||
## 三、裸机部署
|
||
|
||
### 3.1 Windows
|
||
|
||
```bat
|
||
git clone https://git.iwali.top/wangchuanli/workbuddy-portal.git
|
||
cd workbuddy-portal
|
||
py -3 -m venv .venv
|
||
.venv\Scripts\pip install -r requirements.txt
|
||
|
||
.venv\Scripts\python manage.py init --user admin --password 你的强密码
|
||
.venv\Scripts\python manage.py serve
|
||
```
|
||
|
||
开机自启用「任务计划程序」:触发器「计算机启动时」,操作
|
||
`<项目路径>\.venv\Scripts\python.exe`,参数 `manage.py serve`,起始位置设为项目目录。
|
||
|
||
### 3.2 Linux
|
||
|
||
```bash
|
||
git clone https://git.iwali.top/wangchuanli/workbuddy-portal.git
|
||
cd workbuddy-portal
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install -r requirements.txt
|
||
.venv/bin/python manage.py init --user admin --password 你的强密码
|
||
```
|
||
|
||
`/etc/systemd/system/workbuddy-portal.service`:
|
||
|
||
```ini
|
||
[Unit]
|
||
Description=WorkBuddy Portal
|
||
After=network-online.target
|
||
Wants=network-online.target
|
||
|
||
[Service]
|
||
Type=simple
|
||
User=workbuddy
|
||
WorkingDirectory=/opt/workbuddy-portal
|
||
Environment=TZ=Asia/Shanghai
|
||
ExecStart=/opt/workbuddy-portal/.venv/bin/python manage.py serve --host 0.0.0.0 --port 8848
|
||
Restart=always
|
||
RestartSec=5
|
||
|
||
[Install]
|
||
WantedBy=multi-user.target
|
||
```
|
||
|
||
```bash
|
||
sudo systemctl daemon-reload
|
||
sudo systemctl enable --now workbuddy-portal
|
||
sudo systemctl status workbuddy-portal
|
||
journalctl -u workbuddy-portal -f
|
||
```
|
||
|
||
> **不要**用 `manage.py collect` + cron 替代内置调度,除非你确实想让调度留在外部
|
||
> (那种情况下 Web 端要加 `--no-scheduler`,避免和 cron 抢锁——虽然文件锁会保证正确性,
|
||
> 但会白跑一次)。
|
||
|
||
---
|
||
|
||
## 四、反向代理与 HTTPS
|
||
|
||
前面挂 nginx 时要注意两点,否则会踩坑:
|
||
|
||
```nginx
|
||
server {
|
||
listen 443 ssl;
|
||
server_name portal.example.com;
|
||
|
||
ssl_certificate /etc/ssl/certs/portal.crt;
|
||
ssl_certificate_key /etc/ssl/private/portal.key;
|
||
|
||
location / {
|
||
proxy_pass http://127.0.0.1:8848;
|
||
proxy_set_header Host $host;
|
||
proxy_set_header X-Real-IP $remote_addr;
|
||
# 必须透传:登录失败限速按真实 IP 计数,否则所有请求都算到代理头上
|
||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||
proxy_set_header X-Forwarded-Proto $scheme;
|
||
|
||
# 导出 CSV 已带 X-Accel-Buffering: no,这里关掉代理缓冲才能边查边吐
|
||
proxy_buffering off;
|
||
proxy_read_timeout 300s; # 采集/导出可能跑几分钟
|
||
}
|
||
}
|
||
```
|
||
|
||
排错要点:
|
||
|
||
| 现象 | 原因 |
|
||
|---|---|
|
||
| 登录限速「误伤」所有人 | 没透传 `X-Forwarded-For` |
|
||
| 导出 CSV 要等很久才出第一个字节 | 没关 `proxy_buffering` |
|
||
| 手动采集走到 504 | `proxy_read_timeout` 太短 |
|
||
|
||
---
|
||
|
||
## 五、把代码与镜像推到 Gitea
|
||
|
||
Gitea 自带容器注册表(`registry/2.0`)。代码仓库与镜像的目标都是 `git.iwali.top/wangchuanli/workbuddy-portal`。
|
||
|
||
### 5.0 一条命令推代码 + 推镜像
|
||
|
||
准备好 Gitea **Access Token**(「设置 → 应用 → 生成令牌」,勾选 `repo` + `write:package`)后:
|
||
|
||
```bash
|
||
export GITEA_TOKEN=<你的令牌>
|
||
tools/push-all.sh # 推 main 分支 + 镜像 latest
|
||
tools/push-all.sh 1.1.0 # 同时打一个版本 tag 并推送
|
||
```
|
||
|
||
脚本做的事:
|
||
|
||
1. `git push origin main` —— 用 `http.extraHeader` 传 Basic 认证,Token **只在环境变量里**,
|
||
不会写进 `.git/config`、URL 或 reflog;同时用 `-c credential.helper=` 屏蔽凭据助手
|
||
(否则在非交互 / 无桌面会话里 Git Credential Manager 会挂住等弹窗)。
|
||
2. `docker compose build` —— 镜像名本身就是注册表地址。
|
||
3. `docker login` + `docker push`(`--password-stdin`,Token 不进命令行历史)。
|
||
|
||
> 不用脚本、手工推也行:`git push origin main` 弹出凭据窗口时,
|
||
> 用户名填 `wangchuanli`,**密码处填 Access Token**(不是网页登录密码)。
|
||
|
||
### 5.1 传输协议:默认走 HTTPS(不用配 insecure-registries)
|
||
|
||
`git.iwali.top` 有 **Let's Encrypt 通配证书(`*.iwali.top`)**,HTTPS 全程可用:
|
||
|
||
```bash
|
||
curl -s -o /dev/null -w "%{http_code}\n" https://git.iwali.top/api/v1/version # 200
|
||
curl -s -o /dev/null -w "%{http_code}\n" https://git.iwali.top/v2/ # 401(需认证,正常)
|
||
```
|
||
|
||
所以:
|
||
|
||
- **git 远端用 `https://`**(仓库地址见上);
|
||
- **镜像名就是 `git.iwali.top/...`,Docker 默认按 HTTPS 访问** —— 无需任何额外配置。
|
||
|
||
> 只有在 Gitea 前面**没有** TLS 终止(纯 `http://`)时,才需要在 Docker Desktop
|
||
> **Settings → Docker Engine** 的 `daemon.json` 里加:
|
||
> ```json
|
||
> { "insecure-registries": ["git.iwali.top"] }
|
||
> ```
|
||
> 然后 `Apply & Restart`(Linux 改 `/etc/docker/daemon.json` 后重启 docker)。
|
||
> 明文传输会让 Token 暴露在网络里,**能走 HTTPS 就不要开这个口子**。
|
||
|
||
### 5.2 登录
|
||
|
||
```bash
|
||
docker login git.iwali.top -u wangchuanli
|
||
# 密码用 Personal Access Token(Gitea「设置 → 应用 → 生成令牌」,
|
||
# 勾选 write:package;不要用网页登录密码)
|
||
```
|
||
|
||
### 5.3 构建并推送
|
||
|
||
```bash
|
||
# 镜像名默认已经是注册表地址(见 docker-compose.yml 的 image: 字段)
|
||
docker compose build
|
||
|
||
# 打上语义化版本标签
|
||
docker tag git.iwali.top/wangchuanli/workbuddy-portal:latest \
|
||
git.iwali.top/wangchuanli/workbuddy-portal:1.1.0
|
||
|
||
docker push git.iwali.top/wangchuanli/workbuddy-portal:latest
|
||
docker push git.iwali.top/wangchuanli/workbuddy-portal:1.1.0
|
||
```
|
||
|
||
### 5.4 在另一台机器上拉取运行
|
||
|
||
```bash
|
||
docker pull git.iwali.top/wangchuanli/workbuddy-portal:1.1.0
|
||
|
||
# 不 clone 仓库也能跑:只写一个 compose 文件
|
||
cat > docker-compose.yml <<'YAML'
|
||
services:
|
||
portal:
|
||
image: git.iwali.top/wangchuanli/workbuddy-portal:1.1.0
|
||
restart: unless-stopped
|
||
ports: ["8848:8848"]
|
||
environment:
|
||
TZ: Asia/Shanghai
|
||
WB_ADMIN_PASSWORD: "改成你的强密码"
|
||
volumes:
|
||
- wb_data:/app/data
|
||
- wb_logs:/app/logs
|
||
|
||
volumes:
|
||
wb_data:
|
||
wb_logs:
|
||
YAML
|
||
|
||
docker compose up -d
|
||
```
|
||
|
||
### 5.5 验证远端
|
||
|
||
```bash
|
||
docker manifest inspect git.iwali.top/wangchuanli/workbuddy-portal:1.1.0
|
||
# 或
|
||
curl -s -u wangchuanli:TOKEN \
|
||
https://git.iwali.top/api/v1/packages/wangchuanli?type=container
|
||
```
|
||
|
||
---
|
||
|
||
## 六、备份与恢复
|
||
|
||
### 6.1 备份什么
|
||
|
||
数据都在命名卷 `workbuddy-portal_wb_data` 里(对应容器内 `/app/data`):
|
||
|
||
| 文件 | 重要性 | 说明 |
|
||
|---|---|---|
|
||
| `usage.sqlite` | ★★★ | **数据正本**,丢了要重新采集,且官网窗口外的数据永久丢失 |
|
||
| `usage.sqlite-wal` / `-shm` | ★★★ | WAL 模式下未 checkpoint 的数据在这里,**要一起拷** |
|
||
| `instance.json` | ★★ | 含 `secret_key`,丢了所有人都要重新登录(数据不受影响) |
|
||
| `exports/*.csv` | ★ | 导出快照,可再生 |
|
||
| `workbuddy-portal_wb_logs` | ☆ | 排错用,可再生 |
|
||
|
||
`.env` 不在里面——它含密码,**单独用密码管理器保管**。
|
||
|
||
> **不要**在容器运行时用宿主机的 `manage.py` 去碰库(见 2.4 与第九节)。
|
||
|
||
### 6.2 备份
|
||
|
||
**方式 A:整体打包命名卷(推荐,最完整)**
|
||
|
||
```bash
|
||
docker compose stop portal
|
||
|
||
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 .
|
||
|
||
docker compose start portal
|
||
```
|
||
|
||
**方式 B:只导数据库文件(最常用)**
|
||
|
||
```bash
|
||
# 先 checkpoint,把 WAL 落进主库,再拷——这样单独一个 .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
|
||
```
|
||
|
||
**方式 C:逻辑导出(跨版本最安全,可读性最好)**
|
||
|
||
```bash
|
||
docker compose exec portal python manage.py export-csv /app/data/exports
|
||
docker compose cp portal:/app/data/exports/. ./backup/exports/
|
||
```
|
||
|
||
建议方式 B 每天跑、方式 C 每季度跑一次;方式 A 在升级前跑。
|
||
|
||
### 6.3 恢复
|
||
|
||
```bash
|
||
docker compose down
|
||
|
||
# 把备份灌回命名卷(先清空,避免新旧 WAL 混用损坏数据)
|
||
docker volume rm workbuddy-portal_wb_data
|
||
docker volume create workbuddy-portal_wb_data
|
||
docker run --rm \
|
||
-v workbuddy-portal_wb_data:/to \
|
||
-v "$PWD/backup":/from:ro \
|
||
alpine:3.20 sh -c 'cp -a /from/. /to/'
|
||
|
||
docker compose up -d
|
||
docker compose exec portal python manage.py stats # 核对条数
|
||
```
|
||
|
||
> **别把旧库和旧 WAL 混着用**:WAL 里记的是相对旧库的增量,配错会损坏数据。
|
||
> 用方式 B 的备份(已 checkpoint)最省心,恢复时目标目录里只放一个 `.sqlite` 即可。
|
||
|
||
### 6.4 迁移:从绑定挂载换到命名卷
|
||
|
||
如果之前用的是 `docker-compose.hostdir.yml`(或旧版把 `./data` 直接挂进去),数据在
|
||
宿主机目录里,搬进命名卷:
|
||
|
||
```bash
|
||
docker compose down
|
||
docker volume create workbuddy-portal_wb_data
|
||
docker run --rm \
|
||
-v workbuddy-portal_wb_data:/to \
|
||
-v "$PWD/data":/from:ro \
|
||
alpine:3.20 sh -c 'cp -a /from/. /to/'
|
||
docker compose up -d # 不带 -f hostdir 叠加层
|
||
docker compose exec portal python manage.py stats
|
||
```
|
||
|
||
> 迁移完成后,宿主机上的 `./data` 就不再被容器使用了,可以留作迁移前的冷备。
|
||
|
||
---
|
||
|
||
## 七、升级与回滚
|
||
|
||
### Docker
|
||
|
||
```bash
|
||
git pull
|
||
docker compose build
|
||
docker compose up -d # 重建容器,数据在挂载卷里不受影响
|
||
docker compose exec portal python manage.py stats
|
||
```
|
||
|
||
回滚:把 `.env` 里的 `WB_IMAGE` 指回旧版本标签,然后
|
||
|
||
```bash
|
||
docker compose up -d --no-build
|
||
```
|
||
|
||
### 裸机
|
||
|
||
```bash
|
||
git pull
|
||
.venv/bin/pip install -r requirements.txt
|
||
sudo systemctl restart workbuddy-portal
|
||
```
|
||
|
||
### 升级前
|
||
|
||
1. **先备份**(见第六节)——`schema.sql` 用的是 `CREATE TABLE IF NOT EXISTS`,
|
||
加表加索引是安全的,但改列需要手工迁移,所以备份是唯一保险。
|
||
2. 看一眼 [CHANGELOG](CHANGELOG.md) 有没有破坏性变更。
|
||
|
||
---
|
||
|
||
## 八、日常巡检
|
||
|
||
| 频率 | 做什么 |
|
||
|---|---|
|
||
| 每天 | 打开「概览」看「采集健康」;确认今天有采集记录 |
|
||
| 每周 | 「日志管理」按 `warn` / `error` 筛一遍,看有没有 TLS 或解密类告警 |
|
||
| 每月 | 确认 Cookie 没过期(「配置管理」看提示);跑一次备份恢复演练 |
|
||
| 每季度 | `manage.py vacuum`;出一份全量 CSV 归档;检查磁盘占用 |
|
||
|
||
一键体检:
|
||
|
||
```bash
|
||
docker compose exec portal python manage.py status
|
||
docker compose exec portal python manage.py stats
|
||
```
|
||
|
||
---
|
||
|
||
## 九、排错
|
||
|
||
### 容器起来了但页面打不开
|
||
|
||
```bash
|
||
docker compose ps # 看 STATUS 是否 (healthy)
|
||
docker compose logs --tail=100
|
||
```
|
||
|
||
| 症状 | 排查 |
|
||
|---|---|
|
||
| `STATUS` 是 `Restarting` | 看日志里的 Python traceback;多半是 `data/` 权限或端口冲突 |
|
||
| `unhealthy` 但 `Up` | 健康检查打 `/login` 失败;`docker compose exec portal python /app/docker/healthcheck.py` 看具体报错 |
|
||
| 端口占用 | 改 `.env` 的 `WB_PORT`,如 `18848:8848` |
|
||
| 宿主机能访问、局域网不能 | `WB_BIND` 是不是被改成 `127.0.0.1` 了;防火墙有没有放行 |
|
||
|
||
### `exec format error` / `no such file or directory`(entrypoint)
|
||
|
||
`docker/entrypoint.sh` 被 CRLF 污染了。仓库里有 `.gitattributes` 强制 `*.sh` 为 LF;
|
||
若手工传过文件,执行:
|
||
|
||
```bash
|
||
python -c "p='docker/entrypoint.sh';d=open(p,'rb').read();open(p,'wb').write(d.replace(b'\r\n',b'\n'))"
|
||
```
|
||
|
||
### 数据库相关
|
||
|
||
| 报错 | 原因 / 处理 |
|
||
|---|---|
|
||
| `unable to open database file`(**用了 `hostdir` 叠加层**) | 宿主目录属主与容器内 uid 1000 不一致:`sudo chown -R 1000:1000 ./data` |
|
||
| `unable to open database file`(**Windows + Docker Desktop**) | 见下面 [「Windows 绑定挂载的坑」](#windows-绑定挂载的坑容器打不开数据库);根治办法是用默认的命名卷 |
|
||
| `database is locked` | 有另一个写进程;等它跑完(文件锁会串行化,但 SQLite 层仍会短暂报错) |
|
||
| `disk I/O error` | 挂载文件系统不支持 SQLite 的锁语义。改用本地盘或 Docker 命名卷 |
|
||
|
||
### Windows 绑定挂载的坑:容器打不开数据库
|
||
|
||
**症状**:容器起来时一切正常,跑着跑着 `/` `/records` 全部 `500`,应用日志里是
|
||
|
||
```
|
||
File "/app/workbuddy_portal/db.py", line 33, in connect
|
||
conn.execute("PRAGMA journal_mode=WAL")
|
||
sqlite3.OperationalError: unable to open database file
|
||
```
|
||
|
||
**根因**:Windows + Docker Desktop 的绑定挂载走 **9p**(`mount` 里能看到
|
||
`type 9p (…aname=drvfs;path=C:\…)`)。9p 本身支持 WAL(在上面新建库没问题),
|
||
但**当宿主的 Windows 进程打开过这个 WAL 库之后**,容器侧缓存的 `-shm` 映射就失效了,
|
||
下一次连接无法重建共享内存文件,于是打不开数据库。
|
||
|
||
**最小复现**(2026-09-14 实测):
|
||
|
||
```bash
|
||
docker compose -f docker-compose.yml -f docker-compose.hostdir.yml up -d
|
||
docker compose exec portal python -c \
|
||
"import sqlite3;print(sqlite3.connect('/app/data/usage.sqlite').execute('select count(*) from usage_records').fetchone())"
|
||
# -> (1665,) 容器侧正常
|
||
|
||
python manage.py stats # 宿主侧随手跑一次「纯读」的 CLI
|
||
# -> 存档:1665 条 …
|
||
|
||
docker compose exec portal python -c \
|
||
"import sqlite3;sqlite3.connect('/app/data/usage.sqlite')"
|
||
# -> sqlite3.OperationalError: unable to open database file
|
||
```
|
||
|
||
关键点:**纯读也会触发**,而且**宿主进程退出后容器不会自愈**——
|
||
只有 `docker compose restart portal` 才恢复。
|
||
|
||
**处理**:
|
||
|
||
1. **根治(推荐)**:不要用 `hostdir` 叠加层,直接用默认的**命名卷**
|
||
(`docker-compose.yml`)。容器独占 `/app/data`,宿主侧一律通过
|
||
`docker compose exec portal python manage.py …` 操作。
|
||
2. **临时**:`docker compose restart portal` 立刻恢复,但下一次宿主访问会再坏一次。
|
||
3. **想在宿主侧看数据**:用第六节的方式导出到宿主目录再看,不要直接连库。
|
||
|
||
> Linux 宿主机上不存在这个问题(bind mount 与容器是同一个文件系统),
|
||
> 所以 `docker-compose.hostdir.yml` 对 Linux 是安全的。
|
||
|
||
### 采集相关
|
||
|
||
| 报错 | 处理 |
|
||
|---|---|
|
||
| `cookie_expired` / `401` | 重新获取 Cookie 填进「配置管理」,然后按区间补采 |
|
||
| `TLS` / `SSLError` | 企业代理 / 自签证书场景,临时把 `ssl_verify` 设为 `0`;否则保持开启 |
|
||
| 返回 `409 busy` | 正常——已有采集在跑,等它结束 |
|
||
| 新增一直是 0 | 看「抓取」条数:>0 说明都是已存在的(正常);=0 说明云端该时段确实没数据 |
|
||
|
||
### 时区不对导致日期错位
|
||
|
||
```bash
|
||
docker compose exec portal date # 应该输出 CST / +0800
|
||
```
|
||
|
||
若不是,检查 `.env` 的 `TZ=Asia/Shanghai`,改完 `docker compose up -d` 重建容器
|
||
(`TZ` 是环境变量,`restart` 不生效)。
|
||
|
||
### 想临时关掉自动采集
|
||
|
||
「任务管理 → 取消勾选『启用调度』→ 保存」。或者
|
||
|
||
```bash
|
||
echo "WB_DISABLE_SCHEDULER=1" >> .env && docker compose up -d
|
||
```
|
||
|
||
---
|
||
|
||
## 十、配置项速查
|
||
|
||
调度与采集参数都在数据库里,**改完立即生效、不用重启**(页面「任务管理 / 配置管理」可改,
|
||
也可以直接改表):
|
||
|
||
| 键 | 默认 | 说明 |
|
||
|---|---|---|
|
||
| `schedule_enabled` | `1` | 调度总开关 |
|
||
| `schedule_times` | `09:00,17:00` | 每日时刻,逗号分隔,本地时区 |
|
||
| `catch_up` | `1` | 启动补跑开关 |
|
||
| `catch_up_grace_hours` | `12` | 补跑宽限期(小时) |
|
||
| `page_size` | `200` | 采集单页条数(20~1000) |
|
||
| `rewind_minutes` | `2` | 断点回退分钟数(0~120) |
|
||
| `drift_tolerance_minutes` | `5` | 云端时间漂移告警阈值(0~720) |
|
||
| `max_prompt` | `2048` | Prompt 入库截断长度,0 = 不截断 |
|
||
| `verify_days` | `0` | 采集后整日校验天数(0~90) |
|
||
| `timeout` | `30` | HTTP 超时秒数(5~300) |
|
||
| `ssl_verify` | `1` | 校验云端 HTTPS 证书 |
|
||
| `api_base` / `api_path` | 官方地址 | 接口地址(走镜像/代理时改) |
|
||
| `cookie` | 空 | 账号凭证(页面只回掩码) |
|
||
| `user_agent` | Chrome UA | 与 Cookie 同源更稳 |
|
||
|
||
**写错的值会在保存时被拒绝**并给出原因,不会污染配置。
|
||
|
||
环境变量(启动期,改了要重建容器):
|
||
|
||
| 变量 | 说明 |
|
||
|---|---|
|
||
| `TZ` | 时区,影响所有日期口径 |
|
||
| `WB_HOST` / `WB_PORT` | 容器内监听地址 / 端口 |
|
||
| `WB_DATA_DIR` / `WB_LOG_DIR` / `WB_DB` | 数据 / 日志 / 库文件路径覆盖 |
|
||
| `WB_DISABLE_SCHEDULER` | `1` = 不启动调度线程 |
|
||
| `WB_ADMIN_USER` / `WB_ADMIN_PASSWORD` | 首个管理员(仅库为空时生效) |
|
||
| `WB_IMPORT_CREDS` / `WB_IMPORT_XLSX` | 启动时自动导入 |
|