多部署与目录服务
本文是 1.x 的归档文档(冻结于 1.x)。该分支只接收勘误修正, 内容基本不再变动;文中的源码路径、配置项与版本号均以 1.x 为准,不建议用于新项目。
新项目请查看 5.x 最新文档。已在 1.x 上运行的项目,本文仍是可靠依据。
同一个产品交付给多个客户时,第一个冒出来的问题是:一次上架,要不要在每台部署上再上架一遍?
不用。目录(应用 / 版本 / 分类)可以只维护一份,其它部署远程读它。
这一页讲怎么把这样的多套部署真的装起来:选哪条拓扑、装哪个 1Panel 包、两个端口怎么接、共享令牌怎么产生和分发、多套部署同处一台机器时要错开什么、链路怎么验。
运行期原理(IAppCatalogService 的两种实现怎么二选一、edition 分支、令牌拦在哪一层)在 /advanced/catalog-and-edition;配置项逐条说明与风险等级在 /deploy/configuration。本页不重复这两页。
三种拓扑怎么选
| 拓扑 | 装什么 | 约束与代价 | 适用 |
|---|---|---|---|
| 单机自用 | 一个 wujies-platform-server 包。catalog.mode=local 就是默认值,什么都不用配 | 目录只有这一份,改动只影响本机 | 自己用、单客户 |
| 目录服务 + 多个安装方 | 一台装 server 当目录服务;其余每台装 client | 目录服务是安装期依赖(见下文),且它的后端接口端口要对安装方可达 | 多客户,统一上架 |
| 用户版交付 | 客户那台装 wujies-platform-client 包 | 客户没有上架端;必须能连上目录服务才能装应用 | 交付给客户,不让客户上架 |
拓扑可以叠加:一台官方版可以同时给多个用户版供目录。
MarketEditionStartupCheck 只在 edition=client 时校验形态与目录是否自洽 ——
official + catalog.mode=remote 是能启动的。但那样它的上架端写的是本地库,
而安装侧读的是远端目录:上架会"成功",却对谁都不生效。
这正是 client 这个形态要避免的失败,别用配置绕开它。
数据分家:什么共享、什么各自一份
| 类别 | 表 | 位置 |
|---|---|---|
| 目录 | app_info / app_version / app_category / app_plan | 中心一份,由目录服务维护 |
| 安装实例 | app_install / app_install_task / app_version_menu | 每套部署各一份 |
| 迁移账本 | app_schema_history | 每套部署各一份 |
所以「装了什么、装的是哪个版本」是每套部署自己的决定:同一份目录下,A 部署可以装 exam 1.0.1,B 部署一个都不装。
这几张目录表本来就在 security.tenant.excludes 里(平台级数据、不按租户隔离),集中它们不需要带租户参数 —— 这是整件事能干净做掉的原因。表间关系见 /design/data-model。
还要记住一层部署内全局的东西:sys_menu 的菜单行、数据表结构、以及本机的产物(插件 jar 与前端产物目录)。它们在一个部署内跨租户共享,所以卸载应用时有「别的租户还在用就不动」那道闸。
用 1Panel 装两套(推荐路径)
两个包是同一份代码的两个形态,包名与商店显示名如下:
| 包目录 | 商店显示名 | 角色 |
|---|---|---|
wujies-platform-server | Wujies App Market (Server) | 官方版:目录在自己库 + 上架端 + 对外提供 /app/catalog/** |
wujies-platform-client | Wujies App Market (Client) | 用户版:目录读服务端,本机安装 / 升级 / 卸载,无上架端 |
服务端(目录服务)
compose 里写死了 APP_MARKET_EDITION=official 与 APP_MARKET_CATALOG_MODE=local。安装表单里与本页相关的是两栏:
表单字段(data.yml 的 envKey) | 怎么填 |
|---|---|
目录服务令牌(CATALOG_TOKEN) | 面板会预填一个随机值。换成更长的:openssl rand -hex 24。然后抄给每一个安装方 |
后端接口端口(PANEL_APP_PORT_API,默认 18080) | 必须对安装方可达,客户端的 base-url 就指向它。容器内固定 8080 |
客户端(用户版)
compose 里写死了 APP_MARKET_EDITION=client 与 APP_MARKET_CATALOG_MODE=remote。表单里多出两栏,缺一不可:
| 表单字段 | 怎么填 |
|---|---|
官方版地址(服务端)(CATALOG_BASE_URL) | http://<服务端主机>:18080。只写到主机与端口,/app/catalog 这个路径前缀由 CatalogApiContract.BASE_PATH 在代码里拼 |
目录服务令牌(CATALOG_TOKEN) | 必填且默认为空 —— 必须与服务端完全一致,面板不会替你填 |
客户端包没有后端接口端口那一栏:它的后端不需要对公。
打包脚本对两个包一视同仁,所以客户端首次启动同样会导入 sql/files/03-app-catalog.sql。
但远程模式下 IAppCatalogService 装配的是 RemoteAppCatalogService,这些本地目录行没有任何人读。
别据此判断"客户端有本地目录可用"。
上传与安装
# 1) 上传并解包(tar 解包,别用 scp 直接建目录 —— Windows 的 scp 建出 0700,容器里的非 root 用户读不到)
scp wujies-platform-1panel-5.6.3.tar.gz root@<host>:/root/
cd /root && tar -xzf wujies-platform-1panel-5.6.3.tar.gz -C /root/pkg
# 2) 放进面板的本地应用目录
cp -r /root/pkg/apps/* /opt/1panel/resource/apps/local/
chmod -R a+rX /opt/1panel/resource/apps/local/wujies-platform-*
# 3) 构建镜像(主包要两个;调度中心 / 监控中心是可选的另外两个)
cd /root/pkg/docker/backend && docker build -t wujies-platform/backend:5.6.3 .
cd ../frontend && docker build -t wujies-platform/frontend:5.6.3 .然后 面板 → 应用商店 → 本地应用 → 同步/刷新,两个应用都会出现。装完之后面板日志里第一行就是库名(wujies_<6位随机>)。
包本身的构建(build-1panel.ps1 怎么跑、镜像与 SQL 的关系、四条纪律)见 /deploy/backend;前端 dist 与 nginx 的 /remote-apps/ 对齐见 /deploy/frontend。
同一份配置的两种写法
1Panel 包用环境变量覆盖 jar 内的 yml(entrypoint.sh 做映射),手工跑 jar 或自己写 compose 时直接写 yml 等价:
# 目录服务侧(提供方)
app-market:
edition: official # 默认值
catalog:
mode: local # 默认值:目录就在自己库里
token: <两端一致的共享令牌># 安装方侧(使用方)
app-market:
edition: client
catalog:
mode: remote
base-url: http://<服务端主机>:18080
token: <与服务端一致>
connect-timeout-ms: 3000
read-timeout-ms: 15000环境变量名与表单字段的对应关系(CATALOG_BASE_URL → APP_MARKET_CATALOG_BASE_URL、
CATALOG_TOKEN → APP_MARKET_CATALOG_TOKEN、SECURITY_MODE → APP_MARKET_SECURITY_MODE)见包内 data.yml 说明与 /reference/config。
共享令牌:怎么产生、怎么分发
| 行为 | 说明 |
|---|---|
| 产生 | 在服务端那台机器上执行 openssl rand -hex 24,填进服务端表单的「目录服务令牌」 |
| 分发 | 把同一个值填进每一个安装方的「目录服务令牌」栏,一处都不能错 |
| 服务端留空 | 不校验调用方 —— 内网自用可以,公网必须配 |
| 不一致的表现 | 安装方的「应用市场」列表请求 500,服务端返回 {"code":500,"msg":"目录接口令牌校验失败"} |
| 改法 | 两边都去「应用商店 → 已安装 → 参数」改成一致;面板会重建容器 |
它是明文放在请求头(X-Catalog-Token)里的,只防"不知道地址的扫描器与误连",不防抓包。
接口只暴露已发布应用的目录元数据(应用名、版本、包地址),不含任何业务数据。 真正的防护是:别把服务端的后端接口端口暴露到公网。
所以:令牌用长随机串;服务端的 PANEL_APP_PORT_API 只对安装方所在的网段开放。
一套目录、多套部署的日常
- 上架一次就够:在服务端「应用上架」上传并发布版本,所有安装方的市场页立刻能看到(目录读取刻意不做缓存)。
- 升级不会自动传下去:目录只是"可选的版本清单",装不装、什么时候升,由每套部署自己在任务中心里做。见 /deploy/upgrade。
- 目录服务是安装期依赖,不是运行期依赖:已落位的插件 jar 与前端产物都在各自部署的磁盘上,服务端下线时它们照常运行。受影响的是「应用市场」页面与安装 / 升级这类动作。
- 签名只做一次:发布方用同一把私钥签包,所有部署配同一把公钥(
app-market.security.public-key)。各部署的security.mode可以不同。见 /package/signing 与 /package/security-modes。
因为首次安装还没有公钥,enforce 会拒绝所有包(包括你自己签的)。
配好公钥、跑过 GET /app/console/signature-readiness 之后再改成 enforce。
多套部署同处一台机器 / 同一网段
1Panel 的安装表单已经把该随机的都随机了(库名、账号、密码都是 wujies_<6位随机>,重复安装不撞名)。剩下这些要自己盯:
| 项 | 在哪 | |
|---|---|---|
| 必须一致 | 两端 CATALOG_TOKEN | 服务端与每个安装方的「目录服务令牌」 |
| 必须一致 | 客户端的 CATALOG_BASE_URL 必须指向服务端实际暴露的那个后端端口 | 客户端「官方版地址(服务端)」 |
| 必须不同 | 数据库名 / 账号 / 密码 | 面板随机生成,别改成同一个 |
| 必须不同 | 前端访问端口、后端接口端口 | 安装表单;同机装两套必然要改 |
| 必须不同 | 外部访问地址(EXTERNAL_URL) | 登录回调等对外链接用它 |
| 必须不同 | 共用同一个调度中心时的命名空间(SNAIL_JOB_NAMESPACE,默认 prod) | 同一个命名空间 + 任务组会让多套部署互相抢任务 |
| 各自一份 | 对象存储配置、sys_oss_config | 各部署在自己的「系统配置 → 文件存储配置」里填 |
验证链路:看对端日志,不要靠自己 curl
不要一边用 curl 手工打对端 —— 否则对端访问日志里那几条 GET /app/catalog/... 是你自己打的,
你会把"我打成功了"当成"调用方链路通了"的证据。这个坑真实踩过。
正确的做法:去目录服务的访问日志里找安装方打来的请求。
# 在目录服务那台机器上
cd /opt/1panel/apps/local/<应用>/<实例名>
docker compose logs --tail=200 backend | grep '/app/catalog/'应该能看到这些调用(路径常量定义在 CatalogApiContract):
GET /app/catalog/count-on-shelf
GET /app/catalog/app?id=...
GET /app/catalog/version?id=...
GET /app/catalog/latest-version?appId=...
GET /app/catalog/find-app?code=...
GET /app/catalog/market-search?keyword=图床
GET /app/catalog/released-versions?appId=...中文关键词搜索必须能返回结果 —— 如果英文正常、中文搜不到,是 URI 二次编码的问题, 推导见 /advanced/catalog-and-edition。
在安装方本机检查形态与前端(这部分打的是自己,不违反上面的纪律):
# 客户端应为 {"edition":"client","consoleVisible":false}
curl -s http://127.0.0.1:<前端端口>/prod-api/app/edition
# 上架端接口应为 403,且文案点明是 edition=client 导致的
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:<前端端口>/prod-api/app/console/list/prod-api 是包内 nginx 反代后端的前缀(见 deploy/docker/frontend/nginx.conf)。
两个形态都会响应 /app/catalog/**(控制器上没有任何 edition 条件),客户端那台读的是它自己的本地库。
所以「用户版不提供目录接口」不是一条可以依赖的边界 ——
真正的边界是上架端的 403 过滤器,以及不把哪些端口暴露出去。
超时是 connect-timeout-ms: 3000 / read-timeout-ms: 15000:目录读取不该拖垮安装流程,宁可快速失败。
报错与排查速查表见 /ops/troubleshooting。
相关阅读
- /advanced/catalog-and-edition —— 目录中心化与版本形态的运行期原理
- /deploy/configuration ——
app-market:全量配置项与风险等级 - /deploy/backend —— 1Panel 包的构建、上传与安装细节
- /deploy/ —— 部署总览:三种部署方式与上线前检查清单