部署
将 Node.js 应用部署到生产环境需要考虑性能、安全、可靠性等多个方面。本章介绍常见的部署方式和最佳实践。
生产环境准备
环境变量
使用环境变量管理配置:
// config.js
const config = {
port: parseInt(process.env.PORT) || 3000,
nodeEnv: process.env.NODE_ENV || 'development',
database: {
url: process.env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE) || 10,
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d',
},
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
},
};
// 验证必要的环境变量
const requiredEnvVars = ['DATABASE_URL', 'JWT_SECRET'];
requiredEnvVars.forEach(envVar => {
if (!process.env[envVar]) {
throw new Error(`缺少环境变量: ${envVar}`);
}
});
module.exports = config;
日志管理
// logger.js
const winston = require('winston');
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'my-app' },
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' }),
],
});
// 开发环境输出到控制台
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.simple(),
}));
}
module.exports = logger;
健康检查
// health.js
const express = require('express');
const router = express.Router();
router.get('/health', async (req, res) => {
const health = {
status: 'ok',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
};
res.json(health);
});
router.get('/ready', async (req, res) => {
try {
// 检查数据库连接
await checkDatabase();
// 检查 Redis 连接
await checkRedis();
res.json({ status: 'ready' });
} catch (err) {
res.status(503).json({ status: 'not ready', error: err.message });
}
});
router.get('/live', (req, res) => {
res.json({ status: 'alive' });
});
module.exports = router;
进程管理
PM2
PM2 是最流行的 Node.js 进程管理工具。
# 安装
npm install -g pm2
# 启动应用
pm2 start app.js
# 使用配置文件启动
pm2 start ecosystem.config.js
# 查看状态
pm2 status
# 查看日志
pm2 logs
# 监控
pm2 monit
# 重启
pm2 restart all
# 停止
pm2 stop all
# 保存进程列表
pm2 save
# 开机自启
pm2 startup
配置文件 ecosystem.config.js:
module.exports = {
apps: [{
name: 'my-app',
script: './app.js',
instances: 'max', // 或指定数量
exec_mode: 'cluster',
autorestart: true,
watch: false,
max_memory_restart: '1G',
env: {
NODE_ENV: 'development',
},
env_production: {
NODE_ENV: 'production',
},
error_file: './logs/error.log',
out_file: './logs/out.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss',
}]
};
Systemd
创建系统服务:
# /etc/systemd/system/nodeapp.service
[Unit]
Description=Node.js Application
After=network.target
[Service]
Type=simple
User=www-data
Group=www-data
WorkingDirectory=/var/www/nodeapp
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node /var/www/nodeapp/app.js
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
管理服务:
# 重载配置
sudo systemctl daemon-reload
# 启动
sudo systemctl start nodeapp
# 停止
sudo systemctl stop nodeapp
# 重启
sudo systemctl restart nodeapp
# 开机自启
sudo systemctl enable nodeapp
# 查看状态
sudo systemctl status nodeapp
# 查看日志
sudo journalctl -u nodeapp -f
Docker 部署
Dockerfile
# 构建阶段
FROM node:20-alpine AS builder
WORKDIR /app
# 安装依赖
COPY package*.json ./
RUN npm ci --only=production
# 复制源码
COPY . .
# 最终阶段
FROM node:20-alpine
WORKDIR /app
# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodeapp -u 1001
# 复制依赖
COPY --from=builder --chown=nodeapp:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodeapp:nodejs /app/package*.json ./
COPY --from=builder --chown=nodeapp:nodejs /app ./
USER nodeapp
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "app.js"]
Docker Compose
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@db:5432/myapp
- REDIS_URL=redis://redis:6379
depends_on:
- db
- redis
restart: always
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
db:
image: postgres:15-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=myapp
volumes:
- postgres_data:/var/lib/postgresql/data
restart: always
redis:
image: redis:7-alpine
restart: always
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
- ./certs:/etc/nginx/certs:ro
depends_on:
- app
restart: always
volumes:
postgres_data:
Nginx 反向代理
基本配置
# /etc/nginx/conf.d/nodeapp.conf
upstream nodeapp {
server 127.0.0.1:3000;
keepalive 64;
}
server {
listen 80;
server_name example.com;
# 重定向到 HTTPS
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name example.com;
# SSL 配置
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
location / {
proxy_pass http://nodeapp;
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_cache_bypass $http_upgrade;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# 静态文件
location /static/ {
alias /var/www/nodeapp/public/;
expires 1y;
add_header Cache-Control "public, immutable";
}
}
负载均衡
upstream nodeapp {
least_conn; # 最少连接算法
server 127.0.0.1:3000 weight=3;
server 127.0.0.1:3001 weight=2;
server 127.0.0.1:3002 weight=1;
keepalive 64;
}
性能优化
启用压缩
const compression = require('compression');
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
threshold: 1024, // 超过 1KB 才压缩
}));
集群模式
const cluster = require('node:cluster');
const os = require('node:os');
if (cluster.isPrimary) {
const numCPUs = os.cpus().length;
console.log(`主进程 ${process.pid} 正在运行`);
// Fork 工作进程
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log(`工作进程 ${worker.process.pid} 已退出`);
cluster.fork(); // 重启工作进程
});
} else {
// 工作进程启动应用
require('./app');
console.log(`工作进程 ${process.pid} 已启动`);
}
缓存
const redis = require('redis');
const client = redis.createClient(process.env.REDIS_URL);
// 缓存中间件
function cacheMiddleware(duration) {
return async (req, res, next) => {
const key = `cache:${req.originalUrl}`;
try {
const cached = await client.get(key);
if (cached) {
return res.json(JSON.parse(cached));
}
// 保存原始 json 方法
const originalJson = res.json.bind(res);
// 重写 json 方法
res.json = async (data) => {
await client.setEx(key, duration, JSON.stringify(data));
return originalJson(data);
};
next();
} catch (err) {
next();
}
};
}
// 使用
app.get('/api/data', cacheMiddleware(3600), (req, res) => {
res.json({ data: '...' });
});
监控
Prometheus + Grafana
const promClient = require('prom-client');
// 启用默认指标
promClient.collectDefaultMetrics();
// 自定义指标
const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration in seconds',
labelNames: ['method', 'route', 'status'],
buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
});
// 中间件
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
end({ method: req.method, route: req.path, status: res.statusCode });
});
next();
});
// 暴露指标
app.get('/metrics', async (req, res) => {
res.set('Content-Type', promClient.register.contentType);
res.send(await promClient.register.metrics());
});
小结
本章我们学习了:
- 生产环境准备:环境变量、日志、健康检查
- 进程管理:PM2、Systemd
- Docker 部署:Dockerfile、Docker Compose
- Nginx 反向代理:配置、负载均衡
- 性能优化:压缩、集群、缓存
- 监控:Prometheus、Grafana
练习
- 使用 PM2 部署一个 Node.js 应用
- 编写 Dockerfile 和 docker-compose.yml 部署应用
- 配置 Nginx 反向代理和 HTTPS
- 添加 Prometheus 监控指标