文章

Node.js 生产环境部署清单(2025版)

一份经过实战检验的 Node.js 应用生产部署清单,涵盖环境配置、进程管理、健康检查、日志记录、安全加固和零停机部署等关键环节。

将 Node.js 部署到生产环境远不止 git push 这么简单。这份清单涵盖了团队在发布后前 90 天内常因忽略而导致宕机、数据丢失和安全事故的关键步骤。

1. 环境配置

切勿硬编码密钥。 对每个凭证、API 密钥、连接字符串和功能开关都使用环境变量。

# .env(切勿提交此文件)
DATABASE_URL=postgresql://user:password@host:5432/dbname
SESSION_SECRET=a-long-random-string-at-least-32-chars
STRIPE_SECRET_KEY=sk_live_...

在启动时使用验证库,以便在缺少配置时快速失败:

import { z } from "zod";

const env = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  SESSION_SECRET: z.string().min(32),
}).parse(process.env);

export default env;

如果应用启动时缺少必需的环境变量,你会在启动时看到清晰的错误信息,而不是在运行时遇到难以排查的故障。

2. 进程管理

使用进程管理器——切勿在生产环境中直接运行 node server.js。裸 Node 进程会在未捕获异常时挂掉,且不会自动重启。

PM2(VPS / 裸金属)

pnpm add -g pm2

# ecosystem.config.cjs
module.exports = {
  apps: [{
    name: "api",
    script: "dist/server.js",
    instances: "max",        // 集群模式:每个 CPU 一个 worker
    exec_mode: "cluster",
    max_memory_restart: "512M",
    env_production: {
      NODE_ENV: "production",
      PORT: 3000
    }
  }]
};

pm2 start ecosystem.config.cjs --env production
pm2 save
pm2 startup  // 生成操作系统启动命令

Docker + 容器编排

FROM node:22-alpine AS builder
WORKDIR /app
COPY package*.json pnpm-lock.yaml ./
RUN corepack enable && pnpm install --frozen-lockfile
COPY . .
RUN pnpm build

FROM node:22-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY package.json .
EXPOSE 3000
USER node
CMD ["node", "dist/server.js"]

3. 健康检查

每个 Node.js 服务都需要一个 /health 端点,供负载均衡器和编排器轮询:

app.get("/health", async (req, res) => {
  try {
    await db.raw("SELECT 1"); // 验证数据库连接
    res.json({ status: "ok", uptime: process.uptime() });
  } catch (err) {
    res.status(503).json({ status: "error", message: "database unreachable" });
  }
});

配置你的负载均衡器或 Kubernetes 存活探针,每 10-30 秒调用此端点,并自动移除不健康的实例。

4. 结构化日志

在生产环境中避免使用 console.log。使用输出 JSON 的结构化日志记录器,便于在日志聚合工具中查询。

pnpm add pino pino-pretty
import pino from "pino";

export const logger = pino({
  level: process.env.LOG_LEVEL ?? "info",
  ...(process.env.NODE_ENV !== "production" && {
    transport: { target: "pino-pretty" }
  })
});

// 使用示例
logger.info({ userId: req.user.id, action: "login" }, "User authenticated");
logger.error({ err, requestId }, "Unhandled error in payment webhook");

将日志发送到集中式服务(Datadog、Logtail、Papertrail、CloudWatch)。切勿依赖临时容器上的磁盘日志。

5. 错误处理

在每个边界处处理错误:

// 异步路由处理包装器——防止未捕获的拒绝
const asyncHandler = (fn: RequestHandler): RequestHandler =>
  (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

// 全局错误处理器
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
  logger.error({ err, url: req.url, method: req.method }, "Request failed");

  if (res.headersSent) return next(err);

  const status = err instanceof HttpError ? err.status : 500;
  res.status(status).json({
    error: process.env.NODE_ENV === "production" ? "Internal server error" : err.message
  });
});

// 进程级安全网
process.on("uncaughtException", (err) => {
  logger.fatal({ err }, "Uncaught exception — shutting down");
  process.exit(1);
});

process.on("unhandledRejection", (reason) => {
  logger.fatal({ reason }, "Unhandled rejection — shutting down");
  process.exit(1);
});

