跳到主要内容

部署

将 Express 应用部署到生产环境是开发流程的最后一步。正确的部署配置和运维实践对应用的稳定性、性能和安全性至关重要。本章将详细介绍 Express 应用的部署方法和生产环境最佳实践。

生产环境准备

环境变量管理

生产环境的配置应该通过环境变量管理,而不是硬编码在代码中。

# .env.production
NODE_ENV=production
PORT=3000

# 数据库
MONGODB_URI=mongodb://user:password@mongo:27017/myapp
REDIS_URL=redis://redis:6379

# 安全
JWT_SECRET=your-super-secret-key-at-least-32-characters
JWT_EXPIRES_IN=7d

# 日志
LOG_LEVEL=info
// config/index.js
require('dotenv').config();

module.exports = {
nodeEnv: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT, 10) || 3000,

database: {
uri: process.env.MONGODB_URI,
options: {
maxPoolSize: 50,
minPoolSize: 5
}
},

redis: {
url: process.env.REDIS_URL
},

jwt: {
secret: process.env.JWT_SECRET,
expiresIn: process.env.JWT_EXPIRES_IN || '7d'
},

logs: {
level: process.env.LOG_LEVEL || 'info'
}
};

生产配置最佳实践

// app.js
const express = require('express');
const helmet = require('helmet');
const compression = require('compression');
const rateLimit = require('express-rate-limit');
const mongoSanitize = require('express-mongo-sanitize');
const xss = require('xss-clean');
const hpp = require('hpp');
const cors = require('cors');

const app = express();

// 信任代理(如果应用在反向代理后面)
app.set('trust proxy', 1);

// 安全中间件
app.use(helmet());

// CORS 配置
app.use(cors({
origin: process.env.ALLOWED_ORIGINS?.split(','),
credentials: true
}));

// Gzip 压缩
app.use(compression());

// 请求体限制
app.use(express.json({ limit: '10kb' }));
app.use(express.urlencoded({ extended: true, limit: '10kb' }));

// 数据净化
app.use(mongoSanitize()); // 防止 NoSQL 注入
app.use(xss()); // 防止 XSS

// 防止参数污染
app.use(hpp({
whitelist: ['price', 'rating', 'category'] // 允许重复的参数
}));

// 限流
const limiter = rateLimit({
windowMs: 15 * 60 * 1000,
max: 100,
message: { error: '请求过于频繁,请稍后再试' }
});
app.use('/api', limiter);

// 路由
app.use('/api', require('./routes'));

// 404 处理
app.use((req, res) => {
res.status(404).json({ error: '路由不存在' });
});

// 错误处理
app.use(require('./middleware/error'));

module.exports = app;

PM2 进程管理

PM2 是 Node.js 应用最流行的进程管理器,提供进程守护、负载均衡、日志管理等功能。

安装 PM2

npm install -g pm2

PM2 配置文件

创建 ecosystem.config.js

module.exports = {
apps: [
{
name: 'express-app',
script: './src/app.js',

// 集群模式
instances: 'max', // 使用所有 CPU 核心
exec_mode: 'cluster', // 集群模式

// 自动重启
autorestart: true,
watch: false, // 生产环境不建议开启
max_memory_restart: '1G',

// 重启策略
exp_backoff_restart_delay: 100,
max_restarts: 10,
min_uptime: '10s',

// 环境变量
env: {
NODE_ENV: 'development',
PORT: 3000
},
env_production: {
NODE_ENV: 'production',
PORT: 3000
},

// 日志配置
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
error_file: './logs/error.log',
out_file: './logs/out.log',
merge_logs: true,

// 定时重启(可选)
cron_restart: '0 3 * * *' // 每天凌晨 3 点重启
}
]
};

PM2 常用命令

# 启动应用
pm2 start ecosystem.config.js --env production

# 停止应用
pm2 stop express-app

# 重启应用
pm2 restart express-app

# 重载(零停机)
pm2 reload express-app

# 查看状态
pm2 status

# 查看日志
pm2 logs express-app
pm2 logs --lines 100

# 实时监控
pm2 monit

# 保存当前进程列表
pm2 save

# 设置开机自启
pm2 startup

# 清除日志
pm2 flush

# 重置重启计数
pm2 reset express-app

零停机部署

使用 pm2 reload 实现零停机部署:

# 部署脚本
#!/bin/bash
set -e

echo "Pulling latest code..."
git pull origin main

echo "Installing dependencies..."
npm ci --production

