# 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/<spring.application.name>/ 子目录,故一个挂载点够全部服务用。
|
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 <svc>
|
# (注意这份和上面的文件日志是两套: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:
|