Node.js و Express

النشر و DevOps لـ Node.js

40 دقيقة الدرس 32 من 40

مقدمة إلى نشر Node.js

نشر تطبيق Node.js إلى الإنتاج يتضمن أكثر بكثير من مجرد تشغيل node app.js. تحتاج إلى التفكير في إدارة العمليات، وتكوين البيئة، والأمان، والمراقبة، وقابلية التوسع. يغطي هذا الدرس استراتيجيات النشر الحديثة وممارسات DevOps.

الإنتاج مقابل التطوير: بيئات الإنتاج تتطلب تكوينات مختلفة: لا توجد خرائط مصدر، ملفات مصغرة، تسجيل صحيح، متغيرات بيئة، ومراقبة العمليات.

متغيرات البيئة

متغيرات البيئة تفصل التكوين عن الكود، مما يجعل التطبيقات قابلة للنقل وآمنة:

// ملف .env (لا تضعه في git أبدًا)
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@localhost:5432/mydb
JWT_SECRET=your-secret-key-here
API_KEY=your-api-key
REDIS_URL=redis://localhost:6379
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-password

// config.js - تكوين مركزي
require('dotenv').config();

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

  database: {
    url: process.env.DATABASE_URL,
    pool: {
      min: 2,
      max: 10
    }
  },

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

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

  email: {
    host: process.env.SMTP_HOST,
    port: parseInt(process.env.SMTP_PORT, 10),
    auth: {
      user: process.env.SMTP_USER,
      pass: process.env.SMTP_PASS
    }
  },

  isProduction: process.env.NODE_ENV === 'production',
  isDevelopment: process.env.NODE_ENV === 'development'
};

// الاستخدام في التطبيق
const config = require('./config');

if (config.isProduction) {
  // إعداد خاص بالإنتاج
  app.use(compression());
  app.use(helmet());
}

الأمان: لا تضع الأسرار في الكود أبدًا. استخدم دائمًا متغيرات البيئة وأضف .env إلى ملف .gitignore.

إدارة العمليات مع PM2

PM2 هو مدير عمليات إنتاج لتطبيقات Node.js مع موازنة تحميل مدمجة:

// التثبيت
npm install -g pm2

// بدء التطبيق
pm2 start app.js --name "my-app"

// بدء مع وضع المجموعة (متعدد النوى)
pm2 start app.js -i max --name "my-app"

// ecosystem.config.js - تكوين PM2
module.exports = {
  apps: [{
    name: 'my-app',
    script: './app.js',
    instances: 'max',
    exec_mode: 'cluster',
    env: {
      NODE_ENV: 'development'
    },
    env_production: {
      NODE_ENV: 'production',
      PORT: 3000
    },
    error_file: './logs/err.log',
    out_file: './logs/out.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss Z',
    merge_logs: true,
    max_memory_restart: '1G',
    autorestart: true,
    watch: false,
    ignore_watch: ['node_modules', 'logs']
  }]
};

// أوامر PM2 الشائعة
pm2 start ecosystem.config.js --env production
pm2 restart my-app
pm2 reload my-app    // إعادة تحميل بدون توقف
pm2 stop my-app
pm2 delete my-app
pm2 list             // قائمة جميع العمليات
pm2 logs my-app      // عرض السجلات
pm2 monit            // مراقبة CPU/الذاكرة
pm2 save             // حفظ قائمة العمليات
pm2 startup          // إنشاء سكريبت بدء التشغيل
pm2 resurrect        // استعادة العمليات المحفوظة

الحاويات مع Docker

Docker يحزم تطبيقك مع جميع التبعيات في حاوية قابلة للنقل:

# Dockerfile - بناء متعدد المراحل
# المرحلة 1: البناء
FROM node:18-alpine AS builder

WORKDIR /app

# نسخ ملفات الحزم
COPY package*.json ./

# تثبيت التبعيات
RUN npm ci --only=production

# نسخ الكود المصدري
COPY . .

# المرحلة 2: الإنتاج
FROM node:18-alpine

WORKDIR /app

# إنشاء مستخدم غير جذر
RUN addgroup -g 1001 -S nodejs && \
    adduser -S nodejs -u 1001

# النسخ من البناء
COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules
COPY --chown=nodejs:nodejs . .

# التبديل إلى مستخدم غير جذر
USER nodejs

# كشف المنفذ
EXPOSE 3000

# فحص الصحة
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s \
  CMD node healthcheck.js

# بدء التطبيق
CMD ["node", "app.js"]

# .dockerignore
node_modules
npm-debug.log
.env
.git
.gitignore
README.md
docker-compose*.yml
Dockerfile*

بناء وتشغيل حاوية Docker:

