git.iwali.top 有 Let's Encrypt 通配证书(*.iwali.top),HTTPS 全程可用: https://git.iwali.top/api/v1/version -> 200 https://git.iwali.top/v2/ -> 401(Bearer 认证,正常) 证书链校验 ssl_verify_result=0(受信),因此: - git remote 改为 https://git.iwali.top/...(原为 http://) - 镜像名 git.iwali.top/... 由 Docker 默认按 HTTPS 访问, **不再需要 daemon.json 配 insecure-registries**(明文传输会暴露 Token) - DEPLOYMENT.md 5.1 改写为「默认走 HTTPS」,HTTP + insecure 降级为补充说明 - Dockerfile 的 image.source 标签、README/DEPLOYMENT 的 clone 地址同步改 https
503 行
16 KiB
Markdown
503 行
16 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` | `./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 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
|
||
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 \
|
||
https://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` | 启动时自动导入 |
|