文件
workbuddy-portal/docker-compose.yml
T
wangchuanli 3751dffef9 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 配置
2026-09-18 08:46:34 +08:00

113 行
6.3 KiB
YAML

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
# =============================================================================
# workbuddy-portal —— 单服务部署(采集 / 存储 / 呈现都在同一个进程里)
#
# docker compose up -d --build 本机构建并启动
# docker compose logs -f 跟踪日志
# docker compose down 停止(数据在命名卷里,不会丢)
#
# 设计取舍:
# * 刻意只有**一个**服务:SQLite 是单写者,调度线程也在 Web 进程内,
# 多副本只会带来锁竞争与重复采集,所以不做横向扩展。
# * 数据用**命名卷**而不是绑定挂载。这不是随手选的:
# Windows + Docker Desktop 走 9p 挂载,宿主的 Windows 进程一旦访问过
# 这个 WAL 库(哪怕只是 `manage.py stats` 这种纯读),容器侧下一次打开就会
# `sqlite3.OperationalError: unable to open database file`,且**不会自愈**,
# 必须重启容器。命名卷住在 Linux VM 的本地文件系统里,不存在这个问题。
# 需要在宿主机直接看到数据/日志时,叠加 docker-compose.hostdir.yml
# (只建议 Linux 宿主机用)。
# =============================================================================
name: workbuddy-portal
services:
portal:
# 默认就用 Gitea 注册表里的名字,构建完即可直接 push,不用再补 tag
image: ${WB_IMAGE:-git.iwali.top/wangchuanli/workbuddy-portal:latest}
build:
context: .
dockerfile: Dockerfile
container_name: workbuddy-portal
restart: unless-stopped
init: true # tini 接管 PID 1:docker stop 能干净地传到 python
# ---- 容器资源上限(对外提供服务时的第一道闸门)----
# 应用层已经做了「采集频率 / 并发 / 跨度」三重限制,但那些是**业务**刹车;
# 这里限制的是**进程**本身能吃掉多少宿主机资源 —— 两者都要有:
# 业务刹车管「正常的重活别做太多」,容器上限管的是一次异常(内存泄漏、
# 正则回溯、某次超大导出)能不能把整台机器带下去。
#
# 单写者架构下**不要**靠加副本扛负载,所以「限制单实例资源 + 限制单账号
# 频率」才是正解,而不是横向扩展(见文件头的设计取舍说明)。
cpus: "${WB_CPUS:-1.0}" # 1 核:SQLite 单写者,多给核也并行不起来
mem_limit: "${WB_MEM_LIMIT:-512m}"
memswap_limit: "${WB_MEM_LIMIT:-512m}" # 与 mem_limit 相等 = 禁用 swap,
# 否则内存超限会悄悄滑进 swap,
# 表现为「越来越慢」而不是「被 OOM 杀掉」
pids_limit: ${WB_PIDS_LIMIT:-256} # 挡 fork 炸弹
# 文件句柄:SQLite 会在主库之外再持有 -wal / -shm,备份快照与恢复期间
# 还要同时开临时库 + ATTACH 源库,默认 1024 在高并发下偏紧。
ulimits:
nofile:
soft: 4096
hard: 8192
ports:
- "${WB_BIND:-0.0.0.0}:${WB_PORT:-8848}:8848"
environment:
TZ: ${TZ:-Asia/Shanghai} # 决定「每日 09:00 / 17:00」调度与日志时间戳
WB_HOST: 0.0.0.0
WB_PORT: "8848"
WB_ADMIN_USER: ${WB_ADMIN_USER:-admin}
# 留空时程序会生成**随机**口令并只在启动日志里打印一次(不再有 admin123 兜底),
# 所以首次部署要去 `docker compose logs portal | grep 口令` 抄一次。
WB_ADMIN_PASSWORD: ${WB_ADMIN_PASSWORD:-}
WB_DISABLE_SCHEDULER: ${WB_DISABLE_SCHEDULER:-0}
# ---- 反向代理与传输安全(三个必须一起决定,别只改一个)----
# WB_TRUST_PROXY:默认 0 = 不信任 X-Forwarded-For。
# 直接暴露给公网时必须留 0 —— 否则攻击者每次换一个伪造的 XFF,
# 验证码限速 / 注册配额 / 登录锁定三道 IP 防线会同时失效。
# 置 1 的前提:**你自己的**反代会写这个头。开启后程序取 XFF 里
# **最右侧**的合法 IP(最近一跳由你的代理写入,客户端加不进去),
# 所以 nginx 写 $proxy_add_x_forwarded_for(保留链路,便于排查)或
# 写 $remote_addr(覆盖)都可以 —— 关键是别去信最左边那段。
WB_TRUST_PROXY: ${WB_TRUST_PROXY:-0}
# 前面挂了 HTTPS 反代时置 1:读到 X-Forwarded-Proto: https 就不跳转
WB_FORCE_HTTPS: ${WB_FORCE_HTTPS:-0}
# 会话 Cookie 是否只走 HTTPS。纯 HTTP 部署必须留 0:设成 1 时浏览器
# 不会回传会话 Cookie,表现为「登录成功又立刻跳回登录页」。改它要 up -d(环境变量)。
WB_COOKIE_SECURE: ${WB_COOKIE_SECURE:-0}
# 访问日志(waitress 自己不记 access log,出事无据可查时很要命)
WB_ACCESS_LOG: ${WB_ACCESS_LOG:-1}
# waitress 线程数 = 单实例能同时吃进几个慢请求(采集/导出/备份恢复)
WB_THREADS: ${WB_THREADS:-4}
WB_BACKUP_DIR: /app/backups
WB_IMPORT_CREDS: ${WB_IMPORT_CREDS:-0}
WB_IMPORT_XLSX: ${WB_IMPORT_XLSX:-}
volumes:
- wb_data:/app/data # 数据正本 + 导出 + secret_key
- wb_logs:/app/logs # 应用日志(滚动 2 MB × 3)
# 备份**单独一个卷**:备份与正本同卷时,一次 `down -v` 或卷损坏会把
# 两者一起带走 —— 那正是最需要备份的时刻。分开挂才有意义。
- wb_backups:/app/backups
# 可选:把编辑器配置挂进来,配合 WB_IMPORT_CREDS=1 自动接管 cookie
# - ${WB_EDITOR_SETTINGS:-./nonexistent.json}:/mnt/editor-settings.json:ro
healthcheck:
test: ["CMD", "python", "/app/docker/healthcheck.py"]
interval: 30s
timeout: 6s
start_period: 20s
retries: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
wb_data:
wb_logs:
# 备份卷。**必须与 wb_data 分开** —— 同卷时 `down -v` 会把正本与备份一起删。
# 想让它跟着正本走(比如整套搬到别的机器)就先 `docker run --rm -v
# workbuddy-portal_wb_backups:/b -v $PWD/backups:/o alpine cp -a /b/. /o/`
# 把归档捞到宿主机目录里,再一起搬。
wb_backups: