07 第一次完整上线

本章把前面的知识串起来。示例以常见的 Ubuntu/Debian + systemd 为背景;不同发行版的 Nginx 配置目录和包管理命令可能不同。

1. 上线前准备清单

需要知道:

不要到了生产服务器才临时决定数据库密码、端口和目录。

2. 使用普通部署用户

假设使用 deploy 用户。应用目录应归它所有,而不是让 Node.js 长期以 root 运行。

目录示例:

/var/www/nloop/current

如果需要管理员创建目录,可以使用类似:

sudo install -d -o deploy -g deploy /var/www/nloop/current

之后以 deploy 用户完成源码、依赖、构建和 PM2 操作。

3. 安装并固定 Node.js

按照 NVM 官方安装方法安装 NVM 后:

nvm install 22
nvm alias default 22
nvm use 22

node --version
npm --version

这里的 22 是教程示例。应使用项目经过验证且仍受支持的 Node.js 版本。

安装 PM2:

npm install --global pm2@latest
pm2 --version
which pm2

4. 获取项目代码

可以通过 Git、CI 构建产物或内部发布包。使用 Git 时示意:

cd /var/www/nloop
git clone <你的仓库地址> current
cd current

私有仓库的 SSH key 和 access token 属于敏感凭证,应使用最小权限,不能写进教程、仓库或日志。

5. 准备生产环境变量

创建:

/var/www/nloop/current/.env

示例内容:

NODE_ENV=production
HOST=127.0.0.1
PORT=8080
DB_HOST=127.0.0.1
DB_PORT=3306
DB_NAME=nloop
DB_USER=nloop_app
DB_PASSWORD=replace-with-real-secret

限制权限:

chmod 600 .env

不要在终端执行 cat .env 后把输出复制到工单或聊天中。

6. 安装依赖并构建

如果服务器负责构建:

npm ci
npm run typecheck
npm run build
npm prune --omit=dev

检查构建入口:

ls -l dist/server.js

如果不存在,不要继续启动 PM2。先检查 tsconfig.build.jsonrootDiroutDirinclude 和实际输出结构。

7. 先直接运行应用

NODE_ENV=production node dist/server.js

从另一个 SSH 会话测试:

curl -i http://127.0.0.1:8080/health

期望类似:

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8

{"status":"ok"}

验证完成后,用 Ctrl+C 正常停止这个前台进程,再交给 PM2。不要同时保留前台进程,否则 PM2 会因为 8080 端口占用而启动失败。

8. 准备 ecosystem 文件

在项目根目录创建 ecosystem.config.js。当前 package.json 没有 "type": "module",所以这个 .js 文件会按 CommonJS 解释:

module.exports = {
  apps: [
    {
      name: "nloop-api",
      cwd: "/var/www/nloop/current",
      script: "./dist/server.js",
      instances: 1,
      exec_mode: "fork",
      autorestart: true,
      restart_delay: 1000,
      time: true,
      env: {
        NODE_ENV: "production",
        HOST: "127.0.0.1",
        PORT: "8080",
      },
    },
  ],
};

环境变量不要同时在 .env、ecosystem、systemd 和 shell 中到处重复定义。第一版可以:

普通运行配置 → ecosystem
数据库密码等秘密 → .env

并在团队内形成固定约定。

9. 使用 PM2 启动

pm2 start ecosystem.config.js
pm2 list
pm2 show nloop-api
pm2 logs nloop-api --lines 100 --nostream

再次验证:

curl -i http://127.0.0.1:8080/health

如果 PM2 不断增加 restart 次数,说明应用处于崩溃循环,应先看 error log,而不是不断执行 restart。

10. 配置 Nginx

Ubuntu/Debian 常见配置文件:

/etc/nginx/sites-available/nloop

最小内容:

server {
    listen 80;
    server_name example.com;

    root /var/www/frontend;
    index index.html;

    access_log /var/log/nginx/nloop-access.log;
    error_log /var/log/nginx/nloop-error.log warn;

    location / {
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;

        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;
    }
}

在采用 sites-enabled 的发行版中启用站点:

sudo ln -s /etc/nginx/sites-available/nloop \
  /etc/nginx/sites-enabled/nloop

如果目标已经存在,不要使用强制覆盖命令,应先检查现有配置由谁维护。CentOS/RHEL 等系统可能使用 /etc/nginx/conf.d/,以实际发行版为准。

验证并加载:

sudo nginx -t
sudo systemctl reload nginx
sudo systemctl status nginx

11. 分层验证请求

# Express
curl -i http://127.0.0.1:8080/health

# 本机 Nginx,并指定虚拟主机
curl -i -H "Host: example.com" http://127.0.0.1/api/health

# 公网域名
curl -i http://example.com/api/health

每一步成功后再进入下一步,错误范围会小很多。

12. 配置 PM2 开机启动

deploy 用户执行:

pm2 startup

复制执行 PM2 输出的、包含实际 NVM PATH 的 sudo 命令。然后:

pm2 save

确认服务:

sudo systemctl status pm2-deploy
sudo systemctl is-enabled pm2-deploy

13. 验证重启恢复

服务器重启属于有影响操作,只能在有维护权限和维护窗口时进行。重启后检查:

node --version
sudo systemctl status nginx
sudo systemctl status pm2-deploy
pm2 list
curl -i http://127.0.0.1:8080/health
curl -i http://example.com/api/health

如果 systemd 显示 PM2 成功,而 pm2 list 为空,通常是没有正确 pm2 save、用户不一致或 PM2_HOME 不一致。

14. 首次上线验收表

官方参考