echo "Building assets..."
npm run build

echo "Reloading application..."
pm2 reload ecosystem.config.js --env production

echo "Deployment complete!"
pm2 status

Docker 部署

Docker 提供了标准化的部署环境,确保应用在任何服务器上运行一致。

Dockerfile

# 构建阶段
FROM node:20-alpine AS builder

WORKDIR /app

# 复制依赖文件
COPY package*.json ./

# 安装所有依赖(包括 devDependencies)
RUN npm ci

# 复制源代码
COPY . .

# 构建(如果有构建步骤)
RUN npm run build

# 生产阶段
FROM node:20-alpine AS production

WORKDIR /app

# 设置 Node.js 生产环境
ENV NODE_ENV=production

# 创建非 root 用户
RUN addgroup -g 1001 -S nodejs && \
adduser -S express -u 1001

# 复制依赖文件
COPY package*.json ./

# 只安装生产依赖
RUN npm ci --only=production && \
npm cache clean --force

# 从构建阶段复制构建产物
COPY --from=builder --chown=express:nodejs /app/dist ./dist
COPY --from=builder --chown=express:nodejs /app/public ./public

# 切换到非 root 用户
USER express

# 暴露端口
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", "dist/app.js"]

Docker Compose

# docker-compose.yml
version: '3.8'

services:
app:
build:
context: .
target: production
container_name: express-app
restart: always
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- MONGODB_URI=mongodb://mongo:27017/myapp
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
depends_on:
mongo:
condition: service_healthy
redis:
condition: service_started
networks:
- app-network
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s

mongo:
image: mongo:7
container_name: mongo
restart: always
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
volumes:
- mongo_data:/data/db
- mongo_config:/data/configdb
networks:
- app-network
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongosh localhost:27017/test --quiet
interval: 10s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
container_name: redis
restart: always
command: redis-server --appendonly yes
volumes:
- redis_data:/data
networks:
- app-network

nginx:
image: nginx:alpine
container_name: nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
depends_on:
- app
networks:
- app-network

networks:
app-network:
driver: bridge

volumes:
mongo_data:
mongo_config:
redis_data:

Docker 常用命令

# 构建镜像
docker build -t express-app:latest .

# 运行容器
docker run -d -p 3000:3000 --name express-app express-app:latest

# 使用 Docker Compose
docker-compose up -d
docker-compose up -d --build # 重新构建

# 查看日志
docker-compose logs -f app

# 进入容器
docker-compose exec app sh

# 停止服务
docker-compose down
docker-compose down -v # 同时删除卷

# 清理无用资源
docker system prune -a

Nginx 反向代理

Nginx 作为反向代理,提供负载均衡、SSL 终止、静态文件服务等功能。

Nginx 配置

# nginx/nginx.conf
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;

events {
worker_connections 1024;
use epoll;
multi_accept on;
}

http {
include /etc/nginx/mime.types;
default_type application/octet-stream;

# 日志格式
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';

access_log /var/log/nginx/access.log main;

# 性能优化
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
types_hash_max_size 2048;

# Gzip 压缩
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml application/json application/javascript
application/xml application/xml+rss text/javascript;

# 上游服务器
upstream express_app {
least_conn;
server app:3000 weight=5;
keepalive 32;
}

# HTTP 重定向到 HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$server_name$request_uri;
}

# HTTPS 服务器
server {
listen 443 ssl http2;
server_name example.com www.example.com;

# SSL 配置
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;

# 现代 SSL 配置
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;

# HSTS
add_header Strict-Transport-Security "max-age=63072000" always;

# 安全头
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;

# API 代理
location /api {
proxy_pass http://express_app;
proxy_http_version 1.1;

# 请求头
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_set_header Connection "";

# 超时配置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;

# 缓冲
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}

# 静态文件
location /static {
alias /app/public;
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}

# 健康检查
location /health {
proxy_pass http://express_app;
access_log off;
}

# 错误页面
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
}

负载均衡策略

upstream express_app {
# 轮询(默认)
# server app1:3000;
# server app2:3000;
# server app3:3000;

# 最少连接
least_conn;
server app1:3000;
server app2:3000;
server app3:3000;

# IP 哈希(会话保持)
# ip_hash;
# server app1:3000;
# server app2:3000;

# 权重
# server app1:3000 weight=3;
# server app2:3000 weight=2;
# server app3:3000 weight=1;

# 健康检查
# server app1:3000 max_fails=3 fail_timeout=30s;
# server app2:3000 backup; # 备用服务器
}

