部署
概述
将 Node.js 应用程序部署到生产环境需要仔细考虑环境配置、进程管理、监控和扩展策略。本章介绍部署最佳实践、容器化、云平台和生产优化。
生产环境设置
环境配置
javascript
// config/production.js
module.exports = {
// Server configuration
server: {
port: process.env.PORT || 8080,
host: process.env.HOST || '0.0.0.0',
env: 'production'
},
// Database configuration
database: {
url: process.env.DATABASE_URL,
poolSize: parseInt(process.env.DB_POOL_SIZE) || 20,
ssl: process.env.DB_SSL === 'true',
options: {
useNewUrlParser: true,
useUnifiedTopology: true,
maxPoolSize: 20,
serverSelectionTimeoutMS: 5000,
socketTimeoutMS: 45000,
}
},
// Redis configuration
redis: {
url: process.env.REDIS_URL,
maxRetriesPerRequest: 3,
retryDelayOnFailover: 100,
enableOfflineQueue: false
},
// Security configuration
security: {
jwtSecret: process.env.JWT_SECRET,
bcryptRounds: 12,
rateLimitWindowMs: 15 * 60 * 1000,
rateLimitMax: 100,
corsOrigin: process.env.CORS_ORIGIN?.split(',') || false
},
// Logging configuration
logging: {
level: process.env.LOG_LEVEL || 'info',
format: 'json',
transports: {
file: {
enabled: true,
filename: '/var/log/app/app.log',
maxsize: 10485760, // 10MB
maxFiles: 5
},
console: {
enabled: process.env.CONSOLE_LOGGING === 'true'
}
}
},
// Monitoring configuration
monitoring: {
enabled: true,
metricsPort: process.env.METRICS_PORT || 9090,
healthCheckPath: '/health',
readinessCheckPath: '/ready'
}
};使用 PM2 进行进程管理
javascript
// ecosystem.config.js
module.exports = {
apps: [{
name: 'node-app',
script: './src/server.js',
instances: 'max', // Use all CPU cores
exec_mode: 'cluster',
// Environment variables
env: {
NODE_ENV: 'production',
PORT: 8080
},
// Logging
log_file: '/var/log/pm2/app.log',
out_file: '/var/log/pm2/app-out.log',
error_file: '/var/log/pm2/app-error.log',
log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
// Process management
max_memory_restart: '1G',
restart_delay: 4000,
max_restarts: 10,
min_uptime: '10s',
// Monitoring
pmx: true,
// Advanced features
watch: false,
ignore_watch: ['node_modules', 'logs'],
// Graceful shutdown
kill_timeout: 5000,
listen_timeout: 3000,
// Auto restart on file changes (development only)
watch_options: {
followSymlinks: false
}
}],
deploy: {
production: {
user: 'deploy',
host: ['server1.example.com', 'server2.example.com'],
ref: 'origin/main',
repo: 'git@github.com:username/repo.git',
path: '/var/www/production',
'pre-deploy-local': '',
'post-deploy': 'npm install && pm2 reload ecosystem.config.js --env production',
'pre-setup': ''
}
}
};健康检查和监控
javascript
// src/health.js
const express = require('express');
const mongoose = require('mongoose');
const redis = require('redis');
class HealthChecker {
constructor(app) {
this.app = app;
this.setupHealthRoutes();
}
setupHealthRoutes() {
// Basic health check
this.app.get('/health', (req, res) => {
res.status(200).json({
status: 'healthy',
timestamp: new Date().toISOString(),
uptime: process.uptime(),
version: process.env.npm_package_version || '1.0.0'
});
});
// Detailed readiness check
this.app.get('/ready', async (req, res) => {
const checks = await this.performReadinessChecks();
const isReady = checks.every(check => check.status === 'healthy');
res.status(isReady ? 200 : 503).json({
status: isReady ? 'ready' : 'not ready',
checks,
timestamp: new Date().toISOString()
});
});
// Liveness probe
this.app.get('/live', (req, res) => {
res.status(200).json({
status: 'alive',
pid: process.pid,
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
});
});
}
async performReadinessChecks() {
const checks = [];
// Database connectivity
try {
await mongoose.connection.db.admin().ping();
checks.push({
name: 'database',
status: 'healthy',
responseTime: Date.now()
});
} catch (error) {
checks.push({
name: 'database',
status: 'unhealthy',
error: error.message
});
}
// Redis connectivity
try {
const redisClient = redis.createClient(process.env.REDIS_URL);
await redisClient.ping();
await redisClient.quit();
checks.push({
name: 'redis',
status: 'healthy'
});
} catch (error) {
checks.push({
name: 'redis',
status: 'unhealthy',
error: error.message
});
}
// Memory usage check
const memUsage = process.memoryUsage();
const memoryHealthy = memUsage.heapUsed < (1024 * 1024 * 1024); // 1GB threshold
checks.push({
name: 'memory',
status: memoryHealthy ? 'healthy' : 'warning',
heapUsed: memUsage.heapUsed,
heapTotal: memUsage.heapTotal
});
return checks;
}
}
module.exports = HealthChecker;使用 Docker 进行容器化
Dockerfile
dockerfile
# Multi-stage build for production optimization
FROM node:18-alpine AS builder
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production && npm cache clean --force
# Copy source code
COPY . .
# Build application (if needed)
RUN npm run build || true
# Production stage
FROM node:18-alpine AS production
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodejs -u 1001
# Set working directory
WORKDIR /app
# Copy built application from builder stage
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=nodejs:nodejs /app/package*.json ./
COPY --from=builder --chown=nodejs:nodejs /app/src ./src
# Create logs directory
RUN mkdir -p /var/log/app && chown nodejs:nodejs /var/log/app
# Switch to non-root user
USER nodejs
# Expose port
EXPOSE 8080
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD node healthcheck.js
# Start application
CMD ["node", "src/server.js"]用于开发的 Docker Compose
yaml
# docker-compose.yml
version: '3.8'
services:
app:
build: .
ports:
- "3000:8080"
environment:
- NODE_ENV=development
- DATABASE_URL=mongodb://mongo:27017/myapp
- REDIS_URL=redis://redis:6379
depends_on:
- mongo
- redis
volumes:
- ./src:/app/src
- ./logs:/var/log/app
restart: unless-stopped
mongo:
image: mongo:5
ports:
- "27017:27017"
environment:
- MONGO_INITDB_ROOT_USERNAME=admin
- MONGO_INITDB_ROOT_PASSWORD=password
volumes:
- mongo_data:/data/db
restart: unless-stopped
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- app
restart: unless-stopped
volumes:
mongo_data:
redis_data:生产环境 Docker Compose
yaml
# docker-compose.prod.yml
version: '3.8'
services:
app:
image: myapp:latest
deploy:
replicas: 3
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
- REDIS_URL=${REDIS_URL}
- JWT_SECRET=${JWT_SECRET}
networks:
- app-network
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.prod.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
- ./static:/var/www/static
networks:
- app-network
depends_on:
- app
networks:
app-network:
driver: overlay云平台部署
使用 Elastic Beanstalk 部署到 AWS
json
// .ebextensions/01-node-settings.config
{
"option_settings": [
{
"namespace": "aws:elasticbeanstalk:container:nodejs",
"option_name": "NodeCommand",
"value": "npm start"
},
{
"namespace": "aws:elasticbeanstalk:container:nodejs",
"option_name": "NodeVersion",
"value": "18.17.0"
},
{
"namespace": "aws:elasticbeanstalk:application:environment",
"option_name": "NODE_ENV",
"value": "production"
}
]
}Kubernetes 部署
yaml
# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: node-app
labels:
app: node-app
spec:
replicas: 3
selector:
matchLabels:
app: node-app
template:
metadata:
labels:
app: node-app
spec:
containers:
- name: node-app
image: myapp:latest
ports:
- containerPort: 8080
env:
- name: NODE_ENV
value: "production"
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: database-url
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: redis-url
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: node-app-service
spec:
selector:
app: node-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancerCI/CD 管道
GitHub Actions 工作流
yaml
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
mongodb:
image: mongo:5
ports:
- 27017:27017
redis:
image: redis:7
ports:
- 6379:6379
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run linting
run: npm run lint
- name: Run tests
run: npm test
env:
NODE_ENV: test
DATABASE_URL: mongodb://localhost:27017/test
REDIS_URL: redis://localhost:6379
- name: Run security audit
run: npm audit --audit-level high
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to Container Registry
uses: docker/login-action@v2
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v4
with:
context: .
push: true
tags: |
ghcr.io/${{ github.repository }}:latest
ghcr.io/${{ github.repository }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v3
- name: Deploy to production
run: |
echo "Deploying to production..."
# Add your deployment commands here
# e.g., kubectl apply, helm upgrade, etc.性能优化
生产优化
javascript
// src/optimizations.js
const compression = require('compression');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
function applyProductionOptimizations(app) {
// Security headers
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
// Compression
app.use(compression({
filter: (req, res) => {
if (req.headers['x-no-compression']) {
return false;
}
return compression.filter(req, res);
},
level: 6,
threshold: 1024
}));
// Rate limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // limit each IP to 100 requests per windowMs
message: {
error: 'Too many requests from this IP',
retryAfter: Math.ceil(15 * 60 * 1000 / 1000)
},
standardHeaders: true,
legacyHeaders: false
});
app.use('/api/', limiter);
// Static file caching
app.use('/static', express.static('public', {
maxAge: '1y',
etag: true,
lastModified: true
}));
// Request timeout
app.use((req, res, next) => {
req.setTimeout(30000, () => {
res.status(408).json({ error: 'Request timeout' });
});
next();
});
return app;
}
module.exports = { applyProductionOptimizations };监控和日志
应用程序指标
javascript
// src/metrics.js
const prometheus = require('prom-client');
class MetricsCollector {
constructor() {
// Create a Registry
this.register = new prometheus.Registry();
// Add default metrics
prometheus.collectDefaultMetrics({ register: this.register });
// Custom metrics
this.httpRequestDuration = new prometheus.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.1, 0.5, 1, 2, 5]
});
this.httpRequestTotal = new prometheus.Counter({
name: 'http_requests_total',
help: 'Total number of HTTP requests',
labelNames: ['method', 'route', 'status_code']
});
this.activeConnections = new prometheus.Gauge({
name: 'active_connections',
help: 'Number of active connections'
});
// Register custom metrics
this.register.registerMetric(this.httpRequestDuration);
this.register.registerMetric(this.httpRequestTotal);
this.register.registerMetric(this.activeConnections);
}
middleware() {
return (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const route = req.route?.path || req.path;
this.httpRequestDuration
.labels(req.method, route, res.statusCode)
.observe(duration);
this.httpRequestTotal
.labels(req.method, route, res.statusCode)
.inc();
});
next();
};
}
getMetrics() {
return this.register.metrics();
}
}
module.exports = MetricsCollector;下一步
在最后一章中,我们将探索学习资源和高级主题,以继续您的 Node.js 之旅。
关键要点
- 生产配置需要特定于环境的设置
- 像 PM2 这样的进程管理器提供集群和监控
- 容器化确保一致的部署环境
- 健康检查支持适当的负载均衡器集成
- CI/CD 管道自动化测试和部署
- 监控和指标有助于识别性能问题
- 安全头和速率限制防止攻击
- 适当的日志记录有助于调试和审计