# JNPF 全栈一键启动 = 微服务 + 基础设施(Redis/PG)+ 前端 cx-web,本地/服务器同一份编排 # (RocketMQ 已于 2026-08-10 移除,事件总线改 Redis pub/sub,见补丁 #10) # # 地址拓扑三层规则(详见 docs/docker-compose-guide.md): # 配置/发现(补丁 #8 起):镜像烧入 config/shared 静态配置 + SimpleDiscoveryClient 静态实例列表, # 无独立注册/配置中心容器;Dubbo LogProvider 改 registry-less 直连 dubbo://cx-platform:20880(原 cx-system,2026-08-11 聚合) # 数据面(业务→PG/Redis):本地与交付一致=服务名直连 # cx-postgres:5432 等(release-checklist §0.10) # 对外宣告(ApiDomain):cx-infra(浏览器+后端双消费方;本地 hosts + 容器 extra_hosts 双侧解析) # # 前提: # 1. 宿主 /etc/hosts 含「127.0.0.1 cx-infra」(本机代理软件须加 cx-infra 直连规则,L026) # 2. 构建前先打包:mvn clean package -DskipTests # 3. 库在哪由 .env 的 CUSTOMER_DB_HOST 决定,两种形态: # 远端客户库(开发机/测试环境,CUSTOMER_DB_HOST=YOUR_DB_HOST):本机不起 PG,什么都不用准备 # 栈内 PG(交付/正式,CUSTOMER_DB_HOST=cx-postgres):.env 设 COMPOSE_PROFILES=localdb, # 且全新机器先 docker volume create postgres_pgdata 并灌数据(见 docker/postgres/migration/) # 4. 宿主上不能有同端口进程(旧 nohup 服务 / Homebrew redis 均须停) # # 基础设施镜像统一 linux/amd64 + 华为云 SWR 源(swr.cn-north-4.myhuaweicloud.com/ddn-k8s/...): # 本地 Apple Silicon 经 Rosetta 跑,x86 服务器原生;一份 compose 两侧零差异 # # ⚠️ 第三方基础设施镜像在本文件里用**短名**(cx-redis 等), # 它们是 `make pull-infra` 从上述 SWR 长名 retag 出来的本地别名(docker tag 只挂别名、 # 不复制数据)。**因此裸 `docker compose pull` 拉不到它们**——本地没有这些 tag 时 # Docker 会去 docker.io 找不存在的 library/cx-redis。起栈一律走 `make up` # (其 images 目标已含 pull-infra)或离线包的 `make load`。preflight 会在 up 前拦截缺失。 # # 端口策略(Backlog A 安全加固,2026-07-17;Backlog F 增 80): # 本文件=交付基线,发布 cx-web 80(页面入口)+ gateway 30000(API/ApiDomain 直连口); # 其余端口(PG/Redis/引擎/业务调试口)在 docker-compose.override.yml(本地开发用,compose 自动合并,交付包不含)。 # 交付环境数据面服务名直连(见 docs/release-checklist.md §0),不依赖 hairpin 发布端口。 # # 凭据策略:密码/密钥一律经 .env 注入(${VAR:?} 必填,缺失时 compose 直接报错),仓库只留 .env.example 占位。 # # 用法: # cp .env.example .env # 首次;本地默认值可直接用,交付必须改值 # docker compose build && docker compose up -d # 冷启动自动串行:PG → 业务 # docker compose --profile extra up -d # 附带 scheduletask(app 已并入 cx-platform) name: cx # 公共环境变量(单独成锚点,便于某服务只覆盖个别变量而不丢其余: # environment: # <<: *cx-env # JAVA_TOOL_OPTIONS: -Xms512m -Xmx1536m # ) x-cx-env: &cx-env # 官方 Dockerfile ENTRYPOINT 无 $JAVA_OPTS 透传,堆参数走 JVM 自动拾取的 JAVA_TOOL_OPTIONS # stdout/stderr.encoding(2026-08-11):JDK 19+ 起 System.out 的编码由 stdout.encoding 决定, # 与 Dockerfile 里已有的 -Dfile.encoding=utf8 **无关**;rocky 基础镜像没设 locale # (native encoding = ANSI_X3.4-1968),于是中文 println 逐字变 `?`——gateway 的 # 「网关启动成功」在日志里就是那行 `??????`。走 JAVA_TOOL_OPTIONS 而不是改 Dockerfile, # 是为了零重建、且一处覆盖全部服务。 JAVA_TOOL_OPTIONS: -Xms256m -Xmx768m -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 # Spring Boot 的 ASCII banner:容器日志里没人看,白占 8 行。 # 🔴 引号不能省:YAML 1.1 里裸 off 会被解析成布尔 false,compose 拒绝布尔型 environment 值。 SPRING_MAIN_BANNER_MODE: "off" # 日志落盘根目录(2026-08-11):config/shared/logger.yaml 的 log.path 占位符消费, # 与下方 x-cx 的 volumes 那行**成对**——改一个必须改另一个,否则日志又回到容器可写层。 # 各服务自动写 /data/jnpf-logs// 子目录,故一个挂载点够全部服务用。 JNPF_LOG_DIR: /data/jnpf-logs # resources.yaml: storage-path ${JNPF_FILE_DIR:/Users/Shared/}(容器内路径), # 文件落 <根>/jnpf-resources/ JNPF_FILE_DIR: /data/jnpf-files/ # 补丁 #8:域名四键(system-config.yaml 占位符消费;默认值即本机开发形态,交付在 .env 设真值) JNPF_API_DOMAIN: ${JNPF_API_DOMAIN:-http://cx-infra:30000} JNPF_FRONT_DOMAIN: ${JNPF_FRONT_DOMAIN:-http://localhost/} JNPF_APP_DOMAIN: ${JNPF_APP_DOMAIN:-http://localhost/} JNPF_FLOW_DOMAIN: ${JNPF_FLOW_DOMAIN:-http://cx-flow-engine:31000} # 🔴 Dubbo LogProvider 消费端直连地址(2026-08-11 平台聚合必须加)。 # 消费方是 jnpf-common-springaop 的 RequestLogAspect / ResultException,它们的 # @DubboReference 默认值**硬编码为 dubbo://cx-system:20880**——而 cx-system 已并入 # cx-platform、容器不复存在,不覆盖就每分钟刷 UnknownHostException: cx-system。 # 宿主栈(start-all.sh)设的是 dubbo://127.0.0.1:20880,不含服务名,故宿主形态**测不出** # 这个问题;它只在容器形态暴露。改服务名时这里必须跟着改。 # 遗留优化项:聚合后 provider 与 consumer 同处一个 JVM,本可用 injvm 走进程内直调, # 省掉这一跳本地 TCP 与重连定时器;但那要改平台代码的 injvm=false,另案评估。 JNPF_LOG_PROVIDER_URL: ${JNPF_LOG_PROVIDER_URL:-dubbo://cx-platform:20880} # 客户坐标(adr-multi-customer-db-isolation §3):业务库经 config/shared/datasource.yaml 的 # ${CUSTOMER_DB_HOST}/${CUSTOMER_DB_PORT} 占位符消费,故必须透传进业务容器。 # 与下方 cx-flow-engine 的 FLOW_DB_URL 同源——两库要么一起切要么一起不切。 CUSTOMER_DB_HOST: ${CUSTOMER_DB_HOST:-cx-postgres} CUSTOMER_DB_PORT: ${CUSTOMER_DB_PORT:-5432} # 业务库账号也透传:config/shared/datasource.yaml 的 username/password 同样是占位符, # 否则每切一个客户就得改这份文件的默认值,「单一开关」就名存实亡了。 JNPF_APP_DB_USER: ${JNPF_APP_DB_USER:-jnpf_app} JNPF_APP_DB_PASSWORD: ${JNPF_APP_DB_PASSWORD:?缺 JNPF_APP_DB_PASSWORD,见 .env.example} # Redis 密码透传给业务服务(datasource.yaml 的 spring.redis.password 占位符消费)。 # 此前业务侧读的是 yaml 明文字面值、仅 cx-redis 容器自己吃这个 env——两边靠碰巧同值工作; # 参数化后交付环境改 .env 一处即可轮换(2026-08-06 用户指出后收口)。 JNPF_REDIS_PASSWORD: ${JNPF_REDIS_PASSWORD:?缺 JNPF_REDIS_PASSWORD,见 .env.example} # Redis 坐标透传(datasource.yaml 的 spring.redis.host/port 占位符消费)。 # JNPF_DOCKER_REDIS_* 是容器侧专用覆盖:宿主 JVM 可继续使用 127.0.0.1,容器则通过 # cx-infra(host-gateway) 访问宿主 Redis。未设置专用覆盖时沿用 JNPF_REDIS_*,兼容远端 Redis。 # 两组都不设 = 栈内 cx-redis(交付/本机容器形态,见 .env.example §5)。 # 🔴 必须写 ${VAR:-默认值} 而非裸 ${VAR}:宿主未设时裸形式注入空串,会把 yaml 里的默认值静默清空。 JNPF_REDIS_HOST: ${JNPF_DOCKER_REDIS_HOST:-${JNPF_REDIS_HOST:-cx-redis}} JNPF_REDIS_PORT: ${JNPF_DOCKER_REDIS_PORT:-${JNPF_REDIS_PORT:-6379}} x-cx: &cx # 业务镜像当前以 /bin/sh -c 启动 Java;用 init 转发停止信号并回收子进程, # 避免 compose 重建时 PID 变成 zombie 后无法停止旧容器。 init: true restart: unless-stopped networks: [cx] extra_hosts: - "cx-infra:host-gateway" environment: *cx-env # 冷启动排队(C4 补齐强依赖;补丁 #8 起业务改显式依赖 PG): # PG healthy 才放业务(C2 深化后=业务/流程引擎库真就绪,L022 重连空转窗口同样适用); # Redis=会话/缓存**兼事件总线**(2026-08-10 起,见 config/shared/system-config.yaml), # 起在依赖前只会刷错重连。 # MQ namesrv 已移除:RocketMQ 于 2026-08-10 随事件总线切 Redis 一并退栈。 depends_on: cx-postgres: condition: service_healthy # 连远端客户库时本机不起 PG(cx-postgres 在 localdb profile 内,见其服务块注释)。 # required:false = 该服务不在本次启动集合里就跳过、且不自动把它的 profile 拉进来; # 在集合里(COMPOSE_PROFILES=localdb)则照旧等它 healthy——两种形态共用这一份编排。 required: false cx-redis: condition: service_healthy volumes: # 与宿主进程共享同一文件存储根(混合调试两侧看到同一批文件); # 跨平台:宿主侧根目录用 JNPF_DATA_DIR 参数化,Win/Linux 在仓库根 .env 里 # 设 JNPF_DATA_DIR=D:/jnpf-data 或 /data/jnpf 即可,本机 Mac 默认 /Users/Shared - ${JNPF_DATA_DIR:-/Users/Shared}/jnpf-resources:/data/jnpf-files/jnpf-resources # 应用日志落宿主(2026-08-11):与 x-cx-env 的 JNPF_LOG_DIR 成对。 # 此前日志写在容器可写层,容器 recreate 即全丢、且不占可见磁盘配额。 # 宿主侧长成 logs/<服务名>/log_{error,warn,info,debug,total}.log,直接 tail -f 即可; # 滚动归档(按天+满 10MB,保留 7 天)进 <服务名>/{error,info,warn,total}/YYYY-MM-DD/,同样在宿主上。 # 🔴 冒号左边 = 宿主机目录(改这个),右边 = 容器内目录(由 JNPF_LOG_DIR 固定,别改)。 # 交付现场若不想写进部署目录,在 .env 设 JNPF_LOG_DIR_HOST=/var/log/jnpf 这类宿主绝对路径。 # ⚠️ Linux 上该目录由容器内 root 创建,非 root 用户读日志需要 sudo。 - ${JNPF_LOG_DIR_HOST:-./logs}:/data/jnpf-logs logging: # 容器 stdout 的 json-file 轮转上限;查看用 docker compose logs -f # (注意这份和上面的文件日志是两套:stdout 走 json-file 存 /var/lib/docker, # 容器删除即消失;要留存的日志看挂出来的 logs/ 目录) options: max-size: "50m" max-file: "3" # 基础设施公共模板(无业务 env、无文件存储卷) x-infra: &infra restart: unless-stopped networks: [cx] extra_hosts: - "cx-infra:host-gateway" logging: options: max-size: "50m" max-file: "3" services: # ================= 基础设施(M2 收编;调试端口见 docker-compose.override.yml) ================= cx-postgres: <<: *infra container_name: cx-postgres # 「栈内 PG」只是两种库形态之一,故收进 profile,默认不启动(2026-08-07): # 开发机 / 测试环境:CUSTOMER_DB_HOST 指向 CVM 上的客户实例,本机这个 PG 是多余的 # (起它还会因 external 卷 postgres_pgdata 不存在而直接把整栈拦下) # 交付 / 正式:本机 PG 就是唯一库,须在 .env 设 COMPOSE_PROFILES=localdb 才会起 # 谁该设这个开关有唯一判据:CUSTOMER_DB_HOST=cx-postgres 就必须设,preflight 会核对。 # 构建与离线打包不受 profile 影响(Makefile images / assemble-offline-package.sh 显式带上), # 否则交付包会缺 PG 镜像。 profiles: ["localdb"] # 定义自 docker/postgres/docker-compose.yml 平移(该文件已废弃);镜像本地已构建,交付走 tar image: cx/postgres:18.4 build: docker/postgres platform: linux/amd64 shm_size: 1g # 端口不在基线发布(宿主 psql 走 override 的 127.0.0.1:5433,或 docker exec cx-postgres psql) environment: TZ: Asia/Shanghai POSTGRES_USER: postgres # 仅 initdb 时生效;既有 pgdata 卷改密码须 ALTER USER(见 release-checklist §0) POSTGRES_PASSWORD: ${JNPF_PG_PASSWORD:?缺 JNPF_PG_PASSWORD,先 cp .env.example .env} # PG18 官方镜像 PGDATA 迁至 /var/lib/postgresql/18/docker,volume 挂父目录 POSTGRES_INITDB_ARGS: "--locale=C.UTF-8 --encoding=UTF8" volumes: - pgdata:/var/lib/postgresql command: - postgres - -c - shared_preload_libraries=pg_stat_statements,auto_explain - -c - pg_stat_statements.track=all - -c - auto_explain.log_min_duration=1s - -c - timezone=Asia/Shanghai # 9 个业务服务 Druid 池稳态 ~98 连接,默认 100 在真实页面流量下打满(2026-07-17 回归实测) - -c - max_connections=200 healthcheck: # C2 深化:pg_isready 只验"接受连接",空卷/漏灌的 PG 照样 healthy(全新机器静默失败根源)。 # 追加两库连得上 + 关键表在:坏库不放行,下游(业务/流程引擎 service_healthy)等真就绪。 # 实现要点:连上目标库本身即证明"库存在且接受连接"(pg_isready 冗余,省掉); # jnpf_xxljob 无关键表,用 1/count 除零技巧并入首个连接;共 2 次 psql——冷启动 13 个 JVM # 打满 CPU 时(本地 Rosetta)每个进程 spawn 都慢,多进程版实测超 5s timeout 误判 unhealthy。 # 只读校验走容器内 socket=trust,与密码无关(L030 不适用)。 # 补丁 #8:去掉第三库探活——原注册/配置中心的库仍在 PG 里(未删,见 # tasks/jnpf-platform-patches.md 补丁 #8),但栈内无服务再消费它,健康检查不必再等它。 test: - CMD-SHELL - >- psql -qU postgres -d jnpf_init -v ON_ERROR_STOP=1 -c 'SELECT 1 FROM base_user LIMIT 0' -c "SELECT 1/count(*) FROM pg_database WHERE datname='jnpf_xxljob'" >/dev/null && psql -qU postgres -d jnpf_flow -v ON_ERROR_STOP=1 -c 'SELECT 1 FROM act_ru_task LIMIT 0' >/dev/null interval: 15s # 冷启动 CPU 饱和窗口进程 spawn 慢(上);稳态 <1s,20s 只是兜底 timeout: 20s retries: 5 # 大卷 crash recovery 可能远超 5×15s,放宽到 120s:start_period 内失败不计 retries、 # 一旦成功立即 healthy(不拖慢正常冷启动),只延后"真坏"的 unhealthy 判定 start_period: 120s cx-redis: <<: *infra container_name: cx-redis image: cx-redis:8.4.0-alpine platform: linux/amd64 # requirepass 经 .env 注入(config/shared/datasource.yaml 的 spring.redis.password 须同值)。 # # 🔴 2026-08-10 起开持久化(此前是 `--save ""` 不落盘不挂卷)。 # 原来的理由是「纯缓存/会话,数据可丢,反正 jnpf-oauth 每次启动会清全库」—— # **那个理由不成立**:JnpfListener 的清库被 `"false".equals(getTestVersion())` 门控, # 而 test-version 全仓库未配置 → null → 条件为 false → 清库从不执行。 # 实测(CVM 客户A):唯一无 TTL 的 key 是 `idgenerator_Index:`(ID 生成器索引,值已到 174), # 丢了会让雪花 workerId 复用 → 主键冲突/跳号。详见 docs/release-checklist.md §2.2。 # 注意:事件总线走 pub/sub,**不是**开持久化的理由(在途消息落盘也救不回)。 command: - redis-server - --protected-mode - "yes" - --requirepass - ${JNPF_REDIS_PASSWORD:?缺 JNPF_REDIS_PASSWORD,先 cp .env.example .env} # AOF 为主(everysec:最多丢 1 秒),RDB 兜底便于整卷搬迁 - --appendonly - "yes" - --appendfsync - everysec - --save - "900 1" # 🔴 交付基线此前**既无 maxmemory 也无 mem_limit**——Redis 可以吃光整台机器。 # 512mb 是给中小客户的保守起步值;正式环境应按实测定,方法见 # docs/release-checklist.md §2.2「容量与告警」。 # 不变式:若给容器加 mem_limit,必须 ≥ 2 × maxmemory + 64M(AOF rewrite 的 COW 峰值)。 - --maxmemory - 512mb # 保持 noeviction:会话/缓存以外还有无 TTL 的 idgenerator_Index:, # LRU 静默逐出会变成跳号/主键冲突这类诡异故障;打满报错反而立刻暴露问题。 # 代价是打满 = 写入全失败,所以**必须配 70% 告警**,别等撞墙。 - --maxmemory-policy - noeviction volumes: - redisdata:/data environment: # redis-cli 自动读取,healthcheck 免显式 -a(避免密码出现在进程参数) REDISCLI_AUTH: ${JNPF_REDIS_PASSWORD} healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 # ── RocketMQ(cx-mq-namesrv / cx-mq-store-init / cx-mq-broker)已于 2026-08-10 整体移除 ── # 事件总线改用 Redis pub/sub(config/shared/system-config.yaml 的 event.event-publish-type: redis), # binder 依赖也已从 jnpf-common-springaop 排除。要回滚见 tasks/jnpf-platform-patches.md 补丁 #10。 cx-flow-engine: <<: *infra container_name: cx-flow-engine # 独立流程引擎(官方 Flowable 7.0.1 + JNPF 定制层,自有源码工程 jnpf-flow-engine/,详见 docker/flow-engine/README.md); # cx-flowable(30004) 经 FlowDomain=http://cx-flow-engine:31000 服务名直连调用(§0.10 数据面规则)。 # 构建前置:mvn -f jnpf-flow-engine/pom.xml package + ./docker/flow-engine/prepare-context.sh(jar 不入 git) image: cx/flow-engine:${CX_VERSION:-1.0.0} build: docker/flow-engine platform: linux/amd64 # Dockerfile ENTRYPOINT 是 sh -c 形式(PID 1 不收割子进程),曾出僵尸进程导致容器停不掉(L031) init: true # 数据源 = PG 的 jnpf_flow 库(随 pgdata 卷,Flowable 7 同版本 schema); # 连库账号经 env 注入(conf/application-dev.yml 只留占位),A4 起用专用角色 jnpf_flow_app environment: # URL 用 :- 带默认值:默认容器网直连; # 开发机连测试环境共享库时在 .env 覆盖(docs/adr-single-db-topology.md §5.2) # 引擎库与业务库共用同一对坐标变量 → 物理上不可能半切(adr-multi-customer-db-isolation §4)。 # JNPF_FLOW_DB_URL 保留为逃生口(整串覆盖),日常不要用——用它就重新引入了半切风险。 FLOW_DB_URL: ${JNPF_FLOW_DB_URL:-jdbc:postgresql://${CUSTOMER_DB_HOST:-cx-postgres}:${CUSTOMER_DB_PORT:-5432}/jnpf_flow} FLOW_DB_USER: ${JNPF_FLOW_DB_USER:-postgres} FLOW_DB_PASSWORD: ${JNPF_FLOW_DB_PASSWORD:?缺 JNPF_FLOW_DB_PASSWORD,先 cp .env.example .env} depends_on: cx-postgres: condition: service_healthy # 同 x-cx 的说明:栈内 PG 在 localdb profile 内,不在启动集合时跳过而非报错 required: false healthcheck: # 无 actuator,用 springdoc 文档端点探活 test: ["CMD-SHELL", "curl -fsS -o /dev/null http://127.0.0.1:31000/v3/api-docs"] interval: 15s timeout: 5s start_period: 90s retries: 5 cx-onlyoffice: <<: *infra container_name: cx-onlyoffice # OnlyOffice Document Server 社区版:Office 文档在线预览/编辑,由 cx-biz-common 的 # onlyoffice 能力模块驱动(方案见 tasks/onlyoffice-integration-plan.md)。 # 用 *infra 而非 *cx:它不是 JNPF JVM,不需要 JNPF_* 环境变量与 PG/Redis/MQ 依赖。 # # ⚠️ 版本必须固定:9.4.0.1 是 Docker Hub 当前 latest(Hub manifest list digest # sha256:3ab6ebc7c605e5a32b7ae3ff19daed4925090245acc8100ce2230bd766c88212); # tag 9.4.0 / 9.4 是更早的另一个 digest,不要混用。 # # 🔴 2026-08-19:由 Docker Hub 长名改为短名 cx-onlyoffice,与 cx-redis 同机制—— # 国内网络 `docker.io/onlyoffice/documentserver` 拉取超时(registry-1.docker.io:443 # 连不上,Windows 测试环境实测)。短名由 `make pull-infra` 从 SWR 长名 # $SWR/onlyoffice/documentserver:9.4.0.1 retag 而来,故裸 `docker compose pull` # 同样拉不到它(详见本文件顶部 §短名约定与 docs/docker-compose-guide.md §11.1)。 # SWR 侧是已解开的 amd64 单架构 manifest(digest # sha256:66e06b404e11d532832501acf5a78bc759940c58859a80676330f71aedb1ef99, # config sha256:4934c364adccc2898d0998951fb09d455496454de417cce2a74b7b1e808daab0), # 与上面 Hub 的 manifest-list digest 天然不同,**不能据此判断内容不一致**。 # ✅ 2026-08-19 已实证两源内容同一(digest 比不了:本机连不通 Hub,且本地 Hub tag # 在 containerd image store 下只剩 index 引用,inspect 出来 Architecture 空、 # RootFS.Layers=0,无从取层)。改在容器内比,两侧完全一致: # · dpkg 包版本 onlyoffice-documentserver 9.4.0-129(amd64) # · 核心二进制 server/DocService/docservice # sha256:556df4aab7b1a87123046a4459baee34e60f57deaf7bcd189bccd6f271bd8fcb # 并且 SWR 镜像单起容器 20s 内跑通官方 /healthcheck。 image: cx-onlyoffice:9.4.0.1 platform: linux/amd64 environment: # JWT 是 DS 与后端之间的唯一信任凭据:后端签发编辑器 config、DS 签发保存回调, # 两侧共用同一 secret。与 config/shared/jnpf-biz-common.yaml 的 # onlyoffice.jwt-secret 同源(都读 .env 的 ONLYOFFICE_JWT_SECRET)—— # 两边不一致的表现是编辑器一直转圈或回调静默失败,且日志不明显。 JWT_ENABLED: "true" JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:?缺 ONLYOFFICE_JWT_SECRET,先 cp .env.example .env} volumes: # DS 内置 PostgreSQL 存文档缓存与编辑状态,不挂卷则重启丢失(表现为编辑中的 # 文档回到上一个保存点)。字体不挂载——镜像自带 22 个中文字体已够用(实测)。 - onlyoffice-data:/var/www/onlyoffice/Data - onlyoffice-log:/var/log/onlyoffice - onlyoffice-db:/var/lib/postgresql healthcheck: # DS 官方探活端点,就绪时返回字面量 true test: ["CMD-SHELL", "curl -fsS http://127.0.0.1/healthcheck | grep -q true"] interval: 15s timeout: 10s retries: 5 # 首次启动要初始化内置 PG + 生成字体索引,Rosetta 上偏慢 start_period: 90s # ================= JNPF 业务服务(M1) ================= cx-gateway: <<: *cx container_name: cx-gateway image: cx/gateway:${CX_VERSION:-1.0.0} build: { context: ., dockerfile: dockerfile/gateway/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } ports: ["30000:30000"] # 说明:2026-08-11 一度在这里加过 -Dlogging.level.jnpf.util.Ip2RegionUtil=OFF 来压 # 「ip2region xdb 找不到」的两条 ERROR。补丁 #15 从根上解决后(gateway 自带同包替身类 # jnpf/util/Ip2RegionUtil.java,见该文件注释)已撤掉——压噪配置留着会掩盖真问题, # 也会让人误以为那两条错误还在。本服务现在完全走 x-cx-env 的公共环境变量。 healthcheck: &hc-30000 test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30000"] interval: 30s # 冷启动 CPU 饱和下业务真实启动 4-6 分钟,180s 会经历"瞬时 unhealthy 再自愈"的误导态 # (三轮冷启动实测);start_period 内失败不计 retries、成功立即 healthy,放宽无副作用(L034) start_period: 300s retries: 3 # ================= 平台聚合服务(2026-08-11)================= # system / permission / oauth / visualdev / visualdata / message / file / app / flowable # 九个模块合一个 JVM,取代下面九个已注释的 cx-* 容器。 # 实测:RSS 约 1.07GB(对比九个独立容器约 6.7GB),fat jar 328MB(对比九个各 198MB)。 # 服务名不变——config/shared/discovery.yaml 里九个 jnpf-* 名字全部别名到 cx-platform:30002, # 网关 router.yaml 与 60 个 @FeignClient 一行未改。 cx-platform: <<: *cx container_name: cx-platform image: cx/platform:${CX_VERSION:-1.0.0} build: { context: ., dockerfile: dockerfile/platform/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } environment: <<: *cx-env # 九个模块的类元数据同处一个 JVM,堆比单模块服务大一档(模板默认 -Xms256m -Xmx768m)。 # 实测堆 used 约 361MB,1536m 留足余量;即便如此总占用仍远低于九容器形态。 # ⚠️ 本行整体覆盖 x-cx-env 的同名值,编码参数必须跟着抄一份,否则本服务中文日志变问号。 JAVA_TOOL_OPTIONS: -Xms512m -Xmx1536m -Dstdout.encoding=UTF-8 -Dstderr.encoding=UTF-8 # 原 jnpf-system 是 Dubbo LogProvider 唯一 provider(补丁 #8),聚合后本服务接管。 # 不设此项 → 每个请求的 RequestLogAspect 都调用失败。 DUBBO_PROTOCOL_PORT: "20880" healthcheck: <<: *hc-30000 test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30002"] # ── cx-oauth 已并入 cx-platform(2026-08-11),整段注释保留以便回滚 ── # cx-oauth: # <<: *cx # container_name: cx-oauth # image: cx/oauth:${CX_VERSION:-1.0.0} # build: { context: ., dockerfile: dockerfile/oauth/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # healthcheck: # <<: *hc-30000 # test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30001"] # ── cx-system 已并入 cx-platform(2026-08-11),整段注释保留以便回滚 ── # cx-system: # <<: *cx # container_name: cx-system # image: cx/system:${CX_VERSION:-1.0.0} # build: { context: ., dockerfile: dockerfile/system/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # environment: # <<: *cx-env # # 补丁 #8:jnpf-system 是 Dubbo LogProvider 唯一 provider,固定协议端口供直连 # DUBBO_PROTOCOL_PORT: "20880" # healthcheck: # <<: *hc-30000 # test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30002"] # ── cx-visualdev 已并入 cx-platform(2026-08-11),整段注释保留以便回滚 ── # cx-visualdev: # <<: *cx # container_name: cx-visualdev # image: cx/visualdev:${CX_VERSION:-1.0.0} # build: { context: ., dockerfile: dockerfile/visualdev/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # healthcheck: # <<: *hc-30000 # test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30003"] # ── cx-flowable 已并入 cx-platform(2026-08-11),整段注释保留以便回滚 ── # cx-flowable: # <<: *cx # container_name: cx-flowable # image: cx/flowable:${CX_VERSION:-1.0.0} # build: { context: ., dockerfile: dockerfile/flowable/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # healthcheck: # <<: *hc-30000 # test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30004"] # ── cx-file 已并入 cx-platform(2026-08-11),整段注释保留以便回滚 ── # cx-file: # <<: *cx # container_name: cx-file # image: cx/file:${CX_VERSION:-1.0.0} # build: { context: ., dockerfile: dockerfile/file/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # healthcheck: # <<: *hc-30000 # test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30005"] # ── cx-message 已并入 cx-platform(2026-08-11),整段注释保留以便回滚 ── # cx-message: # <<: *cx # container_name: cx-message # image: cx/message:${CX_VERSION:-1.0.0} # build: { context: ., dockerfile: dockerfile/message/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # healthcheck: # <<: *hc-30000 # test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30008"] # ── cx-permission 已并入 cx-platform(2026-08-11),整段注释保留以便回滚 ── # cx-permission: # <<: *cx # container_name: cx-permission # image: cx/permission:${CX_VERSION:-1.0.0} # build: { context: ., dockerfile: dockerfile/permission/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # healthcheck: # <<: *hc-30000 # test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30010"] cx-lims: <<: *cx container_name: cx-lims image: cx/lims:${CX_VERSION:-1.0.0} build: { context: ., dockerfile: dockerfile/lims/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } environment: <<: *cx-env # Spring relaxed binding → audit.spool-dir(AuditSdkAutoConfiguration @Value("${audit.spool-dir:./audit-spool}"))。 # cx-lims 是当前**唯一**的 audit-sdk 宿主,也就是唯一需要 spool 兜底的服务。 AUDIT_SPOOL_DIR: /jnpf/audit-spool # M2 Task A1:audit-sdk 已接入 lims。relaxed binding 自动映射到 audit.enabled(总开关, # AuditSdkAutoConfiguration @ConditionalOnProperty),无需在 application.yml 重复声明占位符。 # 双态断言见 tasks/audit-m2-startup-check.sh。 AUDIT_ENABLED: ${AUDIT_ENABLED:-true} # 2026-08-11:AUDIT_LAYER1_ENABLED / AUDIT_LAYER1_PER_TABLE_MASKED_COLUMNS 随层 1 整套移除。 # 层 1 自交付起一直默认 false,audit_events 表内 source_layer=1 零行,属未启用即负债。 volumes: # ⚠️ 服务级 volumes 会**整体覆盖** x-cx 锚点的那份,不是追加——所以 jnpf-resources # 与日志挂载都得在这里再写一遍,漏一行该服务就静默退回容器内路径(2026-08-11 踩过)。 - ${JNPF_DATA_DIR:-/Users/Shared}/jnpf-resources:/data/jnpf-files/jnpf-resources - ${JNPF_LOG_DIR_HOST:-./logs}:/data/jnpf-logs - audit-spool-lims:/jnpf/audit-spool healthcheck: <<: *hc-30000 test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30019"] # eln 业务模块(ADR 售卖单元制):profile 隔离,默认 make up 不起(本地 VM 内存账,L024)。 # 起用:docker compose --profile eln up -d cx-eln: <<: *cx container_name: cx-eln image: cx/eln:${CX_VERSION:-1.0.0} profiles: ["eln"] build: { context: ., dockerfile: dockerfile/eln/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } healthcheck: <<: *hc-30000 test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30013"] # ══ cx-audit(30014)已于 2026-08-11 退役 ══ # 审计整体搬入 jnpf-biz-common(见下),JVM/容器/镜像各减一个。 # 服务名 jnpf-audit 在 config/shared/discovery.yaml 里保留为**别名**指向 cx-biz-common:30015, # 故 lims 的 Feign 投递与 /api/audit/** 路由一行未改。 # 原先这里的 AUDIT_SPOOL_DIR + audit-spool-audit 卷是**从未生效的配置**:spool 属 audit-sdk, # 而 audit 服务端的依赖链(server→controller→biz→api)从不含 sdk——退役前实测该卷内始终为空。 # 公共业务能力聚合服务(jnpf-biz-common):默认启动(无 profiles,区别于 cx-eln)。 # 一个 JVM 承载多个能力模块(onlyoffice、audit…),准入纪律见 specs/2026-08-07-biz-common-design.md §3 cx-biz-common: <<: *cx container_name: cx-biz-common image: cx/biz-common:${CX_VERSION:-1.0.0} build: { context: ., dockerfile: dockerfile/biz-common/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # 只有本服务消费这个密钥,故不进公共 *cx-env 锚点,单独覆盖(保留锚点其余变量)。 # 🔴 漏了它会静默失败而不是报错:config/shared/jnpf-biz-common.yaml 里写的是 # ${ONLYOFFICE_JWT_SECRET},而 Spring Boot 的 Binder 对解析不了的占位符是原样放行, # 后端会拿字面量 "${ONLYOFFICE_JWT_SECRET}" 当密钥签名,DS 验签必然失败, # 表现为编辑器里「文档安全令牌的格式不正确」——2026-08-08 实踩。 environment: <<: *cx-env ONLYOFFICE_JWT_SECRET: ${ONLYOFFICE_JWT_SECRET:?缺 ONLYOFFICE_JWT_SECRET,先 cp .env.example .env} healthcheck: <<: *hc-30000 test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30015"] cx-dms: <<: *cx container_name: cx-dms image: cx/dms:${CX_VERSION:-1.0.0} build: { context: ., dockerfile: dockerfile/dms/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } healthcheck: <<: *hc-30000 test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30016"] # ── cx-visualdata 已并入 cx-platform(2026-08-11),整段注释保留以便回滚 ── # cx-visualdata: # <<: *cx # container_name: cx-visualdata # image: cx/visualdata:${CX_VERSION:-1.0.0} # build: { context: ., dockerfile: dockerfile/visualdata/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # healthcheck: # <<: *hc-30000 # test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30011"] # ================= 前端(Backlog F)================= # nginx 静态托管 dist + /api、/websocket 反代 gateway;非 JVM 用 *infra 模板(x-cx 的 # JAVA_TOOL_OPTIONS/文件卷/depends_on 均不适用)。不加 depends_on:nginx.conf 用变量 # proxy_pass + resolver 惰性解析,gateway 未起时 nginx 照常启动、重建换 IP 自动重解析。 # dist 由 make web-dist 快照进 docker/frontend/dist/(独立 build context——根 .dockerignore # 是 deny-all 白名单,context: . 拿不到前端文件)。 cx-web: <<: *infra container_name: cx-web image: cx/web:${CX_VERSION:-1.0.0} build: context: docker/frontend args: NGINX_BASE_IMAGE: "${NGINX_BASE_IMAGE:-nginx:1.27-alpine}" ports: - "80:80" # - "443:443" # TLS 现场启用(证书 + nginx.conf 443 块,三步见交付包 INSTALL.md §5) volumes: # 挂载为准(现场免构建改反代/开 TLS),镜像内 COPY 的同份 conf 作自包含兜底 - ./docker/frontend/nginx.conf:/etc/nginx/conf.d/default.conf:ro # - ./docker/frontend/certs:/etc/nginx/certs:ro # TLS 证书挂载点 healthcheck: # alpine 无 bash 用不了 &hc-30000 的 /dev/tcp;busybox wget 探静态入口即可 test: ["CMD", "wget", "-qO", "/dev/null", "http://127.0.0.1/"] interval: 30s timeout: 5s retries: 3 start_period: 10s # ---- 以下 2 个本地暂未使用,按需 --profile extra 启动 ---- cx-scheduletask: <<: *cx profiles: [extra] container_name: cx-scheduletask image: cx/scheduletask:${CX_VERSION:-1.0.0} build: { context: ., dockerfile: dockerfile/scheduletask/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } healthcheck: <<: *hc-30000 test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30009"] # ── cx-app 已并入 cx-platform(2026-08-11),整段注释保留以便回滚 ── # cx-app: # <<: *cx # profiles: [extra] # container_name: cx-app # image: cx/app:${CX_VERSION:-1.0.0} # build: { context: ., dockerfile: dockerfile/app/Dockerfile, args: { BASE_IMAGE: "${BASE_IMAGE:-bellsoft/liberica-openjre-rocky:21}" } } # healthcheck: # <<: *hc-30000 # test: ["CMD-SHELL", "exec 3<>/dev/tcp/127.0.0.1/30012"] networks: cx: name: cx volumes: # Redis 持久化卷(AOF+RDB)。普通内部卷:`down -v` 会删, # 但 idgenerator_Index: 丢失只影响雪花 workerId 分配,重建即可,不是跨机迁移数据。 redisdata: # 复用 PG 迁移期创建的既有卷(原 docker/postgres 项目);external=compose 永不删除(含 down -v), # 全新机器需先 docker volume create postgres_pgdata 再灌数据(docker/postgres/migration/) pgdata: name: postgres_pgdata external: true # audit-sdk spool 兜底目录(两级降级的最后一级):per-service 独立卷(Codex 批次二 #2: # 多实例共享 spool 卷会有孤儿租约风险,per-service 隔离规避)。 # 2026-08-11:audit-spool-audit 随 cx-audit 退役一并删除(它从未被写入过——spool 属 sdk, # 而 audit 服务端不含 sdk,退役前实测卷内为空)。当前只有 SDK 宿主 cx-lims 需要。 audit-spool-lims: # OnlyOffice DS 状态:Data=文档缓存与转换产物,db=内置 PG(编辑会话状态),log=日志。 # 均为可重建数据(丢失只影响编辑中的未保存内容,已保存的文档在 jnpf-file 存储里), # 故用普通内部卷,不设 external onlyoffice-data: onlyoffice-log: onlyoffice-db: