部署与生产环境

本指南介绍如何将 VextJS 应用部署到生产环境,涵盖构建、Docker 容器化、Nginx 反向代理、PM2 进程管理、日志收集和健康检查等实践。

构建生产产物

vext build

使用 vext build 将 TypeScript 源码编译为生产级 JavaScript:

vext build

编译产物输出到 dist/ 目录,保持与 src/ 相同的目录结构:

src/                          dist/
├── index.ts            →     ├── index.js        + index.js.map
├── config/                   ├── config/
│   ├── default.ts      →     │   ├── default.js
│   └── production.ts   →     │   └── production.js
├── routes/                   ├── routes/
│   └── users.ts        →     │   └── users.js
├── services/                 ├── services/
│   └── user.ts         →     │   └── user.js
├── plugins/                  ├── plugins/
│   └── redis.ts        →     │   └── redis.js
└── middlewares/              └── middlewares/
    └── auth.ts         →         └── auth.js

编译选项

选项默认值说明
Source Map开启(外部 .js.map错误堆栈映射回 TypeScript 行号
Minify关闭可选开启,减小产物体积
Targetnode18engines.node >= 18 对齐
FormatCJSCommonJS 输出,Node.js 稳定运行
Tree Shaking开启移除未使用的导出
Keep Names开启保留函数/类名称(错误堆栈可读性)

编译排除

生产编译自动排除以下文件:

  • *.d.ts — 类型声明
  • *.test.* / *.spec.* — 测试文件
  • __tests__/ — 测试目录
  • config/development.* — 开发环境配置
  • config/local.* — 本地覆盖配置
  • config/test.* — 测试环境配置

编译底层

vext build 基于 esbuild 实现,编译速度极快。典型项目(50+ 源文件)的编译时间在 1 秒以内

编译时会自动注入 process.env.NODE_ENV = "production",确保生产环境配置正确加载。

启动生产服务

直接启动

# 方式一:使用 vext start(推荐)
NODE_ENV=production vext start

# 方式二:直接 node 运行
NODE_ENV=production node dist/index.js

# 启用 Source Map 支持(错误堆栈显示 TypeScript 行号)
NODE_ENV=production node --enable-source-maps dist/index.js

vext start 会自动检测 dist/ 目录是否存在:

  • 存在 → 直接用 node 运行 dist/index.js(不依赖 tsx)
  • 不存在 → 使用 tsx 运行时编译 src/index.ts(开发模式回退)

环境变量

变量说明推荐值
NODE_ENV运行环境production
PORT监听端口3000
HOST监听地址0.0.0.0

Docker 部署

Dockerfile

# ── 阶段 1: 构建 ──────────────────────────────────────────
FROM node:22-alpine AS builder

WORKDIR /app

# 先复制依赖文件(利用 Docker 缓存层)
COPY package.json package-lock.json ./

# 安装全部依赖(包括 devDependencies,编译需要)
RUN npm ci

# 复制源码
COPY src/ src/
COPY tsconfig.json ./

# 编译
RUN npx vext build

# ── 阶段 2: 运行 ──────────────────────────────────────────
FROM node:22-alpine AS runner

WORKDIR /app

# 仅安装生产依赖
COPY package.json package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force

# 复制编译产物
COPY --from=builder /app/dist ./dist

# 非 root 用户运行
RUN addgroup --system --gid 1001 vext && \
    adduser --system --uid 1001 vext
USER vext

# 环境变量
ENV NODE_ENV=production
ENV PORT=3000

EXPOSE 3000

# 健康检查
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
  CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

# 启动
CMD ["node", "--enable-source-maps", "dist/index.js"]

.dockerignore

node_modules
dist
.vext
.git
*.md
test
website
.ai-memory
reports

Docker Compose

# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - '3000:3000'
    environment:
      - NODE_ENV=production
      - PORT=3000
      - MONGODB_URL=mongodb://mongo:27017/myapp
    depends_on:
      mongo:
        condition: service_healthy
    restart: unless-stopped
    deploy:
      resources:
        limits:
          memory: 512M
          cpus: '1.0'

  mongo:
    image: mongo:7
    ports:
      - '27017:27017'
    volumes:
      - mongo-data:/data/db
    healthcheck:
      test: echo 'db.runCommand("ping").ok' | mongosh --quiet
      interval: 10s
      timeout: 5s
      retries: 3

volumes:
  mongo-data:

构建和运行

# 构建镜像
docker build -t myapp:latest .

# 运行容器
docker run -d \
  --name myapp \
  -p 3000:3000 \
  -e NODE_ENV=production \
  -e MONGODB_URL=mongodb://host.docker.internal:27017/myapp \
  myapp:latest

# 查看日志
docker logs -f myapp

Nginx 反向代理

基础配置

# /etc/nginx/conf.d/myapp.conf

upstream vext_backend {
    server 127.0.0.1:3000;
    keepalive 64;
}

server {
    listen 80;
    server_name api.example.com;

    # 重定向到 HTTPS
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    server_name api.example.com;

    # SSL 证书
    ssl_certificate     /etc/letsencrypt/live/api.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.example.com/privkey.pem;

    # SSL 安全配置
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;

    # 请求体大小限制
    client_max_body_size 10M;

    # 代理到 VextJS
    location / {
        proxy_pass http://vext_backend;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection 'upgrade';
        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;
        proxy_set_header X-Request-ID $request_id;

        # 超时设置
        proxy_connect_timeout 10s;
        proxy_send_timeout 60s;
        proxy_read_timeout 60s;

        # 缓存
        proxy_cache_bypass $http_upgrade;
    }

    # 健康检查端点(不记录日志)
    location = /health {
        proxy_pass http://vext_backend;
        access_log off;
    }

    # 静态文件(如果有)
    location /static/ {
        alias /var/www/myapp/static/;
        expires 30d;
        add_header Cache-Control "public, no-transform";
    }
}

多实例负载均衡

upstream vext_backend {
    least_conn;
    server 127.0.0.1:3001;
    server 127.0.0.1:3002;
    server 127.0.0.1:3003;
    server 127.0.0.1:3004;
    keepalive 64;
}

PM2 进程管理

安装

npm install -g pm2

ecosystem 配置文件

// ecosystem.config.cjs
module.exports = {
  apps: [
    {
      name: 'myapp',
      script: 'dist/index.js',
      node_args: '--enable-source-maps',

      // 多实例(或使用 VextJS 内置 Cluster 模式)
      instances: 1,

      // 环境变量
      env: {
        NODE_ENV: 'production',
        PORT: 3000,
      },

      // 日志
      error_file: '/var/log/myapp/error.log',
      out_file: '/var/log/myapp/out.log',
      log_date_format: 'YYYY-MM-DD HH:mm:ss.SSS',
      merge_logs: true,

      // 自动重启
      max_restarts: 10,
      min_uptime: '10s',
      restart_delay: 5000,

      // 内存限制(超出则重启)
      max_memory_restart: '500M',

      // 优雅关闭
      kill_timeout: 10000,
      listen_timeout: 10000,
      shutdown_with_message: true,

      // 监控
      exp_backoff_restart_delay: 100,
    },
  ],
};

PM2 常用命令

# 启动
pm2 start ecosystem.config.cjs

# 查看状态
pm2 status

# 查看日志
pm2 logs myapp

# 重启
pm2 restart myapp

# 优雅重载
pm2 reload myapp

# 停止
pm2 stop myapp

# 监控面板
pm2 monit

# 设置开机自启
pm2 startup
pm2 save
VextJS Cluster vs PM2 Cluster

VextJS 内置了 Cluster 多进程模式(vext start --cluster),提供 Rolling Restart、心跳监控等高级功能。如果使用内置 Cluster,PM2 的 instances 设为 1 即可,让 VextJS 自己管理 worker 进程。

详见 Cluster 多进程

日志收集

JSON 日志格式

VextJS 在生产环境(NODE_ENV=production)下默认输出 JSON 格式日志,适合被日志收集系统解析:

{"level":30,"time":"2026-03-05T14:23:05.123Z","requestId":"abc-123","msg":"→ GET /api/users 200 45ms"}

配置日志级别

// src/config/production.ts
export default {
  logger: {
    level: 'info',    // 生产环境建议 info(不输出 debug)
    pretty: false,    // 生产环境禁用 pretty(默认行为)
  },
};

日志收集方案

方案一:文件 + Filebeat → ELK

# PM2 输出日志到文件
pm2 start ecosystem.config.cjs

# Filebeat 采集日志文件 → Elasticsearch → Kibana
# filebeat.yml
filebeat.inputs:
  - type: log
    paths:
      - /var/log/myapp/*.log
    json.keys_under_root: true
    json.overwrite_keys: true

output.elasticsearch:
  hosts: ['http://elasticsearch:9200']
  index: 'myapp-%{+yyyy.MM.dd}'

方案二:Docker 日志 → Loki

# docker-compose.yml
services:
  app:
    build: .
    logging:
      driver: loki
      options:
        loki-url: 'http://loki:3100/loki/api/v1/push'
        loki-batch-size: '400'

方案三:stdout → Cloud 原生

在 Kubernetes / AWS ECS / Cloud Run 等平台中,直接输出到 stdout,由平台自动收集:

# 不需要额外配置,JSON 日志直接输出到 stdout
NODE_ENV=production node dist/index.js

健康检查

实现健康检查端点

// src/routes/health.ts
import { defineRoutes } from 'vextjs';

export default defineRoutes((app) => {
  app.get('/health', {
    override: { rateLimit: false },
    docs: { summary: '健康检查', tags: ['System'] },
  }, async (req, res) => {
    const checks: Record<string, unknown> = {
      status: 'ok',
      uptime: process.uptime(),
      timestamp: new Date().toISOString(),
      memory: {
        rss: Math.round(process.memoryUsage().rss / 1024 / 1024) + 'MB',
        heap: Math.round(process.memoryUsage().heapUsed / 1024 / 1024) + 'MB',
      },
    };

    // 数据库连接检查
    if (app.db) {
      try {
        await app.db.client.db().admin().ping();
        checks.database = 'connected';
      } catch {
        checks.database = 'disconnected';
        checks.status = 'degraded';
      }
    }

    const statusCode = checks.status === 'ok' ? 200 : 503;
    res.json(checks, statusCode);
  });

  // 就绪检查(Kubernetes readinessProbe)
  app.get('/ready', {
    override: { rateLimit: false },
  }, async (req, res) => {
    // 检查所有关键依赖是否就绪
    const ready = app.db !== undefined;

    if (ready) {
      res.json({ status: 'ready' });
    } else {
      res.json({ status: 'not_ready' }, 503);
    }
  });
});

Kubernetes 探针配置

# k8s deployment.yaml
spec:
  containers:
    - name: myapp
      image: myapp:latest
      ports:
        - containerPort: 3000
      livenessProbe:
        httpGet:
          path: /health
          port: 3000
        initialDelaySeconds: 10
        periodSeconds: 30
        timeoutSeconds: 5
      readinessProbe:
        httpGet:
          path: /ready
          port: 3000
        initialDelaySeconds: 5
        periodSeconds: 10
        timeoutSeconds: 3
      resources:
        requests:
          memory: '128Mi'
          cpu: '250m'
        limits:
          memory: '512Mi'
          cpu: '1000m'

异常崩溃通知(onFatalError)

VextJS 内置了进程级异常捕获机制,当发生 uncaughtExceptionunhandledRejection 时,框架会:

  1. 记录 fatal 级别日志
  2. 调用用户配置的 onFatalError 回调(如有)
  3. 执行优雅关闭(onClose hooks 清理资源)
  4. process.exit(1) 退出进程

配置 onFatalError

shutdown 配置中添加 onFatalError 回调,接入告警通知:

// src/config/production.ts
export default {
  shutdown: {
    timeout: 10,
    onFatalError: async (error, origin) => {
      // origin: 'uncaughtException' | 'unhandledRejection'

      // 示例:发送钉钉 Webhook
      await fetch('https://oapi.dingtalk.com/robot/send?access_token=xxx', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          msgtype: 'markdown',
          markdown: {
            title: '⚠️ 服务异常',
            text: [
              '## ⚠️ 服务异常崩溃',
              `- **服务**: my-service`,
              `- **来源**: ${origin}`,
              `- **错误**: ${error.message}`,
              `- **时间**: ${new Date().toISOString()}`,
              `- **堆栈**:\n\`\`\`\n${error.stack}\n\`\`\``,
            ].join('\n'),
          },
        }),
      });
    },
  },
};

企业微信 Webhook 示例

onFatalError: async (error, origin) => {
  await fetch('https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      msgtype: 'markdown',
      markdown: {
        content: [
          `## <font color="warning">服务异常崩溃</font>`,
          `> 服务: my-service`,
          `> 来源: ${origin}`,
          `> 错误: ${error.message}`,
          `> 时间: ${new Date().toISOString()}`,
        ].join('\n'),
      },
    }),
  });
},

Slack Webhook 示例

onFatalError: async (error, origin) => {
  await fetch('https://hooks.slack.com/services/T00/B00/xxx', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      text: `🚨 *Service Crash* (${origin})\nError: ${error.message}\nTime: ${new Date().toISOString()}`,
    }),
  });
},

通用 HTTP Webhook 示例

onFatalError: async (error, origin) => {
  await fetch(process.env.ALERT_WEBHOOK_URL!, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      app: 'my-service',
      env: process.env.NODE_ENV,
      origin,
      error: error.message,
      stack: error.stack,
      hostname: require('os').hostname(),
      pid: process.pid,
      time: new Date().toISOString(),
    }),
  });
},

注意事项

项目说明
超时保护onFatalError 回调有 10 秒超时,超时后强制退出进程
错误隔离回调内部抛出的异常会被捕获并记录,不会阻止进程退出
不可恢复uncaughtException 后进程处于不确定状态,回调应尽量轻量(发通知即可)
测试模式_testMode 下不注册致命错误处理器,避免干扰测试
配合 PM2PM2 自身也有重启通知能力(pm2-slack 等插件),可与 onFatalError 配合使用
为什么不能用中间件实现?

uncaughtExceptionunhandledRejection 发生在 HTTP 中间件执行链之外(例如定时任务、事件监听器中的异常),中间件无法捕获这类错误。因此必须在框架 bootstrap 层注册 process 级事件监听器。

安全加固

生产环境清单

#检查项说明
1HTTPS通过 Nginx/CDN 终结 TLS,不在 Node.js 层处理 SSL
2CORS配置 config.cors 限制允许的来源域名
3限流配置 config.rateLimit,对登录等敏感接口设更严格限流
4Helmet通过中间件设置安全响应头(X-Frame-Options, CSP 等)
5环境变量敏感信息(数据库密码、API Key)通过环境变量传入,不写入配置文件
6config/local.ts确保 .gitignore 中包含 config/local.*
7日志不输出敏感数据(密码、token 等)到日志
8依赖审计定期 npm audit,及时修复已知漏洞
9非 rootDocker 容器中使用非 root 用户运行
10优雅关闭确保 SIGTERM 信号被正确处理(VextJS 内置支持)

环境变量管理

# .env(本地开发,不提交到 Git)
PORT=3000
MONGODB_URL=mongodb://localhost:27017/myapp
JWT_SECRET=local-dev-secret

# 生产环境通过 CI/CD 或密钥管理服务注入
# Docker: docker run -e MONGODB_URL=... myapp
# K8s: Secret + ConfigMap

性能优化

Node.js 参数

# 增大内存上限(默认 ~1.5GB)
node --max-old-space-size=4096 dist/index.js

# 启用 Source Map(推荐)
node --enable-source-maps dist/index.js

# 组合使用
node --enable-source-maps --max-old-space-size=4096 dist/index.js

Cluster 多进程

VextJS 内置 Cluster 模式,充分利用多核 CPU:

# 自动使用 CPU 核心数的 worker
vext start --cluster

# 指定 worker 数量
vext start --cluster --workers 4

详见 Cluster 多进程

连接池优化

// src/config/production.ts
export default {
  // HTTP fetch 连接优化
  fetch: {
    timeout: 5000,        // 生产环境缩短超时
    retry: 2,             // 幂等方法自动重试
  },

  // 数据库连接池
  database: {
    config: {
      url: process.env.MONGODB_URL,
      options: {
        maxPoolSize: 20,
        minPoolSize: 5,
        maxIdleTimeMS: 30000,
      },
    },
  },
};

部署流程建议

CI/CD 流水线

Push to main
  → CI 检查(lint + typecheck + test)
  → vext build(编译)
  → Docker build(构建镜像)
  → Push to Registry
  → Deploy(Rolling Update)
  → Health Check(验证)

灰度发布

# 1. 构建新版本镜像
docker build -t myapp:v1.2.0 .

# 2. 部署到灰度环境
docker run -d --name myapp-canary -p 3001:3000 myapp:v1.2.0

# 3. Nginx 灰度路由(10% 流量到新版本)
upstream vext_backend {
    server 127.0.0.1:3000 weight=9;   # 旧版本
    server 127.0.0.1:3001 weight=1;   # 新版本(灰度)
}

# 4. 观察监控指标(错误率、延迟)
# 5. 确认无异常后全量切换

监控告警

关键监控指标

指标正常范围告警条件
响应时间 P99< 500ms> 1s 持续 5 分钟
错误率(5xx)< 0.1%> 1% 持续 1 分钟
内存使用< 80% limit> 90% 持续 5 分钟
CPU 使用< 70%> 90% 持续 5 分钟
活跃连接数< 1000> 5000
数据库连接池无等待等待时间 > 1s

Prometheus 指标端点

结合 OpenTelemetry 接入示例 暴露 Prometheus 指标:

app.get('/metrics', {
  override: { rateLimit: false },
}, async (req, res) => {
  // OpenTelemetry Prometheus Exporter 会在此端点暴露指标
  // 详见 OpenTelemetry 接入示例
  res.json({ message: 'See /examples/opentelemetry for setup' });
});

下一步