部署
将 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 应用的部署方法:
- 生产配置:环境变量、安全中间件、性能优化
- PM2:进程管理、集群模式、零停机部署
- Docker:容器化部署、多阶段构建、Docker Compose
- Nginx:反向代理、负载均衡、SSL 终止
- 云平台:Heroku、Vercel、Railway、AWS
- 监控日志:健康检查、结构化日志、APM
- 性能优化:设置 NODE_ENV、避免同步函数、正确日志、压缩、缓存、连接池、集群
选择合适的部署方式,并遵循最佳实践,确保应用的稳定运行。
练习
- 使用 PM2 配置一个集群模式的 Express 应用,实现零停机部署
- 为 Express 应用编写 Dockerfile,使用多阶段构建优化镜像大小
- 配置 Nginx 反向代理,实现 HTTPS 和负载均衡
- 实现健康检查端点,包含数据库和 Redis 连接状态
- 编写一个自动化部署脚本,包含备份、部署、验证步骤