开发与生产环境差异
本教程共 76 篇 · 第 66 篇 · 更新于 2026-07-25 · 约 7 分钟阅读
66. 开发与生产环境差异
本节目标:环境变量、配置管理和生产环境注意事项。
开发环境的代码能跑,不代表生产环境也能跑。我见过太多 “本地好好的,上线就崩” 的事故,根因往往是对环境差异缺乏认知。这一章我们把开发和生产的鸿沟填平。
NODE_ENV:一个被滥用的变量
NODE_ENV 是 Node.js 生态里最著名的环境变量,但它的用法一直存在争议。官方文档的态度很明确:
Node.js 本身不区分开发和生产环境。
NODE_ENV只是 npm 生态里某些库的约定。
很多教程教你这么写:
if (process.env.NODE_ENV === 'development') {
// 开发环境配置
}
if (process.env.NODE_ENV === 'production') {
// 生产环境配置
}
这看起来无害,实际上埋了雷:测试环境和 staging 环境走哪条分支?如果漏了判断,可能在测试通过但上线后行为不一致。官方甚至把 “给 NODE_ENV 设成非 production 的值” 视为一种反模式。
更稳妥的做法是:默认按生产环境配置,开发环境用特定开关覆盖。
const isDev = process.env.NODE_ENV === 'development';
const config = {
port: process.env.PORT || 3000,
logLevel: process.env.LOG_LEVEL || 'info',
db: {
host: process.env.DB_HOST,
poolSize: isDev ? 2 : 20,
},
// 生产默认值,开发按需覆盖
};
这样即使 NODE_ENV 没设或者设了奇怪的值,服务也能以安全的生产默认值启动。
配置管理:代码里不要写死配置
数据库地址、API 密钥、缓存地址,这些都不该出现在代码仓库里。推荐的做法是环境变量 + 配置文件 + 默认值三层结构:
import { config } from 'dotenv';
config(); // 加载 .env 文件(开发环境用)
const cfg = {
port: parseInt(process.env.PORT, 10) || 3000,
db: {
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT, 10) || 5432,
name: process.env.DB_NAME || 'myapp',
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
},
redis: {
url: process.env.REDIS_URL,
},
jwtSecret: process.env.JWT_SECRET,
};
// 启动时校验必填项
function validate() {
const required = ['DB_USER', 'DB_PASSWORD', 'JWT_SECRET'];
for (const key of required) {
if (!process.env[key]) {
throw new Error(`缺少必填环境变量: ${key}`);
}
}
}
validate();
export default cfg;
.env 文件只存在于开发环境,提交到 .gitignore 里。生产环境的变量由 CI/CD 或 K8s Secret 注入。
Warning永远不要把
.env文件提交到 Git。我见过有项目把生产数据库密码推到公开仓库,结果被爬虫扫到,数据全泄露。如果不小心提交了,立刻轮换密钥,不要只删文件了事。
错误处理:开发要详细,生产要安全
开发环境看到完整的错误堆栈,能快速定位 bug。生产环境暴露堆栈,等于把代码结构送给攻击者。
import express from 'express';
const app = express();
app.use((err, req, res, next) => {
// 1. 永远先记录完整错误
req.log.error({ err }, '请求处理异常');
// 2. 根据环境决定返回什么
if (process.env.NODE_ENV === 'development') {
res.status(500).json({
error: err.message,
stack: err.stack,
});
} else {
res.status(500).json({
error: 'Internal Server Error',
requestId: req.id, // 给用户一个 ID,方便客服根据日志定位
});
}
});
给用户一个 requestId,而不是技术细节。运维人员拿到这个 ID 去日志系统里一查,就能找到完整的堆栈和上下文。
优雅退出:不要突然掐断
容器编排平台(Kubernetes、Docker Swarm)在重启或缩容时,会先给进程发 SIGTERM 信号,等一段宽限期(默认 30 秒)后再发 SIGKILL。你的应用需要在这段时间里:
- 停止接收新请求(关闭 HTTP server)。
- 等现有请求处理完(或超时)。
- 关闭数据库连接、释放其他资源。
- 退出进程。
import http from 'node:http';
const server = http.createServer((req, res) => {
res.end('ok');
});
server.listen(3000, () => console.log('服务启动'));
let isShuttingDown = false;
function gracefulShutdown(signal) {
console.log(`收到 ${signal},开始优雅关闭`);
isShuttingDown = true;
// 停止接收新连接
server.close(() => {
console.log('HTTP server 已关闭');
// 关闭数据库连接等...
process.exit(0);
});
// 兜底:10 秒后强制退出
setTimeout(() => {
console.error('优雅关闭超时,强制退出');
process.exit(1);
}, 10000);
}
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
Tip如果你用了
cluster,主进程收到SIGTERM后要先通知所有 worker 关闭,等全部退出后再退出主进程。否则容器平台可能认为主进程已退出,但 worker 还在跑,导致连接被粗暴切断。
进程管理:开发用 nodemon,生产用 PM2/systemd
开发阶段文件一改就自动重启,用 nodemon 或 Node.js 内置的 --watch(v18+):
{
"scripts": {
"dev": "node --watch server.mjs"
}
}
生产环境不能靠 --watch,需要专业的进程管理:
- PM2:Node.js 生态最成熟的进程管理器,支持 cluster 模式、日志收集、自动重启、零停机部署。
- systemd:Linux 系统级方案,适合把 Node.js 服务当作系统守护进程运行。
- Docker + K8s:容器化方案,由编排平台负责进程生命周期。
PM2 配置示例:
// ecosystem.config.cjs
module.exports = {
apps: [{
name: 'api-server',
script: './server.mjs',
instances: 'max', // 用满所有 CPU 核
exec_mode: 'cluster', // cluster 模式
env: {
NODE_ENV: 'development',
PORT: 3000,
},
env_production: {
NODE_ENV: 'production',
PORT: 8080,
},
log_file: './logs/combined.log',
out_file: './logs/out.log',
error_file: './logs/error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
max_memory_restart: '500M', // 内存超 500MB 自动重启
}],
};
启动:pm2 start ecosystem.config.cjs --env production
开发 vs 生产差异速查表
| 维度 | 开发环境 | 生产环境 |
|---|---|---|
| 日志级别 | debug/trace | info/warn |
| 日志格式 | 人类可读,带颜色 | 结构化 JSON |
| 错误响应 | 完整堆栈 | 通用错误信息 + requestId |
| 代码监听 | nodemon / node --watch | PM2 / systemd / K8s |
| 进程数 | 1 | CPU 核数(cluster) |
| 数据库连接池 | 小(2~5) | 大(20~50) |
| 静态资源 | 本地文件 | CDN + 缓存头 |
| HTTPS | 本地自签名证书 | 正式证书(Let’s Encrypt/商业证书) |
| 调试端口 | --inspect 开着 | 关闭或限制 IP |
| 环境变量来源 | .env 文件 | K8s Secret / 云厂商参数 store |
12-Factor App 原则
Heroku 提出的 12-Factor App 方法论,对 Node.js 生产化实践非常有指导意义。和我们的主题最相关的几条:
- 配置存储在环境中:代码和配置严格分离,同一套代码可以在不同环境运行。
- 把后端服务当作附加资源:数据库、缓存、消息队列都是资源,通过 URL 配置,不假设本地存在。
- 进程无状态、可共享:进程内不保存 session、上传文件等,所有状态放外部存储。
- 通过进程模型扩展:用进程数扩展并发能力,而不是在进程里加线程。
- 快速启动、优雅终止:启动要快,方便扩容;退出要优雅,不丢请求。