Files
inkwatch/deploy/docker-compose.yml
T
PMon Dev b3db58df06 rename: 项目对外名定为「墨观 InkWatch」
<user> 2026-09-16 定名。本次只改**用户看得见的那一半**,
内部标识(包名 / FFI 符号 / applicationId / compose project name / 卷名 / 数据库名)
一律保持 `pmon` 不动 —— 理由与"若要彻底改名该怎么做"见 docs/rename-inkwatch.md。

已改:
  · 界面标题:采集页「墨观 采集端」、查看页「墨观 查看端」、MaterialApp.title「墨观」
  · 桌面图标名 android:label="墨观"(刻意短,避免被系统截断)
  · 前台服务通知「墨观 正在采集」
  · 日志前缀 [PMon] → [inkwatch](**刻意用 ASCII**:中文在 Dart AOT 里按 UTF-16LE
    存,拿 grep -F 核验恒为 0 —— 这个坑踩过,日志前缀别用中文)
  · pubspec description 换成正式描述(顺手清掉 Flutter 模板残留 "A new Flutter project.")
  · README / docs 抬头与标题;方案文档改名
    docs/pmon-v4-architecture.md → docs/inkwatch-v4-architecture.md(引用同步)
  · 命名约束落地:对外文案不得出现"监视/监听/追踪/窥"类词
    (方案 2.1:产品不提供任何绕过对方操作的隐蔽监控能力,这是过审与杀软信誉的前提)

刻意没改(改了会出事):
  · Dart 包名 pmon_client —— 全部 import 失效,纯风险无收益
  · applicationId com.pmon.pmon_client —— 已装机变成"另一个 App",无法覆盖升级、
    令牌与设备归属断链。越接近上架越不该改
  · Rust 39 个 _pmon_* FFI 符号 —— 要与 .so 严格配对
  · ★ docker compose 的 name: pmon —— **它是卷名锚点**(pmon_pg_data),
    改成 inkwatch 会让 compose 去找一套新空卷 = 生产清库。
    故在其上方加了防呆注释,防止后来者顺手改掉

新增 docs/rename-inkwatch.md:完整记录改了什么、没改什么、为什么,
以及"若要连内部标识一起改"的正确顺序与风险评估(结论:不建议)。

验证:客户端 282/282、dart analyze 干净、codec/protocol 离线契约通过。
2026-09-16 12:09:53 +08:00

