Skip to content

运维手册 ​

本页面向已经跑起来的一套部署。当前的交付形态是 1Panel 应用包 + docker compose + 外部中间件,所以要运维的不是「一个 jar 进程」,而是「一个实例目录 + 两三个容器 + 一堆不在包里的服务」。本页只回答四件事:东西在宿主机的哪里、日志怎么看、每天该盯哪几项、哪些行为是设计如此。

你要解决的问题去哪一页
还没装 / 要重装 / 安装表单怎么填部署总览
装不上、起不来、按症状查原因故障排查
这次安装/升级/卸载做了什么、卡在哪一步任务中心与日志
升级平台、升级应用、存量库补脚本升级与补丁
备什么、密钥纪律、上线前脱敏备份与安全
运行期怎么盯、怎么重启、怎么清本页

三条纪律

  1. 应用生命周期的问题先看任务中心(「我的应用 → 应用任务」):步骤与日志是边跑边落库的,比翻服务端日志快。
  2. 不要 DROP DATABASE。 安装表单建的库只授了库级权限(SHOW GRANTS 只有一行 GRANT ALL PRIVILEGES ON ...),应用账号没有全局 CREATE,库删了就建不回来。要重来请清空表,做法见 升级与补丁。
  3. 应用装的菜单受保护:path / component / perms / menuType / parentId 的修改与删除都会被服务端拒绝(只放行名称 / 图标 / 排序 / 显隐)。想隐藏某个应用,用「停用」,不要删菜单。原理见 菜单归属与保护。

一、这台部署由什么组成 ​

主包只有两个容器,定义在 deploy/1panel/apps/wujies-platform-server/5.6.3/docker-compose.yml(用户版是 wujies-platform-client/5.6.3/docker-compose.yml):

compose 服务容器名端口做什么
frontend${CONTAINER_NAME}(= 1Panel 实例名,属主服务)${PANEL_APP_PORT_HTTP} → 80nginx:1.27-alpine:托管前端 dist、反代 /prod-api/ 到后端、按 /remote-apps/ 提供远程应用产物、/minio/ 反代对象存储、/upload/ 提供本地存储
backend${CONTAINER_NAME}-backend容器内 8080;官方版再映射 ${PANEL_APP_PORT_API}(默认 18080)后端 jar(镜像内 /app/app.jar)。启动前先解析中间件地址,首次启动自己建库建表

两个服务都是 restart: always。用户版的后端没有 ports: —— 它不对公暴露后端端口,浏览器请求一律经前端的 nginx。

中间件一个都不在包里,每个都在安装表单里「选面板已装的服务」或「填外部地址」:

依赖必填?怎么给不给会怎样
MySQL 8.x必填「数据库服务」下拉选面板里的 MySQL(面板自动建库 + 授权),或填 5 个「外部」字段容器启动失败:[entry] !! 没有数据库地址
Redis必填「Redis 服务」,或外部字段容器启动失败:[entry] !! 没有 Redis 地址
RabbitMQ选填服务 / 外部字段应用照常启动,只有「消息中心 / 站内信」不可用(入口脚本会明确写出来)
对象存储不用填装完在「系统工具 → 系统配置 → 文件存储配置」里启用一种默认所有配置都是停用状态 → 上传应用包、图片会失败
调度中心(独立包 wujies-snailjob-server)选填「调度中心地址」填 snailjob-server(该包的固定网络别名)留空 = 不启用:订单自动取消 / 自动收货、优惠券状态与过期、促销启停、统计刷新、会员积分过期等任务都不跑
监控中心(独立包 wujies-monitor-admin)选填「监控中心地址」填 http://monitor-admin:9090/admin留空 = 不注册,功能本身不受影响

地址解析规则写在 deploy/docker/backend/entrypoint.sh 的 pick() 里:外部字段非空 → 用外部;否则用面板选的服务;都空 → fail() 并打印该填哪一栏。这条逻辑必须在 shell 里做,compose 与 Spring 都表达不了这种条件。后端镜像里额外装了 default-mysql-client(deploy/docker/backend/Dockerfile),首次初始化就是用它连库。

