النشر و DevOps لـ Node.js
مقدمة إلى نشر 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
- تنفيذ فحوصات الصحة والإيقاف الأنيق
- تكوين متغيرات البيئة لبيئات مختلفة
اختبر النشر بدون توقف عن طريق تحديث التطبيق مع مراقبة وقت التشغيل.