# بناء الصورة
docker build -t my-app:1.0.0 .

# تشغيل الحاوية
docker run -d \
  --name my-app \
  -p 3000:3000 \
  -e NODE_ENV=production \
  -e DATABASE_URL=postgresql://... \
  --restart unless-stopped \
  my-app:1.0.0

# عرض السجلات
docker logs -f my-app

# تنفيذ الأوامر في الحاوية
docker exec -it my-app sh

# إيقاف وإزالة الحاوية
docker stop my-app
docker rm my-app

Docker Compose للتطبيقات متعددة الحاويات

Docker Compose ينسق حاويات متعددة:

# docker-compose.yml
version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=postgresql://postgres:password@db:5432/mydb
      - REDIS_URL=redis://redis:6379
    depends_on:
      - db
      - redis
    restart: unless-stopped
    volumes:
      - ./logs:/app/logs

  db:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=password
      - POSTGRES_DB=mydb
    volumes:
      - postgres_data:/var/lib/postgresql/data
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    restart: unless-stopped
    volumes:
      - redis_data:/data

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - app
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:

# الأوامر
docker-compose up -d          # بدء جميع الخدمات
docker-compose down           # إيقاف جميع الخدمات
docker-compose logs -f app    # عرض السجلات
docker-compose ps             # قائمة الخدمات
docker-compose restart app    # إعادة تشغيل الخدمة

Nginx كوكيل عكسي

Nginx يعمل كوكيل عكسي، يتعامل مع SSL والتخزين المؤقت وموازنة التحميل:

# nginx.conf
events {
  worker_connections 1024;
}

http {
  upstream app_servers {
    least_conn;
    server app:3000 max_fails=3 fail_timeout=30s;
    # إضافة المزيد من الخوادم لموازنة التحميل
    # server app2:3000 max_fails=3 fail_timeout=30s;
  }

  # تحديد المعدل
  limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

  # إعدادات التخزين المؤقت
  proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m;

  server {
    listen 80;
    server_name example.com www.example.com;

    # إعادة توجيه HTTP إلى HTTPS
    return 301 https://$host$request_uri;
  }

  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_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    # رؤوس الأمان
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

    # الضغط
    gzip on;
    gzip_vary on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml;

    # الملفات الثابتة
    location /static/ {
      alias /app/public/;
      expires 1y;
      add_header Cache-Control "public, immutable";
    }

    # مسارات API مع تحديد المعدل
    location /api/ {
      limit_req zone=api_limit burst=20 nodelay;

      proxy_pass http://app_servers;
      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 / {
      proxy_pass http://app_servers;
      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;

      # التخزين المؤقت لطلبات GET
      proxy_cache my_cache;
      proxy_cache_valid 200 60m;
      proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
      add_header X-Cache-Status $upstream_cache_status;
    }
  }
}

نصيحة احترافية: استخدم Let's Encrypt مع Certbot للحصول على شهادات SSL مجانية: certbot --nginx -d example.com -d www.example.com

التكامل المستمر/النشر المستمر (CI/CD)

أتمتة الاختبار والنشر مع GitHub Actions:

# .github/workflows/deploy.yml
name: النشر إلى الإنتاج

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_PASSWORD: password
          POSTGRES_DB: test_db
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    steps:
      - uses: actions/checkout@v3

      - name: إعداد Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
          cache: 'npm'

      - name: تثبيت التبعيات
        run: npm ci

      - name: تشغيل التدقيق
        run: npm run lint

      - name: تشغيل الاختبارات
        run: npm test
        env:
          DATABASE_URL: postgresql://postgres:password@localhost:5432/test_db

      - name: تشغيل تدقيق الأمان
        run: npm audit --audit-level=moderate

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: تسجيل الدخول إلى Docker Hub
        uses: docker/login-action@v2
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: بناء ودفع صورة Docker
        uses: docker/build-push-action@v4
        with:
          context: .
          push: true
          tags: |
            myapp/app:latest
            myapp/app:${{ github.sha }}

      - name: النشر إلى الخادم
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USERNAME }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd /var/www/myapp
            docker-compose pull
            docker-compose up -d
            docker system prune -f

      - name: فحص الصحة
        run: |
          sleep 10
          curl -f https://example.com/health || exit 1

      - name: إشعار النشر
        if: success()
        run: |
          curl -X POST ${{ secrets.SLACK_WEBHOOK }} \
            -H 'Content-Type: application/json' \
            -d '{"text":"النشر ناجح! 🚀"}'

خيارات الاستضافة السحابية

منصات شائعة لنشر Node.js:

// 1. Heroku - PaaS بسيط
// Procfile
web: node app.js