容器路线没有 -Dloader.path

entrypoint.sh 最后一行只传了 -Dapp-market.deploy.plugin-dir,没有 -Dloader.path —— 插件由运行期加载器 PluginRuntimeManager 在启动期 + 运行期加载,所以「装应用 / 升应用不需要重启 JVM」在这条路线上成立。-Dloader.path 只存在于 jar 直跑那条路线(script/prune/start-host.ps1)。两套机制的差别见 插件运行时。


二、宿主机上的东西在哪 ​

实例目录形如 /opt/1panel/apps/local/wujies-platform-server/<实例名>/(<实例名> 就是面板「已安装」里显示的名字)。安装表单的值写在实例目录的 .env 里,docker-compose.yml、data/、sql/ 也都在这一层 —— 下文所有 docker compose 命令都要先 cd 到这里。

宿主(相对实例目录)容器内内容
./data/plugins-dist/app/plugins-dist插件 jar 落位目录(= app-market.deploy.plugin-dir)+ active-plugins.txt
./data/plugins-dist/migrations/<appCode>//app/plugins-dist/migrations/<appCode>/应用包里的迁移脚本(由后端读取执行);落位前会先清空该目录
./data/remote-apps/app/remote-apps(后端读写)、/var/www/remote-apps:ro(nginx 只读)远程应用前端产物(= app-market.deploy.frontend-dir)
./data/logs/app/logs四个日志文件,见下一节
./data/upload/app/upload本地文件存储(sys_oss_config 选 local 时用)
./data/temp/app/temp上传临时目录(compose 里 SPRING_SERVLET_MULTIPART_LOCATION=/app/temp)
./sql/app/sql:ro初始化 SQL 的 files/*.sql(后端首次启动时导入)

落位产物分三处放(AppPackageDeployService):

  • 后端插件 jar → ./data/plugins-dist/ 根下,同目录的 active-plugins.txt 记「每个应用当前生效哪个 jar」,格式是 appCode=jarFileName;
  • 迁移脚本 → ./data/plugins-dist/migrations/<appCode>/(落位前会先清空该目录,所以包里删掉或改名的旧脚本不会留在磁盘上被再次执行);
  • 前端产物 → ./data/remote-apps/<appCode>/,对外 URL = /remote-apps/<appCode>/ + 清单里 assets.frontend 的文件名,并带 ?v=<版本号> 破缓存(AppRuntimeController)。

一个目录两处挂,缺一处就是「安装成功但点进去 404」

./data/remote-apps 同时挂给后端(读写,落位用)和前端 nginx(只读,提供 /remote-apps/**)。两者必须是同一个宿主目录;不一致的表现是菜单在、接口在,页面报「远程视图不可用」或 /remote-apps/<code>/index.js 404。细节见 前端部署与 nginx。


三、日志在哪 ​

日志由 wujies-admin/src/main/resources/logback-plus.xml 定义:log.path = ./logs,而后端容器的 WORKDIR 是 /app(deploy/docker/backend/Dockerfile),所以容器内是 /app/logs,即宿主 ./data/logs。

文件内容级别滚动与保留
sys-console.log控制台同一份输出,全量INFO 及以上按天 sys-console.%d{yyyy-MM-dd}.log,maxHistory=1(只留 1 天)
sys-info.log只收恰好 INFO(LevelFilter 精确匹配,DEBUG/WARN/ERROR 都不进)恰好 INFO按天 gzip,maxHistory=60
sys-error.log只收 ERROR恰好 ERROR按天 gzip,maxHistory=60
容器标准输出docker compose logs 看到的那份同 console由容器运行时决定
bash
cd /opt/1panel/apps/local/wujies-platform-server/<实例名>

# 容器状态;反复重启说明起不来,看日志
docker compose ps

# 入口脚本的结论都在 [entry] 前缀的行里
docker compose logs --tail=200 backend | grep '\[entry\]'

# 落盘日志在宿主目录上,直接 tail 就行
tail -n 200 data/logs/sys-console.log
tail -n 200 data/logs/sys-error.log

# 容器内的同一份(容器里是 /app/logs)
docker compose exec backend sh -c 'tail -n 100 /app/logs/sys-console.log'

# 看插件到底加载了哪几个
docker compose logs backend | grep -E '插件加载完成|插件目录下有'

[entry] 行会依次给出:中间件地址解析结果(MySQL=… Redis=… RabbitMQ=…)、等待 MySQL 就绪、首次初始化或「已初始化过,跳过导入」、调度中心与监控中心是否启用、启动用的 profile。初始化的判据是 sys_config 里有没有 platform.init.version 这一行。

sys-console.log 只留一天

它是排障时最有用的那份(唯一含 WARN 的落盘文件:插件加载告警、目录远程调用失败、路由墓碑提示都只在这里),但 maxHistory=1。需要留证就当场复制走。

Actuator:能在线看健康与日志,但需要 HTTP Basic ​

application.yml 里 management.endpoints.web.exposure.include: '*'、management.endpoint.health.show-details: ALWAYS、management.endpoint.logfile.external-file: ./logs/sys-console.log:

端点用途
GET /actuator/health健康检查,含各组件明细(show-details: ALWAYS)
GET /actuator/logfile在线读 ./logs/sys-console.log(容器内 /app/logs/sys-console.log)
GET /actuator/loggers查看当前各级别;POST /actuator/loggers/<name> 可临时改级别
其余端点由 include: '*' 一并暴露(含环境、bean 拓扑等敏感信息)
bash
# 注意端口:这是**后端**端口(面板里的「后端接口端口」,默认 18080),不是前端端口
curl -s -u wujies:<密码> http://127.0.0.1:18080/actuator/health
curl -s -u wujies:<密码> 'http://127.0.0.1:18080/actuator/loggers/cn.wujies'

/actuator/** 的两件事必须记住

  1. 它走 HTTP Basic 鉴权,凭据是 spring.boot.admin.client.username / .password(SecurityConfig.getSaServletFilter() 用 SaHttpBasicUtil.check)。1Panel 路线下:填了「监控中心地址 + 账号 + 密码」时,entrypoint.sh 会把表单值导出成这两个属性;留空时用的是构建期写进 jar 的默认值(根 pom.xml 的 monitor.username / monitor.password,仓库里是 wujies / 123456)——上线前必须换掉(填监控中心表单,或给 backend 容器补 SPRING_BOOT_ADMIN_CLIENT_USERNAME / _PASSWORD 两个环境变量,或改 pom 默认值后重建镜像)。
  2. 别把后端端口暴露到公网。 官方版为了给用户版读目录,会把 ${PANEL_APP_PORT_API} 对公映射;而 include: '*' + show-details: ALWAYS 等于把配置项与 bean 拓扑一起送出去。公网只暴露前端端口,后端端口留给内网 / 反代白名单。

从前端端口打 /actuator/health 会「成功」,但那是假象

deploy/docker/frontend/nginx.conf 只反代了 /prod-api/、/remote-apps/、/minio/、/upload/,没有 /actuator。这条请求会落到 location / 的 SPA 回退,返回 HTTP 200 + index.html —— 看着像通了,其实一个字都不是监控数据。要么用后端端口,要么 docker compose exec backend 在容器里打。


四、日常检查清单 ​

#检查项怎么看异常信号
1容器与健康docker compose ps;curl -u <监控账号>:<密码> http://127.0.0.1:<后端接口端口>/actuator/health容器反复重启;health 里某组件非 UP
2插件落位与生效版本宿主 ls data/plugins-dist、cat data/plugins-dist/active-plugins.txt;日志里搜「插件加载完成」清单里的 jar 在磁盘上不存在;同一 appCode 堆了多个版本(可清,见第五节)
3应用入口清单GET /app/runtime/remote-apps(不加权限注解,返回本租户已启用且有前端产物的应用与 entry)少了某个已启用应用;纯后端应用不出现是正常的
4应用可用性GET /app/runtime/enabled-apps(本租户已启用应用编码,不要求有前端产物)与「我的应用」里的状态不一致
5目录服务连通(用户版 / 目录集中)调一次市场页,然后去对端的访问日志里找 GET /app/catalog/...报「目录接口令牌校验失败」;对端日志里根本没有对应请求
6签名就绪度GET /app/console/signature-readiness(权限 app:console:list)ready=false,或 unsigned / invalid / unverifiable 不为 0
7磁盘与卷宿主 df -h、docker system df;重点看 data/logs、data/plugins-dist、data/remote-apps日志盘满;plugins-dist 下同一应用堆积多个版本 jar
8卡死任务「我的应用 → 应用任务」里是否还有长期 进行中有就等超时清扫机制收尾,不要手改库
9备份项是否都在见 备份与安全签名私钥不在仓库之外留有备份 —— 最高优先级的一项

第 6 项不要做成定时任务

signature-readiness 会真实下载市场上所有已发布版本(上限 100,超出会报 truncated)逐个验签。按需调用即可,定时跑等于每天白拉一遍对象存储。

第 5 项有反直觉的纪律

验证目录远程调用时不要自己 curl 对端:否则对端日志里那几条 GET /app/catalog/... 是你自己打的,你会把「我打成功了」当成「调用方链路通了」。另外 HTTP 200 不等于成功 —— 远程目录出现过「200 但 body 是错误信封」。

第 8 项的机制:进程在任务执行中途被杀(重启、OOM、容器被驱逐)时,那条记录会永远停在 running,由 AppTaskTimeoutSweeper 收尾 —— 判据是 update_time(AppTaskRecorder 每推进一步就会刷新它),阈值是 app-market.task.timeout-minutes(默认 10 分钟),启动后 app-market.task.sweep-interval-ms(默认 60000)周期清扫,收敛成 status=failed + current_step=超时失败。看到「超时失败」时原始 log_text 还在,看它最后停在哪一步即可。


五、常用命令 ​

bash
# ---- 定位实例目录(面板「已安装」里能看到实际路径)----
cd /opt/1panel/apps/local/wujies-platform-server/<实例名>

# ---- 启停 ----
docker compose up -d                 # 应用 compose 文件的改动
docker compose restart backend       # 普通重启(**不会**重读 .env)
docker compose up -d --force-recreate backend   # 改过 .env / 面板参数之后用这条
docker compose down                  # 停掉本实例(数据在宿主目录与数据库里,不丢)

# ---- 看产物 ----
docker compose exec backend sh -c 'ls -l /app/plugins-dist && cat /app/plugins-dist/active-plugins.txt'
ls data/plugins-dist/migrations/     # 每个应用一个子目录

# ---- 连库看一眼(镜像里装了 default-mysql-client;库名/账号是安装表单随机生成的那组)----
docker compose exec backend sh
# 下面是**容器内**的交互命令:选外部库时把 PANEL_DB_* 换成 EXTERNAL_DB_*
mysql -h"$PANEL_DB_HOST" -P"${PANEL_DB_PORT:-3306}" -u"$PANEL_DB_USER" -p"$PANEL_DB_USER_PASSWORD" "$PANEL_DB_NAME"
# 进去之后:select config_value from sys_config where config_key='platform.init.version';

面板里有等价入口:应用商店 → 已安装 → 该实例的「日志」「终端」「参数」。在「参数」里改值并保存会重建容器,效果等同于 --force-recreate。

清理残留的旧插件 jar ​

宿主目录是 bind mount,所以在宿主机上删就等于在容器里删。平台仓库自带脚本(script/prune/prune-plugins.sh):

bash
# 先 dry-run,只会打印将删哪些
PLUGIN_DIR=./data/plugins-dist DRY_RUN=1 bash /path/to/wujies-platform/script/prune/prune-plugins.sh

# 确认后真删(只删「同一个 appCode 名下、不是 active-plugins.txt 记录的那一个」)
PLUGIN_DIR=./data/plugins-dist bash /path/to/wujies-platform/script/prune/prune-plugins.sh

必须显式传 PLUGIN_DIR

脚本用「脚本目录的上一级」当工程根来推断 plugins-dist,而它住在 script/prune/,算出来是 script/plugins-dist —— 不传参数会对着一个不存在的目录空跑。prune-plugins.ps1 与 start-host.ps1 有同样的推断缺陷(start-host.ps1 的 $root 也会算成 script/),这条已记在 wujies-platform/docs/开发问题与解决方案.md 的遗留清单里。

残留 jar 现在不致命,但清单是硬前提

运行期 PluginRuntimeManager(容器路线的唯一加载者)与启动期加载器(仅 jar 直跑路线)都按 active-plugins.txt 选版本,不在清单里的 jar 会被刻意跳过,日志里会打印「插件目录下有 N 个 jar,按 active-plugins.txt 只加载 M 个(其余是旧版本残留,正常)」。⚠️ 但 active-plugins.txt 缺失或为空时不过滤,会退回「目录下所有 jar 全挂」的旧行为 —— 那一版才真的会因为两份同样的 mapper XML 起不来。清理仍然值得做:残留占磁盘,且能避免清单缺失时踩坑。


六、改一个东西,怎么让它生效 ​

「改了没反应」绝大多数是生效方式用错了。按改的对象分四类(完整覆盖关系见 配置项):

改的是生效动作要重建镜像吗
安装表单里的值(端口、外部中间件地址、目录服务令牌、检查模式、调度 / 监控地址)面板「已安装 → 参数」保存(会重建容器),或 docker compose up -d --force-recreate不用。compose 把它们映射成环境变量,环境变量优先级高于 jar 内 yml
jar 内 yml 的值(例如 app-market.security.public-key)改源码 → 重新打包平台包 → docker build backend 镜像 → 换镜像重建容器要
前端 dist/(宿主前端源码)同上,重建 frontend 镜像要
实例目录里的 sql/files/*.sql换文件 + 删掉 sys_config.platform.init.version 标记行 + 重启容器不用(SQL 是 bind mount)。改表结构要另想办法,见 升级与补丁
平台表结构(存量库加列 / 加索引)手工执行 script/sql/update/ 里对应的幂等脚本不用
对象存储凭据后台「文件存储配置」直接改(落在 sys_oss_config 表)不用

签名公钥没有安装表单入口,换钥必须重建 backend 镜像

data.yml 里与安全相关的只有「应用包检查模式」,compose 也只传了 APP_MARKET_SECURITY_MODE —— 公钥没有走环境变量,它仍然来自 jar 内 application.yml 的 app-market.security.public-key(仓库里是开发验证阶段生成的那把)。所以要换成自己的钥:keygen 生成 → 用 -SyncPublicKey 或手工改源码 yml → 重新打包 + 重建 backend 镜像。 只改宿主上的文件没有用:yml 是打进 jar 的。顺序错了的表现是 enforce 把你刚签的包全部拒掉、报错却长得像「签名无效」。

1Panel 路线下「检查模式」以表单为准

APP_MARKET_SECURITY_MODE: ${SECURITY_MODE} 会覆盖 jar 内 yml 的值,而 data.yml 里这一栏的默认值是 warn(不是 yml 里的 enforce)。切 enforce 之前先跑一次 /app/console/signature-readiness,理由见 包安全与签名。


七、已知限制(设计如此,不是 bug) ​

下面每一条都有人当成缺陷报过。它们都是刻意的取舍。

限制为什么这样设计想绕过的话怎么做
迁移只做 up,不回滚MySQL 的 DDL 隐式提交,包一层事务是假的安全感;设计上要求脚本自身幂等自己写一个「反向」的新迁移脚本(新文件名),当作一次正常变更发布
迁移脚本内容不可修改已执行过的脚本按 (app_code, script) 记 SHA-256 摘要,内容变了直接拒绝执行新增一个脚本(02-xxx.sql),让全新安装走 01、存量部署走 02
应用装的菜单不能改路由 / 权限,也不能删这些字段与插件代码、前端路由、权限标识绑死,改了下次升级会被清单覆盖回去改名称 / 图标 / 排序 / 显隐是放行的;要换页面就改 app.json 并发布新版本
签名是单公钥模型一把私钥签所有包,服务端只配一把 app-market.security.public-key开放第三方上架需要换多钥注册表或平台审后重签
目录远程读取不做缓存读取频率极低(安装/升级时几次);加缓存会遇到「刚上架却装到旧版本」什么都不用做;超时已配(connect-timeout-ms: 3000 / read-timeout-ms: 15000),宁可快速失败
卸载默认不删数据表默认那条路是「保留数据」(可恢复);只有显式选「彻底删除」才删表,且只删应用包迁移脚本里声明过的表走「彻底删除」;对象存储里的图片两种模式都不删,要清自己清
卸载应用不会删数据库库与账号是安装表单时面板建的,卸载不该顺手删数据要清就在面板「数据库 → MySQL」里手工删库删用户(库名按 wujies_ 前缀找)
初始化 SQL 只在没有标记时导入判据是 sys_config.platform.init.version 这一行,避免每次重启重跑一遍只改数据:删标记行 + 重启;改表结构:清表或手工执行补丁脚本(别 DROP DATABASE)
「部署」按钮不跑迁移脚本POST /app/version/{versionId}/deploy 只做「落位 + 热加载」,迁移只在 install / upgrade 里跑要改表结构就发布更高版本后升级,或「彻底删除」后重装
用户版看不到上架端服务端 MarketEditionGuardFilter 对 /app/console、/app/version、/app/package 一律返回 403(不是 404、也不是静默失效),菜单整棵隐藏这是同一个 jar 的形态开关,不是阉割版;安装 / 升级 / 任务中心 / 菜单保护 / 签名校验全都在
assets.signature 字段未使用签名走的是包根 signature.txt(分离式),字段留着是为了兼容已上架的清单不要以为填了它就等于签了名 —— 校验只看包内有没有 signature.txt

jar 直跑那条路线的两个坑(1Panel 路线不涉及)

只在开发机 / 排障时用 script/prune/start-host.ps1 起 jar 的话:① 启动参数里的 -D 必须加引号("-Dloader.path=plugins-dist"),否则 PowerShell 会把它拆开;② 该脚本与 prune-plugins.ps1 的默认目录推断都是错的(见第五节),要显式传目录。交付与生产请走 1Panel 应用包。


相关阅读 ​

  • 部署总览 —— 四个 1Panel 包、六步部署、上线前必须替换的凭据
  • 后端部署 —— 三条部署路线、镜像与卷、JVM 参数
  • 配置项 —— app-market: 全量配置项与「谁覆盖谁」
  • 升级与补丁 —— 平台升级、应用升级、存量库补脚本、回滚能力
  • 故障排查 —— 按症状查原因(含 1Panel / Docker 一节)
  • 任务中心与日志 —— 安装/升级/卸载的过程记录在第一站怎么看
  • 备份与安全 —— 备什么、密钥纪律、上线前脱敏清单
  • 插件运行时 —— active-plugins.txt、热加载、加载日志怎么读
  • 前端部署与 nginx —— /remote-apps/ 四处路径的对齐与常见坑

我们坚信,即使再复杂的技术,也可以用清晰、干练、易懂的文字描述清楚。如果你在阅读时有难以理解的章节,那一定是我们还没有优化好它 —— 欢迎反馈。