العودة إلى المدونة

استضافة Next.js على خادم VPS: البناء وPM2 وNginx وإعادة التشغيل دون توقف

فريق تحرير IM Host25 أكتوبر 20267 دقائق قراءة
استضافة Next.js على خادم VPS: البناء وPM2 وNginx وإعادة التشغيل دون توقف

يعمل Next.js جيدًا على خادم Linux عادي. كل ما تحتاجه أربعة أجزاء: بناء ينتج خادمًا صغيرًا مكتفيًا بذاته، ومدير عمليات يبقيه قيد التشغيل، وNginx أمامه، وطريقة نشر تستبدل الإصدار القديم بالجديد دون أن تضيع أي طلبات. يشرح هذا الدليل إعداد الأجزاء الأربعة على Ubuntu أو Debian، مع سبب كل خطوة حتى تستطيع تكييفها مع تطبيقك.

ما تحتاجه قبل البدء

  • خادم Linux VPS بصلاحيات root أو sudo. كقاعدة تقريبية، يحتاج بناء تطبيق Next.js متوسط الحجم على الخادم إلى 2 GB من الذاكرة على الأقل، و4 GB أكثر راحة. إذا كنت تبني في CI وتشغّل التطبيق فقط على الخادم، فغالبًا تكفي 1 إلى 2 GB لموقع صغير.

  • Node.js بإصدار LTS (20 أو 22). تحقق بالأمر node -v.

  • نطاق عليه سجل A (وسجل AAAA إن كنت تستخدم IPv6) يشير إلى الخادم.

الخطوة 1: فعّل خيار standalone

افتراضيًا، يحتاج الأمر next start إلى مجلد node_modules كاملًا على الخادم. أما مع خيار standalone فيتتبع Next.js الملفات التي يستوردها تطبيقك فعلًا وينسخها وحدها إلى .next/standalone مع ملف server.js بسيط. النتيجة أصغر حجمًا وأسرع في النسخ، ولا تحتاج إلى npm install وقت التشغيل.

// next.config.js
/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'standalone',
};
module.exports = nextConfig;

ابنِ التطبيق:

npm ci
npm run build

هناك تفصيل يقع فيه الجميع تقريبًا: مخرجات standalone لا تتضمن مجلد public ولا .next/static. إذا لم تنسخهما فستظهر الصفحات دون CSS أو JavaScript، وسيعرض المتصفح أخطاء 404 لكل ملف تحت /_next/static/....

cp -r public .next/standalone/
cp -r .next/static .next/standalone/.next/

اختبر الخادم مباشرة:

cd .next/standalone
PORT=3000 HOSTNAME=127.0.0.1 node server.js
# in a second terminal
curl -I http://127.0.0.1:3000

يقرأ خادم standalone المتغير HOSTNAME ليحدد الواجهة التي يستمع عليها. كثير من بيئات الطرفية تضبط HOSTNAME مسبقًا على اسم الجهاز، لذا اضبطه صراحة على 127.0.0.1. بهذا يستمع التطبيق محليًا فقط، ويبقى Nginx نقطة الدخول العامة الوحيدة.

الخطوة 2: استخدم بنية مجلدات للإصدارات

إذا بنيت التطبيق داخل المجلد الذي يعمل منه التطبيق الحي، فسيستبدل البناء الملفات أثناء تقديمها للزوار، فيرى المستخدمون أخطاء لدقيقة أو اثنتين مع كل نشر. تحل بنية الإصدارات هذه المشكلة: يذهب كل نشر إلى مجلد مستقل، ويشير رابط رمزي (symlink) إلى الإصدار النشط.

/var/www/app/
  repo/        # git checkout, where you build
  releases/    # one folder per deploy
  shared/      # files that survive deploys
  current      # symlink to the active release

سكربت نشر يتبع هذه البنية:

#!/usr/bin/env bash
set -euo pipefail
APP=/var/www/app
REL=$APP/releases/$(date +%Y%m%d-%H%M%S)

cd $APP/repo
git pull --ff-only
npm ci
npm run build

mkdir -p "$REL/.next"
cp -r .next/standalone/. "$REL"/
cp -r public "$REL"/public
cp -r .next/static "$REL"/.next/static

# keep hashed assets from older builds for open browser tabs
mkdir -p $APP/shared/_next/static
cp -r .next/static/. $APP/shared/_next/static/

# atomic switch of the symlink
ln -s "$REL" $APP/current.tmp
mv -T $APP/current.tmp $APP/current

pm2 reload app --update-env