// النشر
heroku create my-app
git push heroku main
heroku config:set NODE_ENV=production
heroku ps:scale web=1

// 2. AWS EC2 - تحكم كامل
// تثبيت Node.js على Ubuntu
curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash -
sudo apt-get install -y nodejs
sudo npm install -g pm2

// النشر مع PM2
pm2 start ecosystem.config.js --env production
pm2 save
pm2 startup

// 3. DigitalOcean App Platform
// app.yaml
name: my-app
services:
  - name: web
    github:
      repo: username/repo
      branch: main
    build_command: npm install
    run_command: npm start
    envs:
      - key: NODE_ENV
        value: production
    http_port: 3000

// 4. Vercel - بدون خادم (محسّن لـ Next.js)
// vercel.json
{
  "version": 2,
  "builds": [
    {
      "src": "app.js",
      "use": "@vercel/node"
    }
  ],
  "routes": [
    {
      "src": "/(.*)",
      "dest": "/app.js"
    }
  ]
}

// النشر
npm i -g vercel
vercel --prod

ترحيلات قاعدة البيانات في الإنتاج

إدارة تغييرات مخطط قاعدة البيانات بأمان:

// migrations/001_create_users.js
exports.up = async (db) => {
  await db.schema.createTable('users', (table) => {
    table.increments('id').primary();
    table.string('email').unique().notNullable();
    table.string('password').notNullable();
    table.timestamps(true, true);
  });
};

exports.down = async (db) => {
  await db.schema.dropTable('users');
};

// migrate.js - منفذ الترحيل
const config = require('./config');
const migrations = require('./migrations');

async function runMigrations() {
  // إنشاء جدول الترحيلات
  await db.schema.createTableIfNotExists('migrations', (table) => {
    table.string('name').primary();
    table.timestamp('run_at').defaultTo(db.fn.now());
  });

  // الحصول على الترحيلات المكتملة
  const completed = await db('migrations').pluck('name');

  // تشغيل الترحيلات المعلقة
  for (const migration of migrations) {
    if (!completed.includes(migration.name)) {
      console.log(`تشغيل الترحيل: ${migration.name}`);

      try {
        await migration.up(db);
        await db('migrations').insert({ name: migration.name });
        console.log(`✓ ${migration.name} مكتمل`);
      } catch (error) {
        console.error(`✗ ${migration.name} فشل:`, error);
        throw error;
      }
    }
  }
}

// تشغيل قبل بدء التطبيق
runMigrations()
  .then(() => {
    console.log('جميع الترحيلات مكتملة');
    require('./app');
  })
  .catch((error) => {
    console.error('فشل الترحيل:', error);
    process.exit(1);
  });

النشر بدون توقف

نشر التحديثات دون انقطاع الخدمة:

// 1. النشر الأزرق-الأخضر
// تشغيل بيئتين متطابقتين، تبديل حركة المرور عندما يكون الإصدار الجديد جاهزًا

// docker-compose-blue-green.yml
services:
  app-blue:
    image: myapp:1.0.0
    # ...

  app-green:
    image: myapp:1.1.0
    # ...

  nginx:
    # التبديل إلى upstream الأخضر عندما يكون جاهزًا

// 2. النشر المتدرج مع PM2
pm2 reload ecosystem.config.js --update-env

// 3. الإيقاف الأنيق
process.on('SIGTERM', async () => {
  console.log('تم استلام SIGTERM، إغلاق الخادم...');

  // إيقاف قبول اتصالات جديدة
  server.close(async () => {
    console.log('تم إغلاق الخادم');

    // إغلاق اتصالات قاعدة البيانات
    await db.destroy();

    // إغلاق اتصال Redis
    await redis.quit();

    console.log('تم إغلاق جميع الاتصالات');
    process.exit(0);
  });

  // إيقاف قسري بعد 30 ثانية
  setTimeout(() => {
    console.error('إيقاف قسري');
    process.exit(1);
  }, 30000);
});

تمرين: نشر تطبيق Full-Stack

انشر تطبيق Node.js بالمتطلبات التالية:

  • تحويل التطبيق إلى حاوية Docker مع بناء متعدد المراحل
  • استخدام Docker Compose مع Node.js وPostgreSQL وRedis
  • تكوين Nginx كوكيل عكسي مع SSL
  • إعداد PM2 لإدارة العمليات
  • إنشاء خط أنابيب CI/CD مع GitHub Actions
  • تنفيذ فحوصات الصحة والإيقاف الأنيق
  • تكوين متغيرات البيئة لبيئات مختلفة

اختبر النشر بدون توقف عن طريق تحديث التطبيق مع مراقبة وقت التشغيل.