云平台部署

Heroku 部署

# 安装 Heroku CLI
npm install -g heroku

# 登录
heroku login

# 创建应用
heroku create my-express-app

# 添加构建包
heroku buildpacks:set heroku/nodejs

# 配置环境变量
heroku config:set MONGODB_URI=mongodb://...
heroku config:set JWT_SECRET=your-secret
heroku config:set NODE_ENV=production

# 部署
git push heroku main

# 查看日志
heroku logs --tail

# 扩容
heroku ps:scale web=2

Procfile

web: node src/app.js

Vercel 部署

vercel.json

{
"version": 2,
"builds": [
{
"src": "src/app.js",
"use": "@vercel/node"
}
],
"routes": [
{
"src": "/(.*)",
"dest": "src/app.js"
}
],
"env": {
"MONGODB_URI": "@mongodb_uri",
"JWT_SECRET": "@jwt_secret"
}
}

Railway 部署

# 安装 Railway CLI
npm install -g @railway/cli

# 登录
railway login

# 初始化项目
railway init

# 添加服务
railway add

# 部署
railway up

# 查看日志
railway logs

AWS EC2 部署

# 连接 EC2 实例
ssh -i your-key.pem ubuntu@your-ec2-instance

# 安装 Node.js
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt-get install -y nodejs

# 安装 PM2
sudo npm install -g pm2

# 克隆代码
git clone https://github.com/your-repo/express-app.git
cd express-app

# 安装依赖
npm ci --production

# 启动应用
pm2 start ecosystem.config.js --env production

# 设置开机自启
pm2 startup
pm2 save

# 安装 Nginx
sudo apt update
sudo apt install nginx

# 配置 Nginx
sudo nano /etc/nginx/sites-available/express-app
sudo ln -s /etc/nginx/sites-available/express-app /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl restart nginx

# 安装 SSL 证书(Let's Encrypt)
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com

监控与日志

健康检查端点

// routes/health.js
const router = require('express').Router();

router.get('/health', async (req, res) => {
const health = {
uptime: process.uptime(),
timestamp: Date.now(),
status: 'healthy',
checks: {
database: await checkDatabase(),
redis: await checkRedis()
}
};

const isHealthy = Object.values(health.checks).every(c => c.status === 'ok');

res.status(isHealthy ? 200 : 503).json(health);
});

async function checkDatabase() {
try {
await mongoose.connection.db.admin().ping();
return { status: 'ok' };
} catch (err) {
return { status: 'error', message: err.message };
}
}

async function checkRedis() {
try {
await redisClient.ping();
return { status: 'ok' };
} catch (err) {
return { status: 'error', message: err.message };
}
}

module.exports = router;

日志管理

使用 Winston 进行结构化日志:

// utils/logger.js
const winston = require('winston');
const { combine, timestamp, printf, json } = winston.format;

const logFormat = printf(({ level, message, timestamp, ...metadata }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(metadata).length > 0) {
msg += ` ${JSON.stringify(metadata)}`;
}
return msg;
});

const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(
timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
logFormat
),
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;

进程监控

// 监控未捕获的异常
process.on('uncaughtException', (err) => {
logger.error('Uncaught Exception:', err);
process.exit(1);
});

// 监控未处理的 Promise 拒绝
process.on('unhandledRejection', (reason, promise) => {
logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
});

// 优雅关闭
const gracefulShutdown = () => {
logger.info('Received shutdown signal. Closing connections...');

server.close(() => {
logger.info('HTTP server closed');

mongoose.connection.close(false).then(() => {
logger.info('MongoDB connection closed');
process.exit(0);
});
});

// 强制退出超时
setTimeout(() => {
logger.error('Forced shutdown after timeout');
process.exit(1);
}, 10000);
};

process.on('SIGTERM', gracefulShutdown);
process.on('SIGINT', gracefulShutdown);

APM 监控

使用 New Relic 或 Elastic APM 进行应用性能监控:

// 安装
npm install newrelic

// 应用入口添加
require('newrelic');

性能优化

1. 设置 NODE_ENV 为 production

这是提升性能最简单也最重要的配置。将 NODE_ENV 设置为 production 会使 Express:

  • 缓存视图模板
  • 缓存从 CSS 扩展生成的 CSS 文件
  • 生成更简洁的错误信息

测试表明,仅此一项就可以将性能提升三倍!

// 检查环境
if (process.env.NODE_ENV !== 'production') {
console.warn('警告:应用未运行在生产模式');
}

