技术

一台腾讯云轻量跑起多站点:从裸机到 HTTPS 的实际路径

一台 2C4G 腾讯云轻量同时承载后台、接口与静态站:备案施工顺序、Nginx 多 server 块、certbot 签证书与上线验收。

目录
  1. 目标与约束
  2. 施工顺序由备案决定,不由技术决定
  3. 第一段:服务器初始化
  4. 第二段:拉代码、构建、跑通
  5. 第三段:Nginx server 块
  6. 第四段:证书与强制 HTTPS
  7. 上线验收:不只看"能不能打开"
  8. 回滚
  9. 小结

这不是一份"照着抄"的部署清单,而是一台真实服务器从裸机到 HTTPS 全绿的过程记录。

目标与约束

要在一台腾讯云轻量(2C4G,Ubuntu 24.04,纯净镜像)上同时跑三类东西:

站点形态端口
admin.rainink.netVue 3 SPA 后台(NestJS 接口)443 → 静态目录
api.rainink.netNestJS 后端443 → 127.0.0.1:3000
rainink.netNuxt 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。这一步不能省——续期失败通常没人发现,直到某天证书过期、所有接口报错。

上线验收:不只看"能不能打开"

验收按五件事依次过:

  1. 状态码:https://站点/ 与关键子路径都要 200;
  2. 跳转:http:// 必须 301 到 https://,且 www 与非 www 只有一个规范域;
  3. 证书主体:openssl s_client -connect 127.0.0.1:443 -servername rainink.net 看 SAN 是否包含两个域名;
  4. 多站点互不影响:admin、api 子域各自 200,改一个站的配置不能打断另一个;
  5. 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 有没有带、续期演练做没做。这些不出问题时没人看得见,出问题时都是全站级别的。