这不是一份"照着抄"的部署清单,而是一台真实服务器从裸机到 HTTPS 全绿的过程记录。
目标与约束
要在一台腾讯云轻量(2C4G,Ubuntu 24.04,纯净镜像)上同时跑三类东西:
| 站点 | 形态 | 端口 |
|---|---|---|
admin.rainink.net | Vue 3 SPA 后台(NestJS 接口) | 443 → 静态目录 |
api.rainink.net | NestJS 后端 | 443 → 127.0.0.1:3000 |
rainink.net | Nuxt 4 纯静态站 | 443 → 静态目录 |
约束有几个:国内服务器,域名必须备案;同机多站点共用 80/443;静态站不允许在服务器上常驻 Node 进程。
施工顺序由备案决定,不由技术决定
一开始按"技术顺序"排:装环境 → 部署 → 配 Nginx → 上 HTTPS。跑到 HTTPS 就卡住了,因为 ACME 验证需要域名解析到这台机器,而域名解析又要等备案。
所以真正的顺序是三段:
| 阶段 | 依赖备案 | 可做的事 |
|---|---|---|
| 备案前 | 否 | 服务器初始化、装运行时、拉代码、构建、本机端口跑通 |
| 备案审核中 | 否 | Nginx 配置、内网反代、用 IP + 非标端口验收,写完文档 |
| 备案后 | 是 | 域名解析 → 签发证书 → 强制跳转 HTTPS → 公网验收 |
备案审核通常一到两周,这段时间正好把前两段做完。核心原则只有一条:审核期间域名不要解析,否则被管局检测到会因为"未备案即对外提供服务"被驳回,重排一次又是两周。
第一段:服务器初始化
登录统一走密钥,不走密码:
ssh -i ~/.ssh/tencent_cloud_ssh.pem -o StrictHostKeyChecking=no ubuntu@<服务器IP>
StrictHostKeyChecking=no 只在首次连接时省掉指纹确认,长期运维还是应该把指纹写进 known_hosts。
安装运行时的时候,国内服务器最容易卡在 Node 这一步——nvm 的安装脚本从 GitHub 拉,大概率超时。改用镜像:
export NVM_SOURCE=https://gitee.com/mirrors/nvm.git
export NODEJS_ORG_MIRROR=https://npmmirror.com/mirrors/node/
装完 Node,把 npm 源也指到镜像,否则 pnpm install 会在拉包上耗掉半小时。
数据库和应用分开装:MySQL 只监听 127.0.0.1,Redis 同样只给本机用。这是同机部署最容易被忽略的一条——数据库端口一旦对公网开放,等于把整个库交给扫描器。
第二段:拉代码、构建、跑通
私有仓库用 deploy key,不共用个人 SSH key:
ssh-keygen -t ed25519 -C "deploy@server" -f ~/.ssh/deploy_key
# 公钥贴到仓库的 Deploy Keys,勾选只读
后端构建与启动:
cd /var/www/super-admin-backend
pnpm install --prod=false
pnpm build
NODE_ENV=production node dist/main.js
先用前台方式跑一次,确认进程能起来、日志没有报错、curl 127.0.0.1:3000/api 有响应,再交给进程守护。
这里有个反复踩的坑:TypeORM 项目部署到空库,migration:run 报 Entity metadata for Menu#roleMenus was not found。原因是 data-source.ts 只手写注册了部分实体,另外还同时写了 export const 和 export default,导致"data source 只能有一个导出"的报错。
处理顺序是:删掉多余的 export default,实体改成 glob 扫描模块目录,没有迁移文件时先 DB_SYNC=true 让同步建表跑通,再补迁移。
静态站不需要任何运行时:
pnpm generate
# 产物在 .output/public,直接同步到 /var/www/rainink
rsync -a --delete .output/public/ /var/www/rainink/
--delete 要带上,否则旧的文章页会永远留在服务器上。
第三段:Nginx server 块
静态站的实际配置(/etc/nginx/sites-available/rainink.conf):
server {
server_name rainink.net www.rainink.net;
root /var/www/rainink;
index index.html;
charset utf-8;
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/css application/javascript application/json image/svg+xml text/plain application/xml application/rss+xml;
access_log /var/log/nginx/rainink.access.log;
error_log /var/log/nginx/rainink.error.log;
location / {
try_files $uri/index.html $uri $uri.html =404;
}
# 构建产物文件名带 hash,可以放心长缓存
location /_nuxt/ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
location = /favicon.svg {
expires 7d;
}
error_page 404 /404.html;
location = /404.html {
internal;
}
}
try_files 的顺序是关键。纯静态导出会把 /posts/foo 生成为 posts/foo/index.html,把 /about 生成为 about.html(取决于 trailingSlash)。两个都兜住,才不会出现"首页能开、文章页 404"。
接口站的反代要点:
location /api/ {
proxy_pass http://127.0.0.1:3000/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
WebSocket 单独一段,必须转发 Upgrade:
location /ws {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
每次改配置都先备份再 reload:
sudo cp /etc/nginx/sites-available/rainink.conf \
/etc/nginx/sites-available/rainink.conf.bak.$(date +%Y%m%d%H%M%S)
sudo nginx -t && sudo systemctl reload nginx
nginx -t 必须串在 reload 前面,用 && 而不是 ;——配置写错时直接中断,不让坏配置生效。
第四段:证书与强制 HTTPS
域名解析生效后,一条命令签发并自动改写 server 块:
sudo certbot --nginx -d rainink.net -d www.rainink.net \
--non-interactive --agree-tos -m <你的邮箱> --redirect --keep-until-expiring
certbot 会做三件事:把 443 的 listen 和证书路径写进 server 块、补一个 80 端口只做 301 跳转的 server 块、注册自动续期定时任务。
验证续期是否真的可用:
sudo certbot renew --dry-run
输出里应该看到每张证书 success。这一步不能省——续期失败通常没人发现,直到某天证书过期、所有接口报错。
上线验收:不只看"能不能打开"
验收按五件事依次过:
- 状态码:
https://站点/与关键子路径都要 200; - 跳转:
http://必须 301 到https://,且www与非www只有一个规范域; - 证书主体:
openssl s_client -connect 127.0.0.1:443 -servername rainink.net看 SAN 是否包含两个域名; - 多站点互不影响:
admin、api子域各自 200,改一个站的配置不能打断另一个; - 404 行为:随机不存在的路径要返回 404,而不是首页内容(否则 SEO 会收录一堆软 404)。
域名还没解析、或者想绕过 DNS 直接验证服务器时,用 --resolve 把域名指到 IP:
curl -sS -o /dev/null -w "%{http_code}\n" \
--resolve rainink.net:443:<服务器IP> https://rainink.net/
这个技巧在备案期验收特别有用:不解析域名,也能验证整条链路是通的。
回滚
配置回滚靠时间戳备份:
sudo cp /etc/nginx/sites-available/rainink.conf.bak.20261022163044 \
/etc/nginx/sites-available/rainink.conf
sudo nginx -t && sudo systemctl reload nginx
静态站回滚靠 Git:把上一个版本的构建产物重新 rsync 一次即可。所以每次发布前,产物目录要保留至少一版。
小结
同机多站点的复杂度不在技术,而在顺序:备案决定了什么时候能上域名,域名决定了什么时候能签证书。把"不依赖备案"的工作全部前置,等域名可用时只剩解析、证书、验收三步,整个上线过程就是可控的。
真正要小心的反而是那些不起眼的细节——try_files 的顺序、nginx -t 有没有串上、--delete 有没有带、续期演练做没做。这些不出问题时没人看得见,出问题时都是全站级别的。