6. 优雅关闭

处理 SIGTERM 信号,确保正在处理的请求在进程退出前完成:

const server = app.listen(env.PORT);

const shutdown = async (signal: string) => {
  logger.info({ signal }, "Shutdown signal received");

  server.close(async () => {
    await db.destroy();  // 关闭数据库连接池
    logger.info("Server closed cleanly");
    process.exit(0);
  });

  // 如果清理过程挂起,30 秒后强制退出
  setTimeout(() => process.exit(1), 30_000);
};

process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => shutdown("SIGINT"));

Kubernetes 在杀死 Pod 前会发送 SIGTERM。没有这个处理,连接会在请求中途被断开。

7. 安全加固

pnpm add helmet compression express-rate-limit
import helmet from "helmet";
import compression from "compression";
import rateLimit from "express-rate-limit";

app.use(helmet());           // 设置安全的 HTTP 头
app.use(compression());      // gzip 压缩响应

app.use("/api", rateLimit({
  windowMs: 15 * 60 * 1000, // 15 分钟
  max: 100,
  standardHeaders: true,
  legacyHeaders: false
}));

在 CI 中运行 pnpm audit,并在发现高危/严重漏洞时阻止构建:

# .github/workflows/security.yml
- name: Audit dependencies
  run: pnpm audit --audit-level=high

8. 数据库连接池

切勿为每个请求创建新的数据库连接。根据实例大小配置连接池:

// Postgres with pg (node-postgres)
import { Pool } from "pg";

export const pool = new Pool({
  connectionString: env.DATABASE_URL,
  max: 20,              // 连接池最大连接数
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 2_000,
  ssl: env.NODE_ENV === "production" ? { rejectUnauthorized: false } : false
});
// Prisma — 在 DATABASE_URL 中设置 pool_timeout
// postgresql://user:pass@host/db?connection_limit=10&pool_timeout=10

9. 零停机部署

使用 PM2 集群模式

pm2 reload ecosystem.config.cjs --env production

PM2 会逐个重启 worker,同时保持其他 worker 正常运行。

使用 Docker / Kubernetes

使用滚动更新——Kubernetes 默认配置:

strategy:
  type: RollingUpdate
  rollingUpdate:
    maxSurge: 1
    maxUnavailable: 0  // 绝不降低低于期望的容量

在 Cloudflare / Railway / Fly.io 上蓝绿部署

这些平台会在 git push 时自动处理零停机部署,无需额外配置。

10. 部署前检查清单

每次生产发布前:

  • pnpm audit 通过(无高危/严重漏洞)
  • TypeScript 编译无错误(tsc --noEmit
  • 所有测试通过(pnpm test
  • 数据库迁移已审查且可回滚
  • 新的环境变量已记录并在生产环境中设置
  • NODE_ENV=production 已设置
  • 部署后健康端点返回 200
  • 错误跟踪(Sentry)显示部署后无新问题
  • 关键指标(请求延迟、错误率)在仪表板上显示正常

每次部署前执行此清单,可以捕获导致部署后事故的最常见原因。

常见问题

生产环境中 Node.js 应该用 PM2 还是 Docker?
如果需要可移植、可复现的环境并部署到 Kubernetes 或容器平台,请使用 Docker。如果在裸机 VPS 上运行并希望轻量级进程管理和集群功能,请使用 PM2。许多团队在 Docker 容器内同时运行 PM2,以兼顾进程管理和容器化优势。
应该启动多少个 Node.js 集群工作进程?
建议从 os.cpus().length - 1 个 worker 开始,留一个核心给操作系统和监控代理。对于 I/O 密集型工作负载(大多数 HTTP API),即使 2-4 个 worker 也能显著提升吞吐量。在过度配置之前,请在实际负载下进行性能分析。
Node.js 生产环境中处理未捕获异常的最佳方式是什么?
记录包含完整堆栈跟踪的错误,然后退出进程,让进程管理器重新启动它。在未捕获异常后尝试继续运行会使进程处于未知状态。仅使用 process.on('uncaughtException') 记录日志,然后调用 process.exit(1)。