feat: 新增备份恢复与公网加固
- 新增备份管理页与 API:在线快照、自动周期备份、按份数清理、下载、一键恢复(恢复前自动兜底) - 新增 /profile/export,普通用户可导出本人全部数据(不含 Cookie 明文) - 修复 X-Forwarded-For 可伪造导致三道 IP 防线失效,统一走 client_ip() 取客户端地址 - 取消 admin123 硬编码默认口令,留空则生成随机初始口令并仅打印一次 - .dockerignore 排除 backups/ 并加构建期断言,防止密钥随镜像分发 - 新增会话版本号,改密/停用/删除及恢复备份后其他会话立即失效 - 新增容器资源上限、采集跨度硬顶 31 天、重操作最小间隔与并发 409 - 新增访问日志、HSTS 条件下发、口令黑名单、验证码抗模板匹配、instance.json 0600 - 版本号升至 1.4.0,同步更新 README、SECURITY、.env.example 与 compose 配置
这个提交包含在:
+360
-79
@@ -90,17 +90,26 @@ workbuddy-portal/
|
||||
**① 首个管理员账号。** 数据库为空时才会创建,之后改密码走「用户管理」或 `manage.py passwd`。
|
||||
|
||||
```bash
|
||||
# 默认是 admin / admin123 —— 局域网部署下必须改掉!
|
||||
python manage.py init --user admin --password '你的强密码'
|
||||
```
|
||||
|
||||
> **不设密码会怎样**(v1.4.0 起):程序用 `secrets.token_urlsafe(12)` 生成一个**随机**口令,
|
||||
> 用醒目的方框打印在输出里,**只在这一次打印**。库里只有散列,日志一滚就再也拿不回来。
|
||||
> **没有 `admin123` 这类默认口令了** —— 硬编码一个默认口令等于把公网实例的钥匙挂在门上。
|
||||
> 所以要么现在就显式设 `WB_ADMIN_PASSWORD`,要么把打印出来的那行抄走。
|
||||
> 容器里的对应做法见 [11.10 拿不到管理员初始口令](#1110-拿不到管理员初始口令)。
|
||||
|
||||
**② 访问方式。** 决定 `WB_BIND` 与 `WB_COOKIE_SECURE`:
|
||||
|
||||
| 场景 | `WB_BIND` | `WB_COOKIE_SECURE` |
|
||||
|---|---|---|
|
||||
| 只本机用 | `127.0.0.1` | `0` |
|
||||
| 局域网 `http://IP:8848` | `0.0.0.0` | **`0`**(设成 1 会导致「登录成功又跳回登录页」) |
|
||||
| 域名 + HTTPS 反代 | `127.0.0.1` | `1` |
|
||||
| 场景 | `WB_BIND` | `WB_COOKIE_SECURE` | `WB_TRUST_PROXY` |
|
||||
|---|---|---|---|
|
||||
| 只本机用 | `127.0.0.1` | `0` | `0` |
|
||||
| 局域网 `http://IP:8848` | `0.0.0.0` | **`0`**(设成 1 会导致「登录成功又跳回登录页」) | **`0`** |
|
||||
| 域名 + HTTPS 反代 | `127.0.0.1` | `1` | **`1`**(前提:反代会写 XFF,见第六节) |
|
||||
|
||||
> `WB_TRUST_PROXY` 是 v1.4.0 新增的,**默认 0**。直接暴露给公网时留 0;
|
||||
> 设成 1 而反代又没写该头,攻击者每次换一个伪造的 `X-Forwarded-For` 就能让
|
||||
> 验证码限速 / 注册配额 / 登录锁定**三道防线同时失效**(实测 45 次请求全部放行)。
|
||||
|
||||
### 2.4 从 v1.0 迁移历史数据(可选)
|
||||
|
||||
@@ -157,16 +166,28 @@ WorkingDirectory=/opt/workbuddy-portal
|
||||
# TZ 直接决定调度时刻与所有日期口径,务必设对
|
||||
Environment=TZ=Asia/Shanghai
|
||||
Environment=WB_COOKIE_SECURE=0
|
||||
# 备份落点默认就是 <仓库>/backups,显式写出来便于以后搬迁
|
||||
Environment=WB_BACKUP_DIR=/opt/workbuddy-portal/backups
|
||||
# 直连(前面没有反代)时保持 0 —— 设成 1 会让三道 IP 防线可被伪造 XFF 绕过
|
||||
Environment=WB_TRUST_PROXY=0
|
||||
ExecStart=/opt/workbuddy-portal/.venv/bin/python manage.py serve --host 0.0.0.0 --port 8848
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
# ---- 资源上限(与容器部署的 cpus / mem_limit 对应)----
|
||||
# 单写者架构下不靠加副本扛负载,所以「限制单实例吃多少」才是正解。
|
||||
# 数值按需调整:内存超限被 OOM 杀掉有明确日志,比「越来越慢」好查得多。
|
||||
CPUQuota=100%
|
||||
MemoryMax=512M
|
||||
MemorySwapMax=0
|
||||
TasksMax=64
|
||||
|
||||
# ---- 加固(可选但建议)----
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
ProtectSystem=strict
|
||||
ProtectHome=true
|
||||
ReadWritePaths=/opt/workbuddy-portal/data /opt/workbuddy-portal/logs
|
||||
ReadWritePaths=/opt/workbuddy-portal/data /opt/workbuddy-portal/logs /opt/workbuddy-portal/backups
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -179,11 +200,15 @@ sudo systemctl status workbuddy-portal
|
||||
journalctl -u workbuddy-portal -f
|
||||
```
|
||||
|
||||
> `User=workbuddy` 需要事先建号,并让 `data/`、`logs/` 归它所有:
|
||||
> `User=workbuddy` 需要事先建号,并让 `data/`、`logs/`、`backups/` 归它所有:
|
||||
> ```bash
|
||||
> sudo useradd --system --home /opt/workbuddy-portal --shell /usr/sbin/nologin workbuddy
|
||||
> sudo chown -R workbuddy:workbuddy /opt/workbuddy-portal/data /opt/workbuddy-portal/logs
|
||||
> sudo chown -R workbuddy:workbuddy /opt/workbuddy-portal/data \
|
||||
> /opt/workbuddy-portal/logs \
|
||||
> /opt/workbuddy-portal/backups
|
||||
> ```
|
||||
> `backups/` 千万别漏 —— 漏了的表现是「备份管理」页能打开,但一备份就报错,
|
||||
> 日志里是 `PermissionError`(服务以 `workbuddy` 身份跑,写不进 root 拥有的目录)。
|
||||
|
||||
### 3.2 Windows
|
||||
|
||||
@@ -266,20 +291,54 @@ docker compose logs -f
|
||||
|
||||
### 4.4 `.env` 主要变量
|
||||
|
||||
#### 监听与调度
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `WB_BIND` | `0.0.0.0` | 宿主机绑定地址。只想本机访问就设 `127.0.0.1` |
|
||||
| `WB_PORT` | `8848` | 宿主机端口 |
|
||||
| `TZ` | `Asia/Shanghai` | **影响调度时刻与所有日期口径** |
|
||||
| `WB_DISABLE_SCHEDULER` | `0` | `1` = 不启动调度线程(只跑手动采集)。多副本时除第一个外都要设 1 |
|
||||
| `WB_ADMIN_USER` | `admin` | 首个管理员用户名(只在库为空时生效) |
|
||||
| `WB_ADMIN_PASSWORD` | 空 | 首个管理员密码。**留空会用 `admin123`**,务必显式设置 |
|
||||
| `WB_COOKIE_SECURE` | `0` | `1` = 会话 Cookie 只走 HTTPS。**纯 HTTP 部署设成 `1` 会导致「登录成功又跳回登录页」** |
|
||||
| `WB_DISABLE_SCHEDULER` | `0` | `1` = 不启动调度线程(只跑手动采集) |
|
||||
| `WB_IMPORT_CREDS` | `0` | `1` = 启动时尝试从挂载的编辑器配置导入 Cookie |
|
||||
| `WB_IMPORT_XLSX` | 空 | 启动时一次性导入这个路径的官网 xlsx |
|
||||
| `WB_ADMIN_PASSWORD` | 空 | 首个管理员密码。**留空则随机生成并只在启动日志打印一次**(v1.4.0 起不再有 `admin123` 兜底) |
|
||||
| `WB_THREADS` | `4`(compose)/ `8`(代码) | waitress 线程数 = 单实例能同时吃进几个慢请求(采集 / 导出 / 备份恢复) |
|
||||
| `WB_IMAGE` | `git.iwali.top/…:latest` | 镜像名(见 [第七节](#七把代码与镜像推到-gitea-注册表)) |
|
||||
|
||||
> `TZ`、`WB_COOKIE_SECURE`、`WB_DISABLE_SCHEDULER` 都是**进程环境变量**,
|
||||
#### 容器资源上限(v1.4.0 起)
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `WB_CPUS` | `1.0` | CPU 上限(允许小数)。SQLite 是单写者,1 核够用,压测后再调 |
|
||||
| `WB_MEM_LIMIT` | `512m` | 内存上限。**同时会设 `memswap_limit` 为同值 = 禁用 swap** |
|
||||
| `WB_PIDS_LIMIT` | `256` | 进程/线程数上限,挡 fork 炸弹 |
|
||||
|
||||
> **为什么 swap 要禁用**:不禁用的话内存超限会悄悄滑进 swap,表现为「越来越慢」
|
||||
> 而不是「被 OOM 杀掉」——后者有明确的日志和退出码,前者只会让人怀疑磁盘。
|
||||
> `nofile` 上限(4096:8192)在 compose 里写死,因为 SQLite 除主库外还持有 `-wal` `-shm`,
|
||||
> 恢复期间还要开临时库 + `ATTACH` 源库,默认 1024 在高并发下偏紧。
|
||||
|
||||
#### 反向代理与传输安全(**三个必须一起决定**)
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `WB_TRUST_PROXY` | `0` | 是否信任 `X-Forwarded-For`。**默认不信任**。设 1 的前提是「你自己的反代会写这个头」(见第六节,两种 nginx 写法都安全) |
|
||||
| `WB_FORCE_HTTPS` | `0` | `1` = 非 HTTPS 的 GET/HEAD 跳转到 https(POST 不跳,否则会掉请求体) |
|
||||
| `WB_COOKIE_SECURE` | `0` | `1` = 会话 Cookie 只走 HTTPS。**纯 HTTP 部署设成 `1` 会导致「登录成功又跳回登录页」** |
|
||||
| `WB_ACCESS_LOG` | `1` | 是否记访问日志(写进 `logs/app.log`,跳过静态资源与验证码图) |
|
||||
|
||||
> **HSTS 的触发条件正是 `WB_COOKIE_SECURE` 或 `WB_FORCE_HTTPS` 为 1** ——
|
||||
> 所以纯 HTTP 部署不会下发 HSTS(下发会让浏览器强升 https,表现成白屏)。
|
||||
|
||||
#### 可选:启动时自动导入
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|---|---|---|
|
||||
| `WB_IMPORT_CREDS` | `0` | `1` = 启动时尝试从挂载的编辑器配置导入 Cookie |
|
||||
| `WB_IMPORT_XLSX` | 空 | 启动时一次性导入这个路径的官网 xlsx |
|
||||
| `WB_BACKUP_DIR` | `/app/backups` | 归档落点(相对**仓库目录**,不是 `data/`,理由见 [8.1](#81-备份什么)) |
|
||||
|
||||
> `TZ`、`WB_COOKIE_SECURE`、`WB_TRUST_PROXY`、`WB_FORCE_HTTPS`、`WB_ACCESS_LOG`、
|
||||
> `WB_THREADS`、`WB_CPUS` / `WB_MEM_LIMIT` / `WB_PIDS_LIMIT` 都是**进程环境变量**,
|
||||
> `docker compose restart` **不生效**,必须 `docker compose up -d` 重建容器。
|
||||
|
||||
### 4.5 数据落点:命名卷,不是绑定挂载
|
||||
@@ -288,6 +347,13 @@ docker compose logs -f
|
||||
|---|---|---|
|
||||
| `/app/data` | 命名卷 `workbuddy-portal_wb_data` | `usage.sqlite`(正本)、`instance.json`(密钥)、`exports/` |
|
||||
| `/app/logs` | 命名卷 `workbuddy-portal_wb_logs` | `app.log`(滚动 2 MB × 3) |
|
||||
| `/app/backups` | 命名卷 `workbuddy-portal_wb_backups` | 备份归档 `usage-<时间戳>.zip`(**含 `instance.json`,机密等级等同密钥**) |
|
||||
|
||||
> **备份为什么必须单独一个卷**:如果和 `wb_data` 同卷,一次 `docker compose down -v`
|
||||
> 或卷损坏会把正本与备份**一起**带走 —— 那正好是最需要备份的时刻。
|
||||
> 而且容器里的 `/app/backups` 若不挂卷,写进去的文件只存在于容器**可写层**:
|
||||
> `docker restart` 还能留住,但 `docker compose up -d --build` 重建容器就没了 ——
|
||||
> 「构建完发现备份全丢」是个会真的踩到的坑。现在挂的是独立命名卷。
|
||||
|
||||
**为什么是命名卷而不是脚本目录里的 `./data`**(这不是随手选的):
|
||||
|
||||
@@ -308,33 +374,41 @@ 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 指定目标目录
|
||||
# 可用 WB_HOST_DATA_DIR / WB_HOST_LOG_DIR / WB_HOST_BACKUP_DIR 指定目标目录
|
||||
docker volume rm workbuddy-portal_wb_backups # 叠加后这个卷没人用,可删
|
||||
```
|
||||
|
||||
> ⚠️ **只建议 Linux 宿主机使用**(bind mount 与容器同一文件系统,WAL 语义正常)。
|
||||
> Windows + Docker Desktop 下必然踩上面那个坑。
|
||||
>
|
||||
> Linux 首次运行若报 `unable to open database file`,是宿主目录属主与容器内 uid 1000
|
||||
> 不一致:`sudo chown -R 1000:1000 ./data ./logs`
|
||||
> 不一致:`sudo chown -R 1000:1000 ./data ./logs ./backups`
|
||||
|
||||
### 4.6 常用命令
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker compose logs -f --tail=100
|
||||
docker compose logs portal | grep -A6 '管理员初始口令' # 首次初始化时唯一一次机会
|
||||
docker compose restart # 注意:环境变量改动 restart 不生效
|
||||
docker compose down # 停并删容器,数据保留在卷里
|
||||
docker compose down -v # ⚠️ 连卷一起删(数据 + 日志 + 备份全没)
|
||||
docker compose up -d --build # 改完代码重新构建
|
||||
|
||||
docker compose exec portal python manage.py collect # 手动采集一次
|
||||
docker compose exec portal python manage.py users # 账号 / 角色 / 凭证状态
|
||||
docker compose exec portal python manage.py status # 调度与最近采集
|
||||
docker compose exec portal python manage.py status # 调度与最近采集
|
||||
docker compose exec portal python manage.py backups # 现有备份清单
|
||||
|
||||
# 看容器实际吃到的资源上限(验证 compose 的 limits 生效了)
|
||||
docker stats --no-stream workbuddy-portal
|
||||
docker inspect -f 'CPU={{.HostConfig.NanoCpus}} MEM={{.HostConfig.Memory}} PIDS={{.HostConfig.PidsLimit}}' workbuddy-portal
|
||||
```
|
||||
|
||||
---
|
||||
@@ -363,10 +437,19 @@ name: workbuddy-portal
|
||||
|
||||
services:
|
||||
portal:
|
||||
image: git.iwali.top/wangchuanli/workbuddy-portal:1.3.0
|
||||
image: git.iwali.top/wangchuanli/workbuddy-portal:1.4.0
|
||||
container_name: workbuddy-portal
|
||||
restart: unless-stopped
|
||||
init: true # tini 接管 PID 1,docker stop 能干净传到 python
|
||||
|
||||
# ---- 容器资源上限(对外提供服务时的第一道闸门)----
|
||||
cpus: "${WB_CPUS:-1.0}" # SQLite 单写者,多给核也并行不起来
|
||||
mem_limit: "${WB_MEM_LIMIT:-512m}"
|
||||
memswap_limit: "${WB_MEM_LIMIT:-512m}" # 与 mem_limit 相等 = 禁用 swap
|
||||
pids_limit: ${WB_PIDS_LIMIT:-256}
|
||||
ulimits:
|
||||
nofile: {soft: 4096, hard: 8192}
|
||||
|
||||
ports:
|
||||
- "${WB_BIND:-0.0.0.0}:${WB_PORT:-8848}:8848"
|
||||
environment:
|
||||
@@ -378,10 +461,17 @@ services:
|
||||
WB_DISABLE_SCHEDULER: ${WB_DISABLE_SCHEDULER:-0}
|
||||
# 纯 HTTP 部署必须留 0,设成 1 会「登录成功又跳回登录页」
|
||||
WB_COOKIE_SECURE: ${WB_COOKIE_SECURE:-0}
|
||||
# 直接暴露公网必须留 0;只有自己的反代重写了 XFF 才置 1(见第六节)
|
||||
WB_TRUST_PROXY: ${WB_TRUST_PROXY:-0}
|
||||
WB_FORCE_HTTPS: ${WB_FORCE_HTTPS:-0}
|
||||
WB_ACCESS_LOG: ${WB_ACCESS_LOG:-1}
|
||||
WB_THREADS: ${WB_THREADS:-4}
|
||||
WB_BACKUP_DIR: /app/backups
|
||||
WB_IMPORT_CREDS: ${WB_IMPORT_CREDS:-0}
|
||||
volumes:
|
||||
- wb_data:/app/data # 数据正本 + instance.json + exports
|
||||
- wb_logs:/app/logs
|
||||
- wb_backups:/app/backups # 归档单独一个卷,别和正本同卷
|
||||
healthcheck:
|
||||
test: ["CMD", "python", "/app/docker/healthcheck.py"]
|
||||
interval: 30s
|
||||
@@ -397,6 +487,7 @@ services:
|
||||
volumes:
|
||||
wb_data:
|
||||
wb_logs:
|
||||
wb_backups:
|
||||
```
|
||||
|
||||
**`.env`**:
|
||||
@@ -408,13 +499,22 @@ WB_BIND=0.0.0.0
|
||||
WB_PORT=8848
|
||||
TZ=Asia/Shanghai
|
||||
|
||||
# 首个管理员(只在数据库为空时生效)—— 务必改掉默认值
|
||||
# 首个管理员(只在数据库为空时生效)。
|
||||
# 留空 = 程序生成随机口令并只在启动日志打印一次(没有 admin123 兜底了)。
|
||||
WB_ADMIN_USER=admin
|
||||
WB_ADMIN_PASSWORD=换成你的强密码
|
||||
|
||||
WB_DISABLE_SCHEDULER=0
|
||||
WB_COOKIE_SECURE=0
|
||||
WB_IMPORT_CREDS=0
|
||||
WB_TRUST_PROXY=0
|
||||
WB_FORCE_HTTPS=0
|
||||
WB_ACCESS_LOG=1
|
||||
WB_THREADS=4
|
||||
|
||||
# 容器资源上限
|
||||
WB_CPUS=1.0
|
||||
WB_MEM_LIMIT=512m
|
||||
WB_PIDS_LIMIT=256
|
||||
EOF
|
||||
chmod 600 .env # 里面有密码
|
||||
```
|
||||
@@ -434,12 +534,30 @@ docker compose logs -f --tail=50
|
||||
|
||||
首次启动时容器入口会依次做三件事(见 `docker/entrypoint.sh`):
|
||||
|
||||
1. `manage.py init` —— 建表 + 灌默认配置 + 创建首个管理员(**幂等**,库非空时不重建);
|
||||
1. `manage.py init` —— 建表 + 灌默认配置 + 创建首个管理员(**幂等**,库非空时不重建)。
|
||||
没设 `WB_ADMIN_PASSWORD` 时,这一步会打印一次随机口令,格式长这样:
|
||||
|
||||
```
|
||||
================= 管理员初始口令 =================
|
||||
用 户 名:admin
|
||||
初 始 口 令:xxxxxxxxxxxxxxxx
|
||||
==================================================
|
||||
```
|
||||
|
||||
**只有这一次机会**。之后想再看到,唯一的办法是删库重来或
|
||||
`docker compose exec portal python manage.py passwd admin 新密码`。
|
||||
2. 可选导入 —— `WB_IMPORT_CREDS=1` / `WB_IMPORT_XLSX=…` 才触发;
|
||||
3. `exec manage.py serve` —— 交接给 waitress,调度线程就在这个进程里。
|
||||
3. `exec manage.py serve` —— 交接给 waitress(线程数 `WB_THREADS`),调度线程就在这个进程里。
|
||||
|
||||
日志里看到 `[entrypoint] 启动 Web 服务(waitress)…` 就是起来了。
|
||||
|
||||
> 口令提示行是被容器启动日志带出来的,所以**别忘了加 `--tail`**
|
||||
> (默认 `docker compose logs` 只给最近若干行;容器重启多轮后提示会被挤出去):
|
||||
>
|
||||
> ```bash
|
||||
> docker compose logs portal | grep -A6 '管理员初始口令'
|
||||
> ```
|
||||
|
||||
### 5.4 完成后的检查清单
|
||||
|
||||
```bash
|
||||
@@ -473,7 +591,10 @@ docker compose exec portal python manage.py status
|
||||
|
||||
## 六、反向代理与 HTTPS
|
||||
|
||||
前面挂 nginx / Caddy / Traefik 时,有两点必须注意,否则会踩坑:
|
||||
前面挂 nginx / Caddy / Traefik 时有三个要点,**第 3 条是 v1.4.0 新增的**,
|
||||
漏了会以「登录限速误伤所有人」的形式出现:
|
||||
|
||||
**① nginx 侧透传真实 IP**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
@@ -487,8 +608,7 @@ server {
|
||||
proxy_pass http://127.0.0.1:8848;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
# 必须透传:登录失败限速按真实 IP 计数,否则所有请求都算到代理头上,
|
||||
# 一个人被锁 → 全站被锁
|
||||
# 透传真实 IP:登录失败限速、注册配额、验证码限速都按它计数
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
@@ -499,14 +619,47 @@ server {
|
||||
}
|
||||
```
|
||||
|
||||
| 现象 | 原因 |
|
||||
**② 应用侧打开 `WB_TRUST_PROXY=1`(关键,别漏)**
|
||||
|
||||
程序**默认不信任** `X-Forwarded-For` —— 这是为了「直接暴露公网」时的安全:
|
||||
XFF 的第一段是客户端自己填的,信了它,攻击者每次换一个伪造值就能绕过全部 IP 限速。
|
||||
|
||||
开启后程序取 XFF 里**最右侧**的合法 IP。最右侧是离你最近的那一跳(由你自己的代理写入),
|
||||
客户端加不进去,所以下面两种 nginx 写法**都安全**:
|
||||
|
||||
| nginx 写法 | 效果 |
|
||||
|---|---|
|
||||
| 登录限速「误伤」所有人 | 没透传 `X-Forwarded-For` |
|
||||
| 导出 CSV 要等很久才出第一个字节 | 没关 `proxy_buffering` |
|
||||
| 手动采集走到 504 | `proxy_read_timeout` 太短 |
|
||||
| `$proxy_add_x_forwarded_for` | 在客户端传来的值后面**追加**你的 `$remote_addr` → 链路完整,便于排查 |
|
||||
| `$remote_addr` | **覆盖**成最近一跳 → 最保守,丢掉链路 |
|
||||
|
||||
真正不能做的是去信 XFF 里**最左边**那一段。
|
||||
|
||||
```bash
|
||||
# .env
|
||||
WB_TRUST_PROXY=1
|
||||
```
|
||||
|
||||
> ⚠️ **漏了这一步的症状**:所有请求都以 `127.0.0.1`(代理的地址)计入限速,
|
||||
> 于是**一个人失败 5 次,全站被锁 10 分钟**。这与「没透传 XFF」的表现一模一样,
|
||||
> 所以两件事要一起改。
|
||||
>
|
||||
> 反过来,**不设反代却设了 `WB_TRUST_PROXY=1`** 是更危险的方向:
|
||||
> 三道 IP 防线直接失效。拿不准就留 `0`。
|
||||
|
||||
**③ HTTPS 相关开关**
|
||||
|
||||
配好 HTTPS 后,把 `.env` 里的 `WB_COOKIE_SECURE` 改成 `1`(会话 Cookie 只走 HTTPS),
|
||||
然后 `docker compose up -d` 重建容器。**只有真的能用 `https://` 访问时才这么做。**
|
||||
需要强制跳转再加 `WB_FORCE_HTTPS=1`。这两个任一为 `1` 时程序才下发 HSTS
|
||||
—— 纯 HTTP 部署**不会**下发(下发会让浏览器强升 https,表现成白屏)。
|
||||
|
||||
改完都要 `docker compose up -d` 重建容器(环境变量在启动期读取)。
|
||||
|
||||
| 现象 | 原因 |
|
||||
|---|---|
|
||||
| 登录限速「误伤」所有人 | 没透传 `X-Forwarded-For`,**或**忘了设 `WB_TRUST_PROXY=1` |
|
||||
| 导出 CSV 要等很久才出第一个字节 | 没关 `proxy_buffering` |
|
||||
| 手动采集走到 504 | `proxy_read_timeout` 太短 |
|
||||
| 访问 http 时白屏 / 一直跳转 | `WB_FORCE_HTTPS=1` 但实际没有 HTTPS 可用 |
|
||||
|
||||
---
|
||||
|
||||
@@ -522,7 +675,7 @@ server {
|
||||
```bash
|
||||
export GITEA_TOKEN=<你的令牌>
|
||||
tools/push-all.sh # 推 main 分支 + 镜像 latest
|
||||
tools/push-all.sh 1.3.0 # 同时打一个版本 tag 并推送
|
||||
tools/push-all.sh 1.4.0 # 同时打一个版本 tag 并推送
|
||||
```
|
||||
|
||||
脚本做的事:
|
||||
@@ -560,15 +713,15 @@ docker login git.iwali.top -u wangchuanli
|
||||
|
||||
docker compose build
|
||||
docker tag git.iwali.top/wangchuanli/workbuddy-portal:latest \
|
||||
git.iwali.top/wangchuanli/workbuddy-portal:1.3.0
|
||||
git.iwali.top/wangchuanli/workbuddy-portal:1.4.0
|
||||
docker push git.iwali.top/wangchuanli/workbuddy-portal:latest
|
||||
docker push git.iwali.top/wangchuanli/workbuddy-portal:1.3.0
|
||||
docker push git.iwali.top/wangchuanli/workbuddy-portal:1.4.0
|
||||
```
|
||||
|
||||
### 7.4 验证远端
|
||||
|
||||
```bash
|
||||
docker manifest inspect git.iwali.top/wangchuanli/workbuddy-portal:1.3.0
|
||||
docker manifest inspect git.iwali.top/wangchuanli/workbuddy-portal:1.4.0
|
||||
# 或
|
||||
curl -s -u wangchuanli:TOKEN \
|
||||
https://git.iwali.top/api/v1/packages/wangchuanli?type=container
|
||||
@@ -589,17 +742,47 @@ curl -s -u wangchuanli:TOKEN \
|
||||
| `instance.json` | ★★★ | 含 `secret_key`(会话签名)**与 `cookie_key`(各账号 Cookie 的加密主密钥)**。丢了 / 被替换:所有人要重新登录,**且所有账号存的 Cookie 都会变成「无法解密」,需各自重填** |
|
||||
| `exports/*.csv` | ★ | 导出快照,可再生 |
|
||||
| `wb_logs` 卷 | ☆ | 排错用,可再生 |
|
||||
| `wb_backups` 卷(`/app/backups`) | ★★★ | 备份归档 `usage-<时间戳>.zip`。**里面含一份 `instance.json`** —— 所以它的机密等级和密钥文件完全相同 |
|
||||
|
||||
`.env` 不在里面 —— 它含密码,**单独用密码管理器保管**。
|
||||
|
||||
工程目录下的 `backups/` 就是给这类快照准备的位置(**刻意不放在 `data/`**:
|
||||
`data/` 是 Docker 卷,`docker compose down -v` 会把备份和正本一起删掉)。
|
||||
> **`instance.json` 为什么必须跟着备份走**:`cookie_key` 在里面,而各账号的 Cookie
|
||||
> 是用它加密后入库的。只备库不备它,恢复出来的库能读,但**所有账号的 Cookie 都会
|
||||
> 显示「无法解密」,需要各自重填**。所以 v1.4.0 的归档里默认打包了它
|
||||
> (恢复时可以关掉,见 [8.3](#83-恢复))。
|
||||
|
||||
容器里归档落在**独立的命名卷** `workbuddy-portal_wb_backups`(对应 `/app/backups`),
|
||||
**刻意不与 `data/` 同卷**:`docker compose down -v` 会把同卷的正本与副本一起删掉 ——
|
||||
那正好是最需要备份的时刻。裸机部署下就是工程目录里的 `backups/`。
|
||||
|
||||
> **不要**在容器运行时用宿主机的 `manage.py` 去碰库(见 [4.5](#45-数据落点命名卷不是绑定挂载) 与 [11.5](#115-windows-绑定挂载的坑容器打不开数据库))。
|
||||
|
||||
### 8.2 备份
|
||||
|
||||
**方式 A:整体打包命名卷(最完整,升级前做)**
|
||||
**方式 A:用内置备份(推荐,v1.4.0 起)**
|
||||
|
||||
三种触发方式,产出的是同一种归档:
|
||||
|
||||
| 方式 | 怎么做 |
|
||||
|---|---|
|
||||
| 自动 | 「备份管理」页开 `backup_enabled`,设周期(默认 24 小时)与保留份数(默认 7)。调度线程每 20 秒检查一次,**有采集在跑就跳过**(不跟采集抢磁盘),到期自动打一份并按份数清理最旧的 |
|
||||
| 页面 | 「备份管理 → 立即备份」,列表里可**下载**(拿 zip)、**恢复**、**删除** |
|
||||
| 命令行 | `docker compose exec portal python manage.py backup --note "升级前"` |
|
||||
|
||||
```bash
|
||||
# 列出已有备份(大小 / 条数 / 积分 / 来源 / 时间),--prune 顺便清理
|
||||
docker compose exec portal python manage.py backups
|
||||
docker compose exec portal python manage.py backups --prune --keep 5
|
||||
|
||||
# 把归档拿到宿主机(备份留在容器里不算备份)
|
||||
docker compose cp portal:/app/backups/. ./backups/
|
||||
```
|
||||
|
||||
> **归档里的快照不是 `cp` 出来的**:走的是 SQLite 在线备份 API(`Connection.backup`),
|
||||
> 按页复制并持有读事务,所以**采集正在写的时候拿到的也是「某一时刻的完整库」**。
|
||||
> 直接 `cp data/usage.sqlite` 可能缺最近一段数据而**不报错**,这是最容易被忽略的差别。
|
||||
|
||||
**方式 B:整体打包命名卷(升级前做,最完整)**
|
||||
|
||||
```bash
|
||||
docker compose stop portal
|
||||
@@ -612,30 +795,6 @@ docker run --rm \
|
||||
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/backups":/backup \
|
||||
alpine:3.20 cp /data/usage.sqlite /backup/usage-$(date +%F).sqlite
|
||||
```
|
||||
|
||||
> 也可以用 SQLite 官方的在线备份 API,**不用停服务、不用手工 checkpoint**:
|
||||
> ```bash
|
||||
> docker compose exec -T portal python -c "
|
||||
> import sqlite3
|
||||
> s = sqlite3.connect('/app/data/usage.sqlite'); d = sqlite3.connect('/tmp/b.sqlite')
|
||||
> s.backup(d); d.close(); s.close()"
|
||||
> docker compose cp portal:/tmp/b.sqlite ./backups/usage-$(date +%F).sqlite
|
||||
> docker compose exec -T portal rm -f /tmp/b.sqlite
|
||||
> ```
|
||||
|
||||
**方式 C:逻辑导出(跨版本最安全,可读性最好)**
|
||||
|
||||
```bash
|
||||
@@ -643,11 +802,53 @@ docker compose exec portal python manage.py export-csv /app/data/exports
|
||||
docker compose cp portal:/app/data/exports/. ./backups/exports/
|
||||
```
|
||||
|
||||
建议:方式 B 每天跑(可用宿主机 cron)、方式 C 每季度跑一次、方式 A 在**升级前**跑。
|
||||
裸机部署把上面命令里的 `docker compose exec portal` 去掉即可,路径换成相对的 `data/`。
|
||||
建议:**方式 A 日常自动跑**(并定期把 zip 下载到容器之外/异地)、
|
||||
方式 B 在**升级前**跑、方式 C 每季度跑一次。
|
||||
裸机部署把上面命令里的 `docker compose exec portal` 去掉即可。
|
||||
|
||||
> ⚠️ **备份留在同一台机器上,只防「改错了」,不防「机器没了」**。
|
||||
> 内置备份负责「本机有历史版本可回滚」,**异地那一份要你自己安排**
|
||||
> (同步 `backups/` 到 NAS / 对象存储 / 另一台机器)。
|
||||
|
||||
### 8.3 恢复
|
||||
|
||||
**方式 A:用内置恢复(推荐)**
|
||||
|
||||
页面:「备份管理」→ 找到那一份 → 点「恢复」→ 先弹一次确认(是否连 `instance.json`
|
||||
一起回滚)→ 再弹一次最终确认。
|
||||
|
||||
```bash
|
||||
# 命令行:不加 --yes 只打印「将要发生什么」,不会动数据
|
||||
docker compose exec portal python manage.py restore usage-20260916-151043.zip
|
||||
docker compose exec portal python manage.py restore usage-20260916-151043.zip --yes
|
||||
```
|
||||
|
||||
它做的事,按顺序:
|
||||
|
||||
1. **先校验**归档(格式版本 / 条目齐全 / `PRAGMA integrity_check` / 库结构版本不高过当前程序)。
|
||||
验不过就**一个字节都不动**。
|
||||
2. **再给当前库自动打一份 `pre-restore` 快照**(`trigger=pre-restore`,在备份列表里能看到)。
|
||||
恢复错了你还能回去 —— 这是恢复链路的兜底。
|
||||
3. 解包 → 在**临时库**上先把结构迁移到当前版本 → 在一个 `BEGIN IMMEDIATE` 事务里
|
||||
**整表替换** `users` / `settings` / `usage_records` / `collect_runs` / `audit_log` / `captchas`。
|
||||
4. 把归档里的 `instance.json` 覆盖回来(原文件先另存 `.pre-restore-<时间戳>`)。
|
||||
5. 让**所有账号的会话立即失效**(`session_ver` 全体 +1)—— 恢复是全局性事件,
|
||||
旧会话描述的账号与权限可能已经被整个换掉了。
|
||||
|
||||
> **为什么是「整表替换」而不是「换文件」**:换文件需要停机、且要求没人持有文件句柄
|
||||
> (Windows 上会直接失败);整表替换在一个事务里完成,中途出错就是一次回滚,
|
||||
> 而且**老版本的备份(`user_version` 2 / 3)也能恢复** —— 迁移在临时库里先做完。
|
||||
> 另外它按**列名交集**搬数据,不是 `SELECT *`:老库的列是 `ALTER` 追加的,
|
||||
> 顺序与新库建表语句不一定一致,`SELECT *` 会**静默错位**(字段整体串位而值都「合法」)。
|
||||
|
||||
不想连密钥一起回滚(例如只想把数据退回去、保留本机当前的 `cookie_key`):
|
||||
|
||||
```bash
|
||||
docker compose exec portal python manage.py restore <文件名> --yes --no-instance
|
||||
```
|
||||
|
||||
**方式 B:整体换卷(容器起不来时的兜底)**
|
||||
|
||||
```bash
|
||||
docker compose down
|
||||
|
||||
@@ -665,9 +866,6 @@ docker compose exec portal python manage.py stats # 核对条数与积分
|
||||
|
||||
> **别把旧库和旧 WAL 混着用**:WAL 里记的是相对旧库的增量,配错会损坏数据。
|
||||
> 恢复时目标目录里**只放一个 `.sqlite`**(外加 `instance.json`)最省心。
|
||||
>
|
||||
> 只恢复了库、没恢复 `instance.json` 的话,所有账号的 Cookie 都会显示「无法解密」,
|
||||
> 各自重新粘贴一次即可 —— 历史用量数据不受影响。
|
||||
|
||||
### 8.4 迁移:从绑定挂载换到命名卷
|
||||
|
||||
@@ -745,8 +943,9 @@ sudo systemctl restart workbuddy-portal
|
||||
|---|---|---|---|
|
||||
| **1.1.0 → 1.2.0**(单用户 → 多用户) | 0 → 2 | 建 `users` 表并写入首个管理员;`settings` / `usage_records` / `collect_runs` / `audit_log` 改为 `(user_id, …)` 复合主键,**老数据整体归到第一个账号**;明文 Cookie **就地加密**;建 `captchas` 表 | 不需要 |
|
||||
| **1.2.0 → 1.3.0**(权限收敛) | 2 → 3 | **配置作用域收敛**:把管理员个人名下的调度与采集参数提升到实例级 `user_id=0`,再清掉个人残留;实例级不再保留 `cookie` / `user_agent` | 不需要 |
|
||||
| **1.3.0 → 1.4.0**(备份 + 对外加固) | 3 → 4 | 只做两件事:`users` 加一列 `session_ver`(非空、默认 0)、建 `backups` 表(备份索引)。**没有数据搬运、没有键位变动** | 不需要 |
|
||||
|
||||
两次迁移都是**幂等**的(可重复启动、可重复执行),各自会写一条审计:
|
||||
三次迁移都是**幂等**的(可重复启动、可重复执行),各自会写一条审计:
|
||||
|
||||
```bash
|
||||
docker compose logs portal | grep -iE "迁移|migrat|提升|promote"
|
||||
@@ -764,10 +963,10 @@ docker compose exec portal python manage.py stats # 各账号条数与积分
|
||||
|
||||
| 频率 | 做什么 |
|
||||
|---|---|
|
||||
| 每天 | 打开「概览」看「采集健康」;确认今天有采集记录 |
|
||||
| 每周 | 「日志管理」按 `warn` / `error` 筛一遍,看有没有 TLS 或解密类告警(**仅管理员**) |
|
||||
| 每月 | 确认各账号 Cookie 没过期(「配置管理」看提示);跑一次备份恢复演练 |
|
||||
| 每季度 | `manage.py vacuum`;出一份全量 CSV 归档;检查磁盘占用 |
|
||||
| 每天 | 打开「概览」看「采集健康」;确认今天有采集记录;确认「备份管理」里今天/昨天有一份 `auto` 备份 |
|
||||
| 每周 | 「日志管理」按 `warn` / `error` 筛一遍,看有没有 TLS 或解密类告警(**仅管理员**);`docker stats --no-stream` 看一眼内存有没有贴着 512m 上限 |
|
||||
| 每月 | 确认各账号 Cookie 没过期(「配置管理」看提示);**把最新归档下载到容器之外**;跑一次备份恢复演练(见下) |
|
||||
| 每季度 | `manage.py vacuum`;出一份全量 CSV 归档;检查磁盘占用与 `wb_backups` 卷的体积 |
|
||||
|
||||
一键体检:
|
||||
|
||||
@@ -775,10 +974,30 @@ docker compose exec portal python manage.py stats # 各账号条数与积分
|
||||
docker compose exec portal python manage.py status # 调度与最近采集
|
||||
docker compose exec portal python manage.py stats # 存档概况 + 逐账号明细
|
||||
docker compose exec portal python manage.py users # 账号 / 角色 / 凭证状态
|
||||
docker compose exec portal python manage.py backups # 备份份数 / 占用 / 来源 / 下次自动备份
|
||||
docker stats --no-stream workbuddy-portal # 实际 CPU / 内存占用
|
||||
```
|
||||
|
||||
磁盘长这样:数据库每 1000 条记录约 1.5 MB,`app.log` 滚动上限 2 MB × 3。
|
||||
增长慢,但**年度归档后记得对旧数据做取舍**(本项目不做自动清理,数据只增不减)。
|
||||
**备份恢复演练(每月一次,五分钟)**
|
||||
|
||||
不演练的备份等于没有备份 —— 只有在真的恢复过一次之后,你才知道那份 zip 是好的:
|
||||
|
||||
```bash
|
||||
# 1) 随便挑一份归档,先只看它校验是否通过(不加 --yes,不会动数据)
|
||||
docker compose exec portal python manage.py restore <文件名>
|
||||
|
||||
# 2) 真要演练恢复:确认当前库条数 → 恢复 → 再确认条数一致
|
||||
docker compose exec portal python manage.py stats | head -3
|
||||
docker compose exec portal python manage.py restore <文件名> --yes
|
||||
docker compose exec portal python manage.py stats | head -3
|
||||
```
|
||||
|
||||
> 恢复是**幂等且可回退**的:它会在动手前自动给你当前库打一份 `pre-restore` 快照,
|
||||
> 恢复错了就再恢复回那一份。演练完记得 `backups --prune --keep N` 把多出来的清掉。
|
||||
|
||||
磁盘长这样:数据库每 1000 条记录约 1.5 MB,`app.log` 滚动上限 2 MB × 3,
|
||||
每份归档约为数据库体积的 1/5(zip 压缩后)。增长慢,但**年度归档后记得对旧数据做取舍**
|
||||
(本项目不做自动清理,数据只增不减;备份会按 `backup_keep` 自动清理)。
|
||||
|
||||
### 自检脚本(开发者向)
|
||||
|
||||
@@ -837,9 +1056,11 @@ docker compose up -d # 环境变量,必须 up -d,restart 不生效
|
||||
|---|---|
|
||||
| `/logs`、`/logs/tail` | 403(导航里不显示) |
|
||||
| `/users`、`/api/users` | 403(导航里不显示) |
|
||||
| `/backups`、`/api/backups*`(含**下载**与**恢复**) | 403(导航里不显示) |
|
||||
| 任务管理页的调度表单 | 只读(时刻由管理员统一设定) |
|
||||
| 配置管理页的采集参数 | 只读 |
|
||||
| 本人的 Cookie / User-Agent | **可读写**(唯一可改的配置) |
|
||||
| 「个人中心 → 导出我的全部数据」 | **可用**(只含本人数据,**不含 Cookie 明文**) |
|
||||
|
||||
改权限:`docker compose exec portal python manage.py passwd alice 密码 --role admin`。
|
||||
|
||||
@@ -934,6 +1155,50 @@ docker compose exec portal date # 应该输出 CST / +0800
|
||||
echo "WB_DISABLE_SCHEDULER=1" >> .env && docker compose up -d
|
||||
```
|
||||
|
||||
> 注意这只关调度。手动采集、`manage.py collect`、**自动备份**都还在跑
|
||||
> (自动备份由调度线程触发,所以关掉调度后自动备份也停了 ——
|
||||
> 停调度期间请手动点几次「立即备份」)。
|
||||
|
||||
### 11.10 拿不到管理员初始口令
|
||||
|
||||
**症状**:第一次 `docker compose up -d` 之后想登录,不知道密码;日志里也找不到那行提示。
|
||||
|
||||
原因通常是**没设 `WB_ADMIN_PASSWORD`**(v1.4.0 起没有默认口令),而提示只在
|
||||
「首次初始化那一刻」打印一次。依次试:
|
||||
|
||||
```bash
|
||||
# 1) 翻日志(默认只给最近若干行,容器重启多轮后提示可能已被挤出,加 --tail 或直接 grep)
|
||||
docker compose logs portal | grep -A6 '管理员初始口令'
|
||||
docker compose logs --tail=2000 portal | grep -A6 '口令'
|
||||
|
||||
# 2) 如果管理员早就建好了,那行不会再出现 —— 直接重置:
|
||||
docker compose exec portal python manage.py passwd admin '一个新的强密码'
|
||||
|
||||
# 3) 或者看数据库里到底有没有账号
|
||||
docker compose exec portal python manage.py users
|
||||
```
|
||||
|
||||
> **口令找不回来是有意的**:库里只有 PBKDF2 散列,明文只在生成时打印那一次、
|
||||
> 不落盘。所以别指望「再查一次」——重置是正道。
|
||||
> 下次部署记得在 `.env` 里显式写 `WB_ADMIN_PASSWORD`。
|
||||
>
|
||||
> 如果 `manage.py users` 显示**一个账号都没有**,说明初始化那步失败了,
|
||||
> 去 `docker compose logs portal | grep -iE "init|error|traceback"` 看真实原因
|
||||
> (最常见是数据卷权限,见 [4.5](#45-数据落点命名卷不是绑定挂载))。
|
||||
|
||||
### 11.11 备份相关
|
||||
|
||||
| 症状 | 原因 / 处理 |
|
||||
|---|---|
|
||||
| `/app/backups` 里是空的,但「备份管理」页说有 N 份 | 索引与实际磁盘不一致。页面每次打开都会 `sync_index()` 双向对齐,刷新一次即可;命令行用 `manage.py backups` 也会重建索引 |
|
||||
| 列表里某一份标着「文件已不存在」 | 被手工删过(或恢复前的临时目录被清)。索引**故意保留这一行**,是为了留下「这里曾有过一份」的痕迹;用 `backups --prune` 或页面上的删除把它清掉 |
|
||||
| 备份失败,日志里没有明显报错 | 自动备份的失败会记 `log.error`,看 `docker compose logs portal \| grep -i 备份`。常见原因是磁盘满或卷权限 |
|
||||
| 恢复时提示「有采集任务正在运行」 | 设计如此:恢复要持采集锁。等当前采集结束(`manage.py status` 看互斥锁),或先停调度 |
|
||||
| 恢复后所有人都被踢下线 | **设计如此**。恢复会让全体 `session_ver` +1 —— 归档里的会话版本与旧 Cookie 可能恰好相等,那样旧会话会带着「已被替换掉的账号与权限」继续用 |
|
||||
| 恢复后所有 Cookie 显示「无法解密」 | 恢复时选了「不恢复 instance.json」,而两边的 `cookie_key` 不同。重新走一次恢复并带上 `instance.json`(默认会带),或让各账号重填 Cookie |
|
||||
| 升级前想留一份最完整的 | 用 [8.2](#82-备份) 的方式 B(整体打包命名卷),它连 `exports/` 与 `instance.json` 一起包 |
|
||||
| 想改保留份数 / 关掉自动备份 | 「备份管理 → 自动备份设置」里的 `backup_enabled` / `backup_interval_hours` / `backup_keep`(实例级,仅管理员) |
|
||||
|
||||
---
|
||||
|
||||
## 十二、配置项速查
|
||||
@@ -943,6 +1208,7 @@ echo "WB_DISABLE_SCHEDULER=1" >> .env && docker compose up -d
|
||||
表主键是 `(user_id, key)`:`user_id=0` 表示**实例级**(所有账号共用),其余是**个人级**。
|
||||
|
||||
**v1.3.0 起只有两个键是个人级的**(`cookie` / `user_agent`)—— 普通账号唯一能改的东西。
|
||||
其余全部是**实例级**(`GLOBAL_KEYS`,v1.4.0 起共 **23** 个),只有管理员能改。
|
||||
写权限判断统一走 `config.writable_by(key, is_admin)`,页面与接口用的是同一个函数。
|
||||
|
||||
| 键 | 默认 | 作用域 | 谁能改 | 说明 |
|
||||
@@ -959,6 +1225,12 @@ echo "WB_DISABLE_SCHEDULER=1" >> .env && docker compose up -d
|
||||
| `schedule_times` | `09:00,17:00` | 实例级 | 仅管理员 | 每日时刻,逗号分隔,本地时区 |
|
||||
| `catch_up` | `1` | 实例级 | 仅管理员 | 启动补跑开关 |
|
||||
| `catch_up_grace_hours` | `12` | 实例级 | 仅管理员 | 补跑宽限期(1~168 小时) |
|
||||
| `max_schedule_slots_per_day` | `6` | 实例级 | 仅管理员 | **每日调度时刻数上限**(1~12,代码硬顶 12)。挡住「填 200 个时刻」 |
|
||||
| `collect_min_interval_seconds` | `60` | 实例级 | 仅管理员 | **同一账号两次手动采集的最小间隔**(0~3600 秒);期间再点会 `429` |
|
||||
| `collect_max_range_days` | `31` | 实例级 | 仅管理员 | **单次采集的最长跨度**(1~31 天,代码硬顶 31 = 1 个月)。改大也突破不了硬顶 |
|
||||
| `backup_enabled` | `1` | 实例级 | 仅管理员 | 是否开启自动备份 |
|
||||
| `backup_interval_hours` | `24` | 实例级 | 仅管理员 | 备份周期(1~720 小时) |
|
||||
| `backup_keep` | `7` | 实例级 | 仅管理员 | 保留最近几份,超出的删最旧的(1~100) |
|
||||
| `page_size` | `200` | 实例级 | 仅管理员 | 采集单页条数(20~1000) |
|
||||
| `rewind_minutes` | `2` | 实例级 | 仅管理员 | 断点回退分钟数(0~120) |
|
||||
| `drift_tolerance_minutes` | `5` | 实例级 | 仅管理员 | 云端时间漂移告警阈值(0~720) |
|
||||
@@ -994,13 +1266,19 @@ echo "WB_DISABLE_SCHEDULER=1" >> .env && docker compose up -d
|
||||
| `WB_BIND` | `0.0.0.0` | 宿主机绑定地址(仅 compose 用) |
|
||||
| `WB_HOST` / `WB_PORT` | `0.0.0.0` / `8848` | 容器内监听地址 / 端口 |
|
||||
| `WB_DATA_DIR` / `WB_LOG_DIR` / `WB_DB` | `/app/data` / `/app/logs` / `<数据目录>/usage.sqlite` | 路径覆盖 |
|
||||
| `WB_COOKIE_SECURE` | `0` | `1` = 会话 Cookie 只走 HTTPS(纯 HTTP 部署必须留 `0`) |
|
||||
| `WB_DISABLE_SCHEDULER` | `0` | `1` = 不启动调度线程 |
|
||||
| `WB_ADMIN_USER` / `WB_ADMIN_PASSWORD` | `admin` / 空 | 首个管理员(**仅库为空时生效**) |
|
||||
| `WB_BACKUP_DIR` | `/app/backups` | 备份归档落点(**不要放进 `data/`**,见 [8.1](#81-备份什么)) |
|
||||
| `WB_COOKIE_SECURE` | `0` | `1` = 会话 Cookie 只走 HTTPS(纯 HTTP 部署必须留 `0`)。**同时是下发 HSTS 的开关之一** |
|
||||
| `WB_TRUST_PROXY` | `0` | `1` = 信任 `X-Forwarded-For`(取**最右侧**合法 IP)。**直接暴露公网必须留 0**,见 [第六节](#六反向代理与-https) |
|
||||
| `WB_FORCE_HTTPS` | `0` | `1` = 非 HTTPS 的 GET/HEAD 跳转(POST 不跳) |
|
||||
| `WB_ACCESS_LOG` | `1` | 记访问日志到 `logs/app.log`(跳过静态资源与验证码图) |
|
||||
| `WB_THREADS` | `4`(compose)/ `8`(代码默认) | waitress 线程数 = 单实例并发处理慢请求的上限 |
|
||||
| `WB_CPUS` / `WB_MEM_LIMIT` / `WB_PIDS_LIMIT` | `1.0` / `512m` / `256` | 容器资源上限(仅 compose 用,见 [4.4](#44-env-主要变量)) |
|
||||
| `WB_DISABLE_SCHEDULER` | `0` | `1` = 不启动调度线程(**自动备份也会一起停**) |
|
||||
| `WB_ADMIN_USER` / `WB_ADMIN_PASSWORD` | `admin` / 空 | 首个管理员(**仅库为空时生效**)。留空则随机生成、只在启动日志打印一次 |
|
||||
| `WB_IMPORT_CREDS` | `0` | 启动时从挂载的编辑器配置导入 Cookie |
|
||||
| `WB_IMPORT_XLSX` | 空 | 启动时导入指定路径的官网 xlsx |
|
||||
| `WB_IMAGE` | Gitea 地址 | 镜像名(仅 compose 用) |
|
||||
| `WB_HOST_DATA_DIR` / `WB_HOST_LOG_DIR` | `./data` / `./logs` | 仅叠加 `hostdir` 时有效(**只建议 Linux**) |
|
||||
| `WB_HOST_DATA_DIR` / `WB_HOST_LOG_DIR` / `WB_HOST_BACKUP_DIR` | `./data` / `./logs` / `./backups` | 仅叠加 `hostdir` 时有效(**只建议 Linux**) |
|
||||
|
||||
### 12.3 `manage.py` 子命令速查
|
||||
|
||||
@@ -1019,5 +1297,8 @@ echo "WB_DISABLE_SCHEDULER=1" >> .env && docker compose up -d
|
||||
| `passwd USER [PASSWORD] [--role admin\|user] [--activate]` | 重置 / 创建账号 |
|
||||
| `status` | 各账号的调度与最近采集状态 |
|
||||
| `vacuum` | 整理数据库(checkpoint + VACUUM) |
|
||||
| `backup [--note 说明]` | 立即打一份备份(所有 `data/*.sqlite` + `instance.json` → zip),并按保留份数清理最旧的 |
|
||||
| `backups [--prune] [--keep N]` | 列出备份(大小 / 条数 / 积分 / 来源 / 时间);`--prune` 顺便清理 |
|
||||
| `restore <文件名> [--yes] [--no-instance]` | **从备份恢复**(整表替换)。**不加 `--yes` 只打印将要发生什么**;`--no-instance` = 不覆盖 `instance.json` |
|
||||
|
||||
Docker 部署下前面加 `docker compose exec portal`。
|
||||
|
||||
在新工单中引用
屏蔽一个用户