Skip to content

部署

概述

将 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: LoadBalancer

CI/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 管道自动化测试和部署
  • 监控和指标有助于识别性能问题
  • 安全头和速率限制防止攻击
  • 适当的日志记录有助于调试和审计

本站内容仅供学习和研究使用。