文件
workbuddy-portal/docs/DEPLOYMENT.md
T
wangchuanli 5942f7fe1b chore(ci): 新增 tools/push-all.sh —— 一条命令推代码 + 推镜像
- Token 只从 GITEA_TOKEN 环境变量读,不落 .git/config / URL / reflog
- 用 -c credential.helper= 屏蔽 GCM:非交互会话里它会挂住等弹窗
- docker login 用 --password-stdin,Token 不进命令行历史
- 支持可选版本 tag:tools/push-all.sh 1.1.0
- DEPLOYMENT.md 补 5.0 节,说明脚本行为与手工推送的替代做法
2026-09-14 14:58:50 +08:00

497 行
16 KiB
Markdown
原始文件 Blame 文件历史

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# 部署与运维指南
> 面向**运维 / 部署者**。从零到跑起来,以及跑起来之后的备份、升级、排错。
**目录**
- [一、部署方式怎么选](#一部署方式怎么选)
- [二、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 http://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` | `./data` | `usage.sqlite`(正本)、`instance.json`(secret_key)、`exports/` |
| `/app/logs` | `./logs` | `app.log`(滚动 2 MB × 3) |
**绑定挂载**而非命名卷,是为了:备份就是拷目录;宿主机上的 `manage.py` 能直接读同一份数据。
> **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 流式下载都可用。
---
## 三、裸机部署
### 3.1 Windows
```bat
git clone http://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 http://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 准备:本机允许 HTTP 注册表
Gitea 走的是 **HTTP**,Docker 默认只允许 HTTPS。Docker Desktop:**Settings → Docker Engine**,
在 `daemon.json` 里加:
```json
{
"insecure-registries": ["git.iwali.top"]
}
```
`Apply & Restart`。Linux 上同理改 `/etc/docker/daemon.json` 后 `sudo systemctl restart docker`。
> 这是**内网自托管服务**的常规做法。若 Gitea 前面有带证书的 Caddy/nginx,
> 用 `https://` 地址即可,不必开 insecure。
### 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
volumes:
- ./data:/app/data
- ./logs:/app/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 \
http://git.iwali.top/api/v1/packages/wangchuanli?type=container
```
---
## 六、备份与恢复
### 6.1 备份什么
| 文件 | 重要性 | 说明 |
|---|---|---|
| `data/usage.sqlite` | ★★★ | **数据正本**,丢了要重新采集,且官网窗口外的数据永久丢失 |
| `data/usage.sqlite-wal` / `-shm` | ★★★ | WAL 模式下未 checkpoint 的数据在这里,**要一起拷** |
| `data/instance.json` | ★★ | 含 `secret_key`,丢了所有人都要重新登录(数据不受影响) |
| `data/exports/*.csv` | ★ | 导出快照,可再生 |
| `logs/` | ☆ | 排错用,可再生 |
`.env` 不在里面——它含密码,**单独用密码管理器保管**。
### 6.2 备份命令
```bash
# 方式 A:先 checkpoint 再拷(推荐,最干净)
docker compose exec portal python -c "
from workbuddy_portal import db
c = db.connect(); c.execute('PRAGMA wal_checkpoint(TRUNCATE)')
"
# Windows 宿主机
copy data\usage.sqlite D:\backup\usage-%DATE%.sqlite
# Linux 宿主机
cp data/usage.sqlite ~/backup/usage-$(date +%F).sqlite
# 方式 B:直接整体拷(含 -wal / -shm)
docker compose stop portal
tar czf backup-$(date +%F).tar.gz data/
docker compose start portal
# 方式 C:逻辑导出(跨版本最安全)
docker compose exec portal python manage.py export-csv /app/data/exports
```
建议方式 A 配合计划任务每天跑一次;每季度用方式 C 出一份逻辑快照。
### 6.3 恢复
```bash
docker compose down
cp ~/backup/usage-2026-09-14.sqlite data/usage.sqlite
rm -f data/usage.sqlite-wal data/usage.sqlite-shm # 关键:清掉旧 WAL
docker compose up -d
docker compose exec portal python manage.py stats # 核对条数
```
> **别把旧库和旧 WAL 混着用**。WAL 里记的是相对旧库的增量,配错会损坏数据。
---
## 七、升级与回滚
### 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` | 目录属主不对:`sudo chown -R 1000:1000 ./data` |
| `database is locked` | 有另一个写进程(另一个容器?宿主机上的 CLI?);等它跑完 |
| `disk I/O error` | 挂载文件系统不支持 SQLite 的锁语义。改用本地盘或 Docker 命名卷 |
### 采集相关
| 报错 | 处理 |
|---|---|
| `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` | 启动时自动导入 |