أطلِق إلى الإنتاج: NODE_ENV، ومدير العمليات، وصورة Docker صحيحة، والتوسّع عبر عدة أنوية باستخدام cluster.
افتح هذا الدرس في Kodokonإن NODE_ENV مجرّد اصطلاح بين المكتبات: فنواة Node تتجاهله كليًا تقريبًا. لكن Express، من بين غيرها، تستخدمه لتخزين العروض مؤقتًا وإخفاء آثار تتبّع المكدس؛ وبعض أطر العمل أسرع بأضعاف مع NODE_ENV=production. أما على صعيد التبعيات، فتُثبِّت npm ci --omit=dev ملف القفل (lockfile) بالضبط، دون تبعيات التطوير - وهي قابلة للاستنساخ وأسرع من npm install. ومنذ Node 20.6، تُحمِّل --env-file ملف .env بشكل أصيل، دون أيّ تبعية.
NODE_ENV=production node server.js
npm ci --omit=dev
node --env-file=.env server.jsإن عملية Node تنتهي دائمًا إلى الموت: تجاوز في الذاكرة، أو خلل، أو استثناء. ودور مدير العمليات هو إعادة تشغيلها. وثمة مدرستان: على آلة مجرّدة، تُشرِف pm2 (أو systemd) وتعيد التشغيل وتُعيد التحميل دون توقّف؛ أما في حاوية، فالمُنسِّق (Kubernetes، ECS) هو من يؤدّي هذا الدور - عملية واحدة لكل حاوية، وتصبح pm2 عديمة الفائدة، بل ضارّة، لأنها تُخفي الأعطال عن المُنسِّق.
npm install -g pm2
pm2 start server.js -i max --name api
pm2 reload api
pm2 logs api --lines 100مزلقان في Docker خاصّان بـ Node. الأول: ينبغي أن تعمل الصورة بمستخدم node غير المتميّز، الذي توفّره الصور الرسمية. الثاني: لم تُصمَّم Node لتكون PID 1 - فهي لا تحصد العمليات الشبحية (zombie)؛ فأطلِق الحاوية بـ docker run --init (أو init: true في Compose) لإدراج init بسيط. وأخيرًا، اكتب CMD بـ الصيغة التنفيذية (exec form): فالصيغة الصدفية (shell form) تُدرِج /bin/sh، التي لا تُمرِّر SIGTERM - فالإيقاف السلس من الدرس السابق لن يُطلَق أبدًا.
FROM node:20-slim AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
FROM node:20-slim
ENV NODE_ENV=production
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]تستخدم عملية Node الواحدة نواةً واحدة فقط. وتُنشئ الوحدة cluster عملية عامل (worker) واحدة لكل نواة: إذ تستمع العملية الرئيسية (primary) إلى المنفذ وتوزّع الاتصالات على العُمّال بالتناوب الدوري (round-robin) (وهو السلوك الافتراضي، عدا على Windows). ولكل عامل ذاكرته الخاصة وحلقة أحداثه الخاصة: لا حالة مشتركة - فالجلسات والذاكرات المؤقّتة يجب أن تعيش في Redis أو ما يعادله. وللحساب الصرف، فضّل worker_threads، التي تشارك الذاكرة عبر SharedArrayBuffer.
import cluster from "node:cluster";
import { createServer } from "node:http";
import { availableParallelism } from "node:os";
if (cluster.isPrimary) {
const count = availableParallelism();
for (let i = 0; i < count; i += 1) cluster.fork();
cluster.on("exit", (worker) => {
console.log(`worker ${worker.process.pid} down`);
cluster.fork();
});
} else {
createServer((req, res) => {
res.end(`pid ${process.pid}`);
}).listen(3000);
}