// 在 systemd 中设置
// /etc/systemd/system/myservice.service
// Environment=NODE_ENV=production

2. 避免使用同步函数

同步函数会阻塞事件循环,在高流量网站中会严重影响性能。在生产环境中,始终使用异步版本的函数。

// 错误:使用同步函数
const data = fs.readFileSync('/path/to/file', 'utf8');

// 正确:使用异步函数
fs.readFile('/path/to/file', 'utf8', (err, data) => {
if (err) throw err;
console.log(data);
});

// 或使用 Promise 版本
const data = await fs.promises.readFile('/path/to/file', 'utf8');

可以使用 --trace-sync-io 命令行标志来检测代码中是否使用了同步 API:

node --trace-sync-io app.js

3. 正确的日志记录

console.log()console.error() 在输出到终端或文件时是同步的,不适合在生产环境使用。

调试日志:使用 debug 模块

npm install debug
const debug = require('debug')('app:routes');

debug('用户请求: %O', req.body);

// 通过环境变量控制输出
// DEBUG=app:* node app.js

应用活动日志:使用 Pino(性能最快的日志库)

npm install pino
const pino = require('pino');
const logger = pino({
level: process.env.LOG_LEVEL || 'info'
});

logger.info('用户登录', { userId: 123 });
logger.error({ err }, '请求处理失败');

4. 启用压缩

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 才压缩
}));

注意:对于高流量网站,最好在反向代理层面(如 Nginx)实现压缩,而不是在应用层面。

5. 静态文件缓存

app.use(express.static('public', {
maxAge: '1y', // 缓存 1 年
immutable: true, // 资源不可变
etag: true, // 启用 ETag
lastModified: true // 启用 Last-Modified
}));

6. 数据库连接池

await mongoose.connect(uri, {
maxPoolSize: 50,
minPoolSize: 5,
socketTimeoutMS: 45000,
serverSelectionTimeoutMS: 5000
});

7. Redis 缓存

const cacheMiddleware = (key, ttl = 3600) => {
return async (req, res, next) => {
const cacheKey = `${key}:${req.originalUrl}`;

try {
const cached = await redis.get(cacheKey);
if (cached) {
return res.json(JSON.parse(cached));
}

const originalJson = res.json.bind(res);
res.json = (data) => {
redis.setex(cacheKey, ttl, JSON.stringify(data));
return originalJson(data);
};

next();
} catch (err) {
next();
}
};
};

app.get('/api/products', cacheMiddleware('products', 300), getProducts);

8. 集群模式

利用多核 CPU:

// cluster.js
const cluster = require('cluster');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);

// Fork workers
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

cluster.on('exit', (worker, code, signal) => {
console.log(`Worker ${worker.process.pid} died`);
cluster.fork(); // 重启 worker
});
} else {
require('./app'); // 启动应用

console.log(`Worker ${process.pid} started`);
}

部署检查清单

部署前

  • 所有测试通过
  • 代码审查完成
  • 环境变量配置正确
  • 数据库备份完成
  • 回滚方案准备就绪
  • 运行 npm audit 检查依赖漏洞
  • 确认 NODE_ENV 设置为 production

部署时

  • 使用零停机部署
  • 监控部署过程
  • 验证健康检查端点
  • 检查日志无错误

部署后

  • 功能验证测试
  • 性能指标正常
  • 错误率监控
  • 用户反馈收集

小结

本章详细介绍了 Express 应用的部署方法:

  1. 生产配置:环境变量、安全中间件、性能优化
  2. PM2:进程管理、集群模式、零停机部署
  3. Docker:容器化部署、多阶段构建、Docker Compose
  4. Nginx:反向代理、负载均衡、SSL 终止
  5. 云平台:Heroku、Vercel、Railway、AWS
  6. 监控日志:健康检查、结构化日志、APM
  7. 性能优化:设置 NODE_ENV、避免同步函数、正确日志、压缩、缓存、连接池、集群

选择合适的部署方式,并遵循最佳实践,确保应用的稳定运行。

练习

  1. 使用 PM2 配置一个集群模式的 Express 应用,实现零停机部署
  2. 为 Express 应用编写 Dockerfile,使用多阶段构建优化镜像大小
  3. 配置 Nginx 反向代理,实现 HTTPS 和负载均衡
  4. 实现健康检查端点,包含数据库和 Redis 连接状态
  5. 编写一个自动化部署脚本,包含备份、部署、验证步骤

参考资料