别再逐台登录传 jar 了:我用 Node 写了个多服务器 Java 服务自动发布工具
前因:发一次版,要登 N 台服务器
我们的业务系统是典型的多地区部署:20 多台服务器,每台上面跑着几个用 Docker 起的 Java 服务,分布在不同国家、网络环境各异。每次发版的流程大概是这样:
- SSH 登录服务器,找到 jar 所在目录
- 手动把旧包改个名字备份(
app.jar.bak.20250101之类的命名逐渐失控) scp上传新 jar(几十到一百多 MB,看网速)docker restart xxxdocker logs -f盯到出现Started ... in x seconds
一套下来单台五分钟,20 多台就是一两个小时,还全是最机械的操作。更重要的是这套流程完全依赖人肉:传错包、忘了备份、重启后发现起不来、健康检查靠肉眼——这些坑我全踩过。最崩溃的一次是某台服务器服务没起来,而我已经不记得传之前备份的文件叫什么名字了。
于是有了这个项目:april-updater,一个开源的 Java 服务自动化发布工具。
解决了什么
核心思路很简单:把「登录 → 备份 → 传包 → 重启 → 确认」这条流水线固化到程序里,把人从循环里拿出来。
- 按服务器管理服务:选一台服务器,看到它上面所有的 Java 服务,选服务、选 jar、点部署
- 批量接入:粘贴「IP + 私钥」,自动 SSH 连接扫描上面的 Java 容器、jar 挂载路径、端口映射,配置自动生成——20 台服务器十分钟接完
- 失败自动回滚:部署前把旧 jar 改名为
.bak,上传重启后轮询健康检查接口,任一步失败就还原备份、重启旧版本、再次确认旧版本健康 - 服务器之间互不影响:某台失败只标记那一条任务,其他台照常
还有一个设计决策值得一提:工具只依赖 SSH,不依赖任何面板的 API。最初我走的是 1Panel OpenAPI(md5 鉴权那套,我从它的 Go 源码里确认的鉴权算法),但后来发现每台服务器都要去面板开 API、配 IP 白名单,20 多台配下来比手点还累。而 SSH + SFTP 是所有服务器天然就有的能力——上传用 SFTP,备份和重启就是普通的 shell 命令,docker restart、systemctl restart 都行。所以最终整个连接层换成了 ssh2,面板只是被扫描的对象,不是依赖。
技术实现
技术栈刻意保持简单:Node.js 20 + Express + ssh2,前端一个无构建步骤的单 HTML 文件。
部署流水线(lib/deployer.js)是核心。每台服务器一条独立的异步任务:
检查旧 jar 存在 → mv 备份为 .bak
→ SFTP 上传新 jar
→ 执行 restartCommand
→ 健康检查轮询(默认 120s 超时、5s 间隔)
└─ 任一步失败 → 回滚:还原 .bak → 重启 → 健康检查
任务状态实时写进一个 steps 数组,前端每 1.5 秒轮询一次渲染成时间线。这里有个值得记的坑:最早一版我在单个服务的回滚逻辑里引用了外部状态,异步竞态下偶发空指针,而且异常没兜底会导致整个进程退出——部署工具自己崩了比不发版还尴尬。后来把每台服务器的任务彻底隔离成独立对象,所有异步路径都包了 try/catch,单台失败只影响自己那条任务。
批量接入(lib/provisioner.js)解决的是「20 台服务器配置太繁琐」:
- 连接后用
docker ps按镜像关键词(java/openjdk/jdk)筛出 Java 容器 docker inspect看 Mounts,找到挂载到/app的宿主目录,ls *.jar确定唯一的 jar 文件- 从端口映射(
0.0.0.0:8884->8084)拼出健康检查地址 - 全部结果在页面上确认后合并进
config.json,同名服务自动去重
私钥管理:每台服务器一对独立密钥,导入时私钥落盘到 keys/<IP>.pem(权限 600),配置文件里只存路径;API 永远不把密钥内容回显给前端。配合 .gitignore 把 config.json、keys/、*.pem 全部排除,开源仓库里不会带出任何敏感信息。
整条流水线长这样:
怎么使用
1. 启动
git clone https://github.com/april-projects/april-updater.git
cd april-updater
npm install
cp config.example.json config.json
npm start # http://127.0.0.1:5230
2. 接入服务器
点右上角「接入服务器」,每台服务器一段粘贴(IP 一行,私钥整块紧跟其后):
1.2.3.4
-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
点「扫描」——工具自动连上、装好配置、列出发现的 Java 服务,确认后点「保存到配置」。没有密码登录也没关系,这就是纯密钥模式。
3. 日常发版
把构建好的 jar 放进该服务的本地目录(配置里的 localJarDir,或页面上传)→ 选服务器、选服务、选 jar → 点「开始部署」,然后看进度条:
✔ 备份旧版本 /opt/app/app.jar → app.jar.bak
✔ 上传新 jar 已上传至 /opt/app/app.jar(98 MB)
● 重启服务 docker restart app
○ 健康检查 ...
健康检查变绿就完事了;变红会自动回滚,任务状态标成「已回滚」,你只需要看失败原因。
怎么部署
工具自己也可以容器化,镜像已发布到 GitHub Container Registry,直接拉取:
docker pull ghcr.io/april-projects/april-updater:latest
仓库 deploy/ 目录带了 docker-compose.yml:
services:
april-updater:
image: ghcr.io/april-projects/april-updater:latest
ports: ["5230:5230"]
volumes:
- ./config.json:/app/config.json
- ./keys:/app/keys
- ${HOME}/.ssh:/root/.ssh:ro
- ${HOME}/jars:/root/jars
cd deploy && docker compose up -d
有几个容器化时踩到的点,如果你也要这么做可以参考:
- 容器内
HOME=/root,配置里写的~/jars这类路径要挂载到/root/jars才能被解析到 - 历史配置里指向宿主机绝对路径的私钥,我加了一个回退逻辑:路径读不到时尝试
/app/keys/<同名文件> - 镜像基于
node:20-alpine,npm ci --omit=dev,整个镜像 100MB 出头;GitHub Actions 在每次推送时自动构建发布
总结
这个工具解决的问题很朴素——把重复的运维动作程序化,把「确认」留给机器,把「决策」留给人。它没有引入任何新概念:不需要 Agent、不需要在服务器上装东西、不需要改现有部署方式,一个 SSH 账号就能跑起来。
代码已开源:GitHub - april-updater,MIT 协议。如果你也有一堆散落在各处的 Java 服务要维护,可以试试。当然,部署工具会通过 SSH 修改远端文件并重启服务,先在测试环境把流程跑通再上生产。

浙公网安备 33010602011771号