# keep the last 5 releases
ls -1dt $APP/releases/* | tail -n +6 | xargs -r rm -rf

استخدام ln -s ثم mv -T يستبدل الرابط الرمزي في خطوة واحدة، فلا توجد لحظة يختفي فيها current. والاحتفاظ بالإصدارات القديمة يمنحك تراجعًا بأمر واحد، كما سنرى لاحقًا.

بخصوص متغيرات البيئة: ضع قيم وقت التشغيل في ملف ecosystem الخاص بـ PM2 أو في ملف خارج مجلد الإصدار، حتى ترى كل الإصدارات القيم نفسها. أما المتغيرات التي تبدأ بـ NEXT_PUBLIC_ فتُدمج في كود JavaScript وقت البناء، لذا يجب أن تكون موجودة عند تنفيذ npm run build وليس وقت التشغيل فقط.

الخطوة 3: شغّل التطبيق باستخدام PM2

يعيد PM2 تشغيل التطبيق إذا توقف، ويشغّله عند إقلاع الخادم، ويتولى إعادة التحميل.

sudo npm install -g pm2

أنشئ الملف /var/www/app/ecosystem.config.js:

module.exports = {
  apps: [{
    name: 'app',
    script: '/var/www/app/current/server.js',
    cwd: '/var/www/app/current',
    exec_mode: 'cluster',
    instances: 2,
    listen_timeout: 10000,
    kill_timeout: 5000,
    max_memory_restart: '600M',
    env: {
      NODE_ENV: 'production',
      PORT: 3000,
      HOSTNAME: '127.0.0.1'
    }
  }]
};
cd /var/www/app
pm2 start ecosystem.config.js
pm2 save
pm2 startup systemd

يطبع الأمر pm2 startup أمرًا آخر. نفّذه باستخدام sudo حتى يبدأ PM2 مع الإقلاع بقائمة العمليات التي حفظتها.

لماذا هذه الإعدادات:

  • وضع cluster هو ما يجعل pm2 reload بلا توقف. يشغّل PM2 عاملًا (worker) جديدًا وينتظر حتى يبدأ الاستماع، ثم يوقف عاملًا قديمًا، وتتشارك العمليات المنفذ 3000. أما في وضع fork فيتصرف reload مثل إعادة التشغيل العادية وتضيع بعض الطلبات.

  • listen_timeout هي المدة التي ينتظرها PM2 حتى يبدأ العامل الجديد الاستماع. على خادم صغير قد يستغرق إقلاع Next.js وقتًا أطول من القيمة الافتراضية.

  • kill_timeout تمنح الطلبات الجارية وقتًا لتكتمل قبل إنهاء العامل القديم.

  • instances: ابدأ بعاملين. لكل عامل ذاكرته الخاصة، لذا راقب الاستهلاك بالأمر pm2 monit قبل زيادة العدد. يحلّ Node الرابط الرمزي current عند تحميل الكود، فيحمّل كل عامل جديد الإصدار الجديد.

ملاحظة مهمة: مع عدة عمليات يكون لكل منها ذاكرة تخزين مؤقت خاصة. إذا كنت تعتمد على ISR أو إعادة التحقق عند الطلب، فقد تقدم العمليات المختلفة نسخًا مختلفة من الصفحة لفترة قصيرة. إذا كان هذا يهم تطبيقك، فاستخدم معالج تخزين مؤقت مشتركًا أو شغّل عملية واحدة.

الخطوة 4: ضع Nginx في الواجهة

يتولى Nginx شهادات TLS، ويضغط الاستجابات، ويقدم الملفات الثابتة دون المرور بـ Node، ويستقبل العملاء البطيئين حتى لا يشغلوا عمليات Node.

sudo apt install nginx

أنشئ الملف /etc/nginx/sites-available/app:

upstream nextjs {
    server 127.0.0.1:3000;
    keepalive 32;
}

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

    location /_next/static/ {
        alias /var/www/app/shared/_next/static/;
        add_header Cache-Control "public, max-age=31536000, immutable";
        access_log off;
    }

    location / {
        proxy_pass http://nextjs;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        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;
    }
}
sudo ln -s /etc/nginx/sites-available/app /etc/nginx/sites-enabled/
sudo nginx -t && sudo systemctl reload nginx
sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
sudo ufw allow 'Nginx Full'

لا تفتح المنفذ 3000 في الجدار الناري. يجب أن يكون الوصول إلى التطبيق عبر Nginx فقط.

ترويسات التخزين المؤقت لمسار /_next/static

تحمل الملفات تحت /_next/static بصمة للمحتوى (hash) أو معرّف البناء في مسارها. أي أن الرابط الواحد يعيد دائمًا المحتوى نفسه، لذا من الآمن تخزينه مؤقتًا لمدة سنة مع immutable. يرسل Next.js هذه الترويسة بنفسه عندما يقدم Node هذه الملفات، لكن تقديمها من Nginx يخفف الحمل عن Node ويتيح لك استخدام المجلد المشترك الذي يحتفظ بملفات البناءات السابقة.

لا تنسخ هذه الترويسة إلى مسارات أخرى:

  • صفحات HTML تتغير مع كل نشر. اترك Next.js يضبط ترويساتها.

  • ملفات مجلد public/ مثل /logo.png تحتفظ بالاسم نفسه عند تغير محتواها. استخدم مدة قصيرة (من ساعة إلى يوم) أو أضف رقم إصدار إلى اسم الملف.

  • /_next/image يتولاه Next.js، ويحدد مدة التخزين من الإعداد images.minimumCacheTTL.

تحقق من النتيجة:

curl -sI https://example.com/_next/static/chunks/main-app.js | grep -i cache-control

استخدم اسم ملف حقيقيًا من البناء الخاص بك. يكبر المجلد المشترك مع الوقت، لذا نظّفه من حين لآخر، مثلًا بالأمر find /var/www/app/shared/_next/static -type f -mtime +30 -delete.

أخطاء شائعة ومعناها

  • 502 Bad Gateway مع الرسالة connect() failed (111: Connection refused) while connecting to upstream في /var/log/nginx/error.log: لا شيء يستمع على 127.0.0.1:3000. نفّذ pm2 status وpm2 logs app --lines 50.

  • صفحات بلا تنسيق وأخطاء 404 لمسارات /_next/static/...: لم يُنسخ المجلد الثابت، أو مسار alias خاطئ. يحتاج كل من location وalias إلى شرطة مائلة في النهاية.

  • ChunkLoadError: Loading chunk ... failed في المتصفح بعد النشر: تبويب مفتوح يطلب ملفًا من البناء السابق. يمنع المجلد المشترك أغلب هذه الحالات.

  • يتوقف البناء برسالة Killed أو JavaScript heap out of memory: نفدت ذاكرة الخادم أثناء البناء. ابنِ في CI، أو أضف ذاكرة swap، أو استخدم خطة بذاكرة أكبر.

  • الأمر ss -tlnp | grep 3000 يعرض 0.0.0.0:3000: المتغير HOSTNAME غير مضبوط، والتطبيق يستمع على كل الواجهات.

التراجع إلى الإصدار السابق

لأن الإصدارات القديمة تبقى على القرص، فالتراجع مجرد تغيير للرابط الرمزي ثم إعادة تحميل:

cd /var/www/app/releases
PREV=$(ls -1 | sort | tail -n 2 | head -n 1)
ln -s /var/www/app/releases/$PREV /var/www/app/current.tmp
mv -T /var/www/app/current.tmp /var/www/app/current
pm2 reload app

هل خادم VPS هو المكان المناسب لتطبيق Next.js؟

الإجابة تعتمد على فريقك. يمنحك VPS تحكمًا كاملًا وتكلفة شهرية ثابتة، وفي المقابل تتحمل أنت تحديثات النظام وترقيات Node.js والمراقبة والنسخ الاحتياطي. يجدد Certbot شهادات TLS تلقائيًا عبر مؤقت، لكن تحقق من أنه يعمل فعلًا. وإذا كان موقعك ثابتًا بالكامل، فالخيار output: 'export' يتيح لـ Nginx تقديمه دون أي عملية Node، وهذا أبسط.

شغّله على IM HOST

تبدأ خطط Linux VPS من 5 دولارات شهريًا مع صلاحيات root كاملة على KVM، وعنوان IPv4 مخصص مع نطاق IPv6 /64، وحماية من هجمات DDoS. وإذا كنت تبني على الخادم وتريد معالجًا غير مشترك، فخطط Cloud VPS تبدأ من 16 دولارًا شهريًا مع 4 vCPU مخصصة و8 GB RAM، ونسخة احتياطية تلقائية مجانية كل 3 أيام. لا توجد رسوم إعداد، والدعم الفني من مهندسين متاح على مدار الساعة، مع ضمان استرداد الأموال خلال 30 يومًا. هل تنقل تطبيقًا قائمًا؟ تتولى خدمة النقل المجاني نقل الملفات وقواعد البيانات دون توقف.

المزيد من مدونتنا

اكتشف المزيد من الأدلة العملية ورؤى الخدمات من فريق IM Host.

عرض كل المقالات