<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 离线契约通过。
214 lines
9.4 KiB
YAML
214 lines
9.4 KiB
YAML
# 墨观 云端部署编排(单机起步,可平滑扩多实例)
|
||
# 组件: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:
|