一键部署脚本
在本机运行一条命令,自动完成「本地构建(可选)→ 打包 → 上传服务器 → 重启进程」。
服务器上不需要安装任何东西,它复用的就是你已经能用的 SSH。
用哪个脚本
| 你的系统 | 脚本 | 配置文件 |
|---|---|---|
| Windows | deploy.ps1 |
deploy.config.ps1(照抄 deploy.config.example.ps1) |
| macOS / Linux | deploy.sh |
deploy.config.sh(照抄 deploy.config.example.sh) |
Mac 上先执行一次 chmod +x deploy/deploy.sh,之后用法和 Windows 版完全一样:
./deploy/deploy.sh api
./deploy/deploy.sh admin
./deploy/deploy.sh
./deploy/deploy.sh api --dry-run # 只打包、只显示会上传哪些文件,不真传
./deploy/deploy.sh api --with-install # 顺带装依赖(只有改了 package.json 才需要)
第一次用先跑
--dry-run,它会打印打包大小和文件清单,核对无误再去掉这个参数。
一次性配置
1. 填配置文件
编辑 deploy/deploy.config.ps1(Mac 上是 deploy/deploy.config.sh,字段名多一层下划线,含义完全一样),需要改这几项:
| 配置项 | 填什么 | 怎么查 |
|---|---|---|
Host |
服务器 IP 或域名 | — |
Port |
SSH 端口 | 宝塔 → 安全 → SSH 端口 |
User |
SSH 登录用户 | 一般是 root |
NodeBin |
Node 的 bin 目录 | 宝塔 → 软件商店 → Node 版本管理器 |
Api.RemotePath |
服务端在服务器上的绝对路径 | 你部署时放的位置 |
Admin.RemotePath |
管理端在服务器上的绝对路径 | 同上 |
Api.RestartCmd |
重启服务端的命令 | 服务器上执行 pm2 list 看进程名 |
Admin.RestartCmd |
重启管理端的命令 | 同上 |
Api.StartCmd / Admin.StartCmd |
杀掉旧进程后把服务拉起来的命令 | 照抄模板;项目路径或端口变了才需要改 |
Api.HealthUrl / Admin.HealthUrl |
启动后的探活地址 | 照抄模板;端口变了才需要改 |
NginxCacheDir |
Nginx 反向代理缓存目录 | 宝塔 → 软件商店 → Nginx → 配置修改,搜 proxy_cache_path |
NginxBin |
Nginx 可执行文件路径 | 一般是 /www/server/nginx/sbin/nginx |
2. 配置免密登录(强烈建议)
不配的话,每次部署要输两次密码。配好之后全程无提示。
# 本机生成密钥(已生成过可跳过,直接第二次命令)
ssh-keygen -t ed25519
# 把公钥传到服务器
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh root@你的服务器 "mkdir -p ~/.ssh && cat >> ~/.ssh/authorized_keys"
3. 确认服务器上的 .env 已就位
脚本不会上传 .env(这是故意的,避免覆盖服务器上的密钥配置)。
- 服务端:
yelaymaths_api/.env必须已在服务器上配好 - 管理端:
yelaymaths_admin/.env.local必须已在服务器上配好- 注意
NEXT_PUBLIC_*是构建时写死的,所以第一次构建前它就得在
- 注意
日常使用
.\deploy\deploy.ps1 api # 只部署服务端
.\deploy\deploy.ps1 admin # 只部署管理端
.\deploy\deploy.ps1 # 两个都部署
输出长这样:
=== yelaymaths_api (server API) ===
[1/5] Building locally: pnpm prisma:generate && pnpm build
[2/5] Packing yelaymaths_api
160 KB (dist, prisma, package.json, pnpm-lock.yaml, pnpm-workspace.yaml)
[3/5] Uploading to /www/wwwroot/yelaymaths/yelaymaths_api/api
[4/5] Running on server
__DEPLOY_DONE__
[5/5] Restarted
Started, health check passed: http://127.0.0.1:3002/healthz
Nginx proxy cache cleared and reloaded
All done in 12.0s
其它参数
| 参数 | 作用 |
|---|---|
-DryRun |
只打包、只显示会上传哪些文件,不真的传。第一次用建议先跑这个 |
-WithInstall |
额外安装依赖。只在改了 package.json 时才需要 |
为什么默认不装依赖
服务器上的全局 pnpm 是 8.x,而本机是 10.x,两代锁文件格式不兼容——服务器直接读不了我们的 pnpm-lock.yaml。
而服务器上不能升级 pnpm(其他服务也在用)。
好在只有「装依赖」这一步需要版本匹配,「构建」不需要——pnpm build 只是执行 package.json 里的脚本,pnpm 8 也能跑。
所以日常部署走「上传源码 + 构建」,用服务器上现成的 node_modules,既不碰全局 pnpm,也快得多。
只有 package.json 变了才需要装依赖,这时用 -WithInstall,它会通过 npx 临时拉一个 10.27.0 的 pnpm 来跑,同样不影响全局。
上传哪些文件
脚本用的是白名单,只有列在 DeployPaths 里的路径会被打包。所以这些东西结构上不可能被传上去:
.env/.env.local(服务器上的配置不会被覆盖)node_modules/- 白名单之外的源码目录
Windows 版配置(deploy.config.ps1)已经在本机构建、只传产物:服务端传 dist + prisma(约 160 KB),
管理端传 .next + next.config.ts(约 540 KB,73 MB 的构建缓存已排除),服务器不再执行编译,部署完自动重启并探活。
已知限制
删除的文件不会同步删除。 脚本是「覆盖式上传」,如果你在本地删掉某个源码文件,服务器上那份还在。
一般不影响运行(多余的文件不会被执行),但如果你发现改了名字的旧文件仍在作怪,手动上服务器删掉即可。
需要「完全同步」的场景(比如删文件、想要版本回滚),改用 git 推送部署更合适——那是另一个方案,需要时再说。
部署结束后会清一次 Nginx 缓存
宝塔在 /www/server/nginx/conf/proxy.conf 里对所有反向代理站点开了 proxy_cache cache_one,而 Next.js 给页面发的响应头是 Cache-Control: s-maxage=31536000——翻译过来就是「这一页允许缓存一年」。
两边一叠加,后果很隐蔽:新构建传上去、进程也重启了,用户访问到的还是缓存里的旧 HTML;而旧 HTML 引用的分包早被新构建删掉,请求直接 404,页面渲染到一半就断,表现成「某个区块整块消失」,看起来像代码写错了,实际跟代码毫无关系。
所以脚本在每次部署的最后会多做一步:
find /www/server/nginx/proxy_cache_dir -mindepth 1 -delete # 清空缓存文件
/www/server/nginx/sbin/nginx -s reload # 重载 Nginx
服务端和管理端两条部署线都带这一步。缓存只影响响应速度,清空后第一次访问会回源到新构建,不会丢数据。
把 NginxCacheDir 留空('')就会跳过这一步。
部署结束会把服务自动拉起来
杀掉旧进程不等于新进程会跑起来。之前这里是「只杀不拉」,所以每次部署完都要去宝塔面板点一下「启动」,忘了点线上就是 502。
现在每个应用多两项配置:
RestartCmd:杀掉旧进程StartCmd:把新进程拉起来(setsid+nohup+ 重定向,SSH 会话断了也不受影响)HealthUrl:拉起后探活,30 秒内没响应就让部署直接判失败
所以正常输出结尾会是:
[5/5] Restarted
Started, health check passed: http://127.0.0.1:3001/
Nginx proxy cache cleared and reloaded
进程日志写在服务器上:/www/wwwlogs/yelaymaths-api.out.log、/www/wwwlogs/yelaymaths-admin.out.log。
有一点要知道:进程是脚本拉起来的,不是宝塔面板拉起来的,所以面板里那个项目的状态可能显示成「未启动」,但服务本身是正常在跑的(脚本每次都会探活确认)。如果想让面板重新接管,在面板点一次「重启」即可,之后照常用面板管。
只有 RestartCmd 不想自己启动(比如服务由 PM2 或 systemd 托管),把 StartCmd 留空('')就会退回「只杀不拉」的老行为。
出问题怎么排查
| 现象 | 原因 | 处理 |
|---|---|---|
deploy.config.ps1 not found |
没建配置文件 | 复制 deploy.config.example.ps1 |
| 一直提示输密码 | 没配免密登录 | 见上面第 2 步 |
pnpm: command not found |
NodeBin 填错 |
在服务器上执行 which pnpm 看真实路径 |
pm2: command not found |
PM2 不在 Node 的 bin 目录 | 服务器上执行 which pm2,把目录也加到 NodeBin |
| 构建成功但页面没变 | 进程没重启,或 Nginx 缓存没清 | 检查 RestartCmd、NginxCacheDir 填对没有 |
| 页面某个区块整块消失 | 缓存里的旧 HTML 引用了已 404 的旧分包 | 手动清一次 NginxCacheDir 并 reload |
部署报 service did not come up |
新进程没起来 | 看 /www/wwwlogs/yelaymaths-*.out.log 的报错,多半是端口被占或依赖没装 |
Permission denied |
目标目录权限不对 | 服务器上 chown -R root:root 目标目录 |