跳到主要内容

部署

将 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());
});

小结

本章我们学习了:

  1. 生产环境准备:环境变量、日志、健康检查
  2. 进程管理:PM2、Systemd
  3. Docker 部署:Dockerfile、Docker Compose
  4. Nginx 反向代理:配置、负载均衡
  5. 性能优化:压缩、集群、缓存
  6. 监控:Prometheus、Grafana

练习

  1. 使用 PM2 部署一个 Node.js 应用
  2. 编写 Dockerfile 和 docker-compose.yml 部署应用
  3. 配置 Nginx 反向代理和 HTTPS
  4. 添加 Prometheus 监控指标