文件
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

16 KiB
原始文件 Blame 文件历史

部署与运维指南

面向运维 / 部署者。从零到跑起来,以及跑起来之后的备份、升级、排错。

目录


一、部署方式怎么选

场景 建议
有 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 步骤

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 不一致:

sudo chown -R 1000:1000 ./data ./logs

2.5 常用命令

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

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

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:

[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
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 时要注意两点,否则会踩坑:

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)后:

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 里加:

{
  "insecure-registries": ["git.iwali.top"]
}

Apply & Restart。Linux 上同理改 /etc/docker/daemon.json 后 sudo systemctl restart docker。

这是内网自托管服务的常规做法。若 Gitea 前面有带证书的 Caddy/nginx, 用 https:// 地址即可,不必开 insecure。

5.2 登录

docker login git.iwali.top -u wangchuanli
# 密码用 Personal Access Token(Gitea「设置 → 应用 → 生成令牌」,
# 勾选 write:package;不要用网页登录密码)

5.3 构建并推送

# 镜像名默认已经是注册表地址(见 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 在另一台机器上拉取运行

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 验证远端

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 备份命令

# 方式 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 恢复

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

git pull
docker compose build
docker compose up -d                    # 重建容器,数据在挂载卷里不受影响
docker compose exec portal python manage.py stats

回滚:把 .env 里的 WB_IMAGE 指回旧版本标签,然后

docker compose up -d --no-build

裸机

git pull
.venv/bin/pip install -r requirements.txt
sudo systemctl restart workbuddy-portal

升级前

  1. 先备份(见第六节)——schema.sql 用的是 CREATE TABLE IF NOT EXISTS, 加表加索引是安全的,但改列需要手工迁移,所以备份是唯一保险。
  2. 看一眼 CHANGELOG 有没有破坏性变更。

八、日常巡检

频率 做什么
每天 打开「概览」看「采集健康」;确认今天有采集记录
每周 「日志管理」按 warn / error 筛一遍,看有没有 TLS 或解密类告警
每月 确认 Cookie 没过期(「配置管理」看提示);跑一次备份恢复演练
每季度 manage.py vacuum;出一份全量 CSV 归档;检查磁盘占用

一键体检:

docker compose exec portal python manage.py status
docker compose exec portal python manage.py stats

九、排错

容器起来了但页面打不开

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; 若手工传过文件,执行:

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 说明云端该时段确实没数据

时区不对导致日期错位

docker compose exec portal date        # 应该输出 CST / +0800

若不是,检查 .env 的 TZ=Asia/Shanghai,改完 docker compose up -d 重建容器 (TZ 是环境变量,restart 不生效)。

想临时关掉自动采集

「任务管理 → 取消勾选『启用调度』→ 保存」。或者

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 启动时自动导入