这篇是上一份部署路径的"另一半":把过程中真实卡住的地方逐条记下来,方便下次少走一遍。
1. nvm 装不上:GitHub 拉不动
现象是 curl -o- https://raw.githubusercontent.com/.../install.sh | bash 一直挂着,最后超时。
解法是换镜像源,两条环境变量就够:
export NVM_SOURCE=https://gitee.com/mirrors/nvm.git
export NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node/
NVM_SOURCE 管的是 nvm 自己,NODEJS_ORG_MIRROR 管的是后续 nvm install 下载 Node 二进制。只改前者,装完 nvm 依然会在下载 Node 时卡住。
2. npm 源没跟着换
nvm 换完源,npm install 还是慢。补一条持久化配置:
npm config set registry https://registry.npmmirror.com
3. 权限:ubuntu 用户读不了 /root
想让 Nginx 指向某个目录,ls 报 Permission denied——文件在 /root 下面。
原则很直接:Web 根目录永远不要放在任何用户的家目录里。统一放 /var/www/<站点名>,属主给 www-data,权限 755。/root 是 700,Nginx 的 worker 进程进不去。
4. TypeORM 部署到空库直接起不来
三个报错连着来:
Unknown argument: src/data-source.ts
Given data source file must contain only one export
Entity metadata for Menu#roleMenus was not found
对应三个原因:
| 报错 | 原因 | 处理 |
|---|---|---|
Unknown argument | 命令后多拼了 -d src/data-source.ts,package.json 的脚本里已经带了 | 删掉多余参数 |
must contain only one export | 文件里同时有 export const 和 export default | 删掉 export default |
Entity metadata ... not found | 只手写注册了部分实体 | 改成 glob 扫描模块目录 |
另外,项目里没有迁移文件却设了 DB_SYNC=false,空库永远建不出表。临时解法是先 DB_SYNC=true 跑通,再补迁移——不要在空库上死等 migration:run。
5. curl 请求本机,命中了别的站点
curl 127.0.0.1 返回的不是预期站点的内容。原因很简单:Nginx 里 server_name 是 admin.xxx.com,请求没有 Host 头,Nginx 就交给 default_server 处理了。
验证时手动带 Host:
curl -sS -H 'Host: api.rainink.net' http://127.0.0.1/api/health
这条坑和 --resolve 是同一类问题的两面:域名相关的配置,必须带域名去验证。
6. PowerShell 吞掉远端命令里的 $(date)
想给配置加时间戳备份:
ssh ubuntu@host "sudo cp x.conf x.conf.bak.$(date +%Y%m%d%H%M%S)"
结果本地 PowerShell 先执行了 $(date ...),报"无法将值 +%Y%m%d%H%M%S 转换为 DateTime"。
原因:双引号里的内容在本地就被展开了。远端命令必须用单引号包裹,让 $ 原样传过去:
ssh ubuntu@host 'sudo cp x.conf x.conf.bak.$(date +%Y%m%d%H%M%S)'
反过来的坑也存在:$host、$uri 这类 Nginx 变量如果写在双引号里,会被 PowerShell 当场吃掉。所以凡是内容里带 $ 的,一律改成本地写文件 + scp 上传,比在命令行里跟引号搏斗可靠得多。
7. scp 上传配置再 mv,比 heredoc 安全
带引号的 Nginx 配置通过 SSH 直接写,极易被本地 shell 改写。稳定做法:
scp -i $pem .\temp\rainink.conf ubuntu@host:/tmp/rainink_new.conf
ssh -i $pem ubuntu@host 'sudo cp /etc/nginx/sites-available/rainink.conf /etc/nginx/sites-available/rainink.conf.bak.$(date +%Y%m%d%H%M%S) && sudo cp /tmp/rainink_new.conf /etc/nginx/sites-available/rainink.conf && sudo nginx -t && sudo systemctl reload nginx'
先在 /tmp 落地、备份、替换、nginx -t、reload,五步串在一条 && 链里,任何一步失败都不会把线上配置弄坏。
8. 未备案就别解析域名,验收用 IP + 端口
备案审核期的验收方案:
- 后端:
ufw allow 3000/tcp,直接验http://<IP>:3000/api/health; - 前端:临时开 8080 端口验页面;
- 验完立即
ufw delete allow 3000/tcp,别把临时口留着过夜。
或者更干净的做法:不开放任何额外端口,在服务器本机用 curl -H 'Host: ...' 验证,验完即走。
9. certbot 多域名的坑
--redirect 会补一个 80 端口只做 301 的 server 块。站点多了以后常见两个问题:
- 同一个 IP 上多个证书,80 端口块的
server_name写重复,Nginx 报conflicting server name; - 申请时漏了
www,导致https://www.xxx证书主体不匹配。
确认一下证书覆盖范围:
echo | openssl s_client -connect 127.0.0.1:443 -servername xxx.net 2>/dev/null \
| openssl x509 -noout -subject -dates -ext subjectAltName
subject 与 SAN 里应该同时出现裸域和 www。
10. 续期演练没人做
certbot renew --dry-run 通过,不代表真的会续。要检查三件事:
systemctl list-timers | grep certbot有定时器;- 80 端口对 ACME 验证可达(安全组、
ufw都要放行); - 续期后 Nginx 会自动 reload(certbot 的 deploy hook)。
这三条任意一条断了,三个月后就是一次线上事故。
附:一套顺序化的自检清单
上线前后按这个顺序过一遍,能挡掉上面绝大多数问题:
[ ] node/npm 源已指向国内镜像
[ ] 数据库/Redis 只监听 127.0.0.1,安全组未放行 3306/6379
[ ] Web 根目录在 /var/www,属主 www-data
[ ] 构建产物 rsync 带 --delete
[ ] nginx -t 与 reload 用 && 串联
[ ] 每个域名都能带 Host 在 127.0.0.1 上验通
[ ] http → https 301 生效,www 与非 www 只有一个规范域
[ ] 证书 SAN 包含全部域名
[ ] certbot renew --dry-run 全绿,定时器存在
[ ] 临时放行的端口已删除
[ ] 上一版产物与配置备份都还在
小结
这十个问题里,只有两个是"知识型"的(TypeORM 实体注册、certbot 多域名),其余八个都是执行习惯问题:引号怎么包、备份做不做、nginx -t 串不串、临时端口删不删。
习惯类问题不会因为经验增加而消失,只会因为清单存在而被跳过。所以最后那份自检清单,比任何一条具体的解决办法都更值得留着。