214 lines
9.4 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 墨观 云端部署编排(单机起步,可平滑扩多实例)
# 组件:Nginx + gateway + business + redis + postgres(timescaledb)
# 见方案 V4.0 第 11 章
# ⚠️⚠️ 这个 name 是 **Docker 卷名的锚点**(`pmon_pg_data` 等),
# **绝对不要**因为"项目改名叫墨观了"就把这里改成 inkwatch ——
# 改了之后 compose 会去找一套**新的空卷**,数据库数据全丢(等于把生产清库)。
# 项目显示名(界面/文档/APK)与这里的内部标识是两件事,详见 docs/rename-inkwatch.md。
name: pmon
services:
# ---------- 接入层:反向代理 + WSS 升级 ----------
# 默认仅 HTTP(开箱可用)。启用 HTTPS 见 deploy/nginx.ssl.conf.example
nginx:
image: nginx:1.27-alpine
ports:
- "80:80"
# 启用 HTTPS 时取消下行注释,并把 nginx.conf 换成 ssl 模板
# - "443:443"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
# 证书目录可选:目录为空时默认 HTTP 配置不读取证书
- ./certs:/etc/nginx/certs:ro
- nginx_logs:/var/log/nginx
depends_on:
gateway:
condition: service_healthy
restart: unless-stopped
logging: &logging
driver: json-file
options:
max-size: "10m" # 日志有界:防磁盘打满(见方案 ADR-001)
max-file: "3"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1/healthz"]
interval: 20s
timeout: 5s
retries: 3
# ---------- 网关:无状态 Socket.io 转发 ----------
gateway:
build:
# 上下文取仓库根:网关需引用 server/shared 与 shared/(协议+编解码)
context: ..
dockerfile: deploy/Dockerfile.gateway
environment:
PORT: 3000
REDIS_URL: redis://redis:6379
DATABASE_URL: postgres://${POSTGRES_USER:-pmon}:${POSTGRES_PASSWORD:-pmon_dev_only}@postgres:5432/${POSTGRES_DB:-pmon}
REDIS_ADAPTER: "${REDIS_ADAPTER:-0}" # 多实例扩容时置 1
CORS_ORIGIN: "${CORS_ORIGIN:-*}"
# ── 第二步安全开关(与 business 段同名同义,默认全关 = 兼容模式)──────
# ⚠️ 这两个必须传:**设备令牌校验发生在网关的 onHello 里**,不传则
# REQUIRE_DEVICE_TOKEN 形同虚设(.env 里设了也没人读)。
# 真实事故:2026-09-15 部署评估时发现 business 段传了、gateway 段漏传,
# 而 tests/auth-mode-test.js 是直接 spawn 进程传 env,测不到 compose 这条通路。
# 现由 tests/dockerfile-coverage-test.js「开关通路」段锁定。
REQUIRE_AUTH: "${REQUIRE_AUTH:-0}"
REQUIRE_DEVICE_TOKEN: "${REQUIRE_DEVICE_TOKEN:-0}"
# 注意:--expose-gc / --max-old-space-size 已在 Dockerfile 的 CMD 中传入。
# 不要在此重复配置 NODE_OPTIONS,否则 Node 会报 "bad option"。
depends_on:
redis:
condition: service_healthy
postgres:
condition: service_healthy
restart: unless-stopped
# 长连接场景关键调优:fd 限制必须显式放宽(见方案 ADR-001)
ulimits:
nofile:
soft: 65536
hard: 65536
# 端口映射:网关在默认桥接网络内,通过服务名 redis/postgres 访问依赖组件。
# 注意:不要用 network_mode: host —— 那会脱离 compose 网络,导致
# getaddrinfo ENOTFOUND redis 之类的服务名解析失败。
ports:
- "3000:3000"
logging: *logging
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 15s
timeout: 5s
start_period: 10s
retries: 3
# 部署时先摘流量再停实例(滚动更新,见方案 11.3)
stop_grace_period: 15s
# ---------- 业务服务:账号 / 配对 / 授权 ----------
business:
build:
# 上下文取仓库根(同 gateway,需引用 server/shared)
context: ..
dockerfile: deploy/Dockerfile.business
environment:
PORT: 4000
# 凭据从环境注入(默认值为本地开发用,生产务必覆盖)
DATABASE_URL: postgres://${POSTGRES_USER:-pmon}:${POSTGRES_PASSWORD:-pmon_dev_only}@postgres:5432/${POSTGRES_DB:-pmon}
REDIS_URL: redis://redis:6379
# 配对接口按来源 IP 限流,需要信任反代头。默认 1 层(本栈的 nginx)。
# 若改了拓扑(例如让业务服务直接对外),必须同步调整,否则
# X-Forwarded-For 可被伪造而绕过 IP 限流。直连语义填 0。
TRUST_PROXY_HOPS: "${TRUST_PROXY_HOPS:-1}"
# 单 IP 配对验证次数上限(默认 30 次/小时,见 shared/pairing.js)。
# NAT 出口后合法用户很多时按需调大。
PAIRING_IP_LIMIT: "${PAIRING_IP_LIMIT:-30}"
# ── 第二步安全开关(默认全关 = 兼容模式,旧客户端不受影响)──────────
# REQUIRE_AUTH=1 强制登录:无令牌一律 401;同时关闭自助注册
# (需要开自助注册再另设 ALLOW_OPEN_REGISTER=1)
# ENFORCE_DEVICE_QUOTA=1 按档位限制名下设备数(免费 3 / 专业 10 / 团队 30)
# 切换时机由运维掌握:改环境变量重启即可,不必改代码重新评审。
REQUIRE_AUTH: "${REQUIRE_AUTH:-0}"
ENFORCE_DEVICE_QUOTA: "${ENFORCE_DEVICE_QUOTA:-0}"
# 强制登录时是否仍允许自助注册(默认关)。**只有业务服务读**,
# 但同样必须传:不传则 .env 里设了也没人读(与网关那两个同一类事故)。
ALLOW_OPEN_REGISTER: "${ALLOW_OPEN_REGISTER:-0}"
# 业务服务同时依赖 PG(授权/账号)与 Redis(配对码)
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
restart: unless-stopped
ports:
- "4000:4000"
logging: *logging
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:4000/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
interval: 20s
timeout: 5s
retries: 3
# ---------- 实时层:环形缓冲 / 在线态 / 订阅关系 / 配对码 ----------
redis:
image: redis:7.4-alpine
command: >
redis-server
--appendonly no
--maxmemory 256mb
--maxmemory-policy allkeys-lru
--save ""
volumes:
- redis_data:/data
# 仅绑定 127.0.0.1:本机测试脚本可直连,不暴露到公网
ports:
- "127.0.0.1:6379:6379"
restart: unless-stopped
logging: *logging
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
# ---------- 存储层:元数据 + 时序(TimescaleDB 扩展) ----------
postgres:
# 生产用 TimescaleDB;本地/CI 无该镜像时可切换为普通 PG:
# POSTGRES_IMAGE=postgres:15-alpine docker compose up -d
# init.sql 已做成"扩展可缺省":无 TimescaleDB 时只跳过超表/自动保留/连续聚合策略,
# 表结构与指标列完全一致,功能测试与开发不受影响(见 init.sql 顶部说明)。
image: ${POSTGRES_IMAGE:-timescale/timescaledb:2.17.2-pg16}
environment:
POSTGRES_USER: ${POSTGRES_USER:-pmon}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-pmon_dev_only}
POSTGRES_DB: ${POSTGRES_DB:-pmon}
volumes:
- pg_data:/var/lib/postgresql/data
- ./init.sql:/docker-entrypoint-initdb.d/init.sql:ro
# /migrate.sql 放在 initdb 目录**之外**:init.sql 用 \i 引用它,
# 而下面的 migrate 服务也跑它 —— 两条路径共用同一份定义。
- ./migrate.sql:/migrate.sql:ro
# 仅绑定 127.0.0.1:本机测试脚本可直连,不暴露到公网
ports:
- "127.0.0.1:5432:5432"
restart: unless-stopped
logging: *logging
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-pmon} -d ${POSTGRES_DB:-pmon}"]
interval: 10s
timeout: 5s
retries: 5
# ---------- 幂等迁移:每次 up 都跑一次,跑完即退出 ----------
#
# 为什么必须有它(2026-09-15 运维坑):`init.sql` 挂在 /docker-entrypoint-initdb.d/,
# **只在数据目录为空时执行** —— 已有库上,写在里面的迁移语句永远不会跑。
# 于是"更新代码但忘了手工迁移"会变成**静默的链路中断**:
# 例如新增 devices.hardware_id 后,registerDevice 写一个不存在的列 → SQL 报错 →
# 网关 catch 掉只记日志 → 设备不再落库,而 /health 全绿。
#
# 一次性服务(跑完退出、不 restart)比"让人记得敲 SQL"可靠:
# 它把"迁移"变成 `docker compose up -d` 的必然副作用。
# migrate.sql 里每条语句都幂等,故反复执行无副作用。
migrate:
image: ${POSTGRES_IMAGE:-timescale/timescaledb:2.17.2-pg16}
depends_on:
postgres:
condition: service_healthy
environment:
PGPASSWORD: ${POSTGRES_PASSWORD:-pmon_dev_only}
# ON_ERROR_STOP=1:迁移失败必须**非零退出**,不能"打个警告继续跑",
# 否则又是"看起来部署成功、功能全废"
command: >
psql -v ON_ERROR_STOP=1 -h postgres -U ${POSTGRES_USER:-pmon}
-d ${POSTGRES_DB:-pmon} -f /migrate.sql
volumes:
- ./migrate.sql:/migrate.sql:ro
restart: "no"
logging: *logging
volumes:
redis_data:
pg_data:
nginx_logs: