技术

国内服务器部署的十个坑:从镜像源到 certbot

从镜像源到 certbot 的十个真实故障:nvm 拉不动、目录权限、空库迁移、Host 头命中错站、证书续期与备案期验收。

目录
  1. 1. nvm 装不上:GitHub 拉不动
  2. 2. npm 源没跟着换
  3. 3. 权限:ubuntu 用户读不了 /root
  4. 4. TypeORM 部署到空库直接起不来
  5. 5. curl 请求本机,命中了别的站点
  6. 6. PowerShell 吞掉远端命令里的 $(date)
  7. 7. scp 上传配置再 mv,比 heredoc 安全
  8. 8. 未备案就别解析域名,验收用 IP + 端口
  9. 9. certbot 多域名的坑
  10. 10. 续期演练没人做
  11. 附:一套顺序化的自检清单
  12. 小结

这篇是上一份部署路径的"另一半":把过程中真实卡住的地方逐条记下来,方便下次少走一遍。

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 块。站点多了以后常见两个问题:

  1. 同一个 IP 上多个证书,80 端口块的 server_name 写重复,Nginx 报 conflicting server name;
  2. 申请时漏了 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 串不串、临时端口删不删。

习惯类问题不会因为经验增加而消失,只会因为清单存在而被跳过。所以最后那份自检清单,比任何一条具体的解决办法都更值得留着。