将 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)显示部署后无新问题
- 关键指标(请求延迟、错误率)在仪表板上显示正常
每次部署前执行此清单,可以捕获导致部署后事故的最常见原因。