systemd لتشغيل Node.js على VM بدون crash صامت
هتخرج من المقال ده بإعداد عملي يخلي خدمة Node.js ترجع بعد الفشل في حوالي 8 ثواني، وتكتب logs واضحة، وتتقفل بصلاحيات أقل بدل ما تفضل شغالة بـ nohup من غير رقابة.
مستوى القارئ: متوسط
المشكلة باختصار
الطريقة الشائعة هي تشغيل التطبيق بـ node server.js داخل screen أو nohup. الطريقة دي بتفشل في 3 نقاط: لو العملية ماتت مش دايمًا هترجع، الـ logs بتتشتت في ملف عشوائي، والتطبيق غالبًا بيشتغل بصلاحيات أوسع من احتياجه.
الافتراض هنا إن عندك VM واحدة أو اتنين، تطبيق Node.js خلف Nginx، وحجم ترافيك في حدود 5K إلى 50K زائر يوميًا. لو عندك Kubernetes أو منصة PaaS، الفكرة نفسها مفيدة، لكن التنفيذ هيختلف.
الفكرة بمثال واضح
ركز في المثال ده: عندك API فواتير على /srv/invoice-api. الساعة 2 بالليل حصل memory leak والعملية خرجت بكود خطأ. بدون مدير خدمة، الموقع يفضل واقع لحد ما حد يفتح السيرفر. مع systemd، العملية تفشل، تتحول لحالة failed، ثم ترجع بعد RestartSec=5. المكسب إن الانقطاع يبقى ثواني بدل دقائق. الـ trade-off إنك لازم تكتب unit file مضبوط وتراقب سبب الفشل، بدل ما تخبي المشكلة وراء restart لا نهائي.
في قياس داخلي بسيط، خدمة كانت تحتاج تدخل يدوي بعد crash خلال 3 دقائق تقريبًا. بعد Restart=on-failure رجعت في 8 ثواني. وبعد hardening نزل تقدير systemd-analyze security من 8.8 إلى 4.1. الأرقام هنا تقديرية لتوضيح طريقة القياس، مش وعد ثابت لكل تطبيق.
ملف service جاهز
أنشئ مستخدم تشغيل محدود بدل root. بعد كده اكتب ملف الخدمة. هذا مثال عملي لتطبيق Express يستمع على port داخلي مثل 3000 وNginx يمرر له الترافيك.
sudo useradd --system --home /srv/invoice-api --shell /usr/sbin/nologin invoice-api
sudo mkdir -p /srv/invoice-api
sudo chown -R invoice-api:invoice-api /srv/invoice-api
sudo nano /etc/systemd/system/invoice-api.service[Unit]
Description=Invoice API Node.js service
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=invoice-api
Group=invoice-api
WorkingDirectory=/srv/invoice-api
Environment=NODE_ENV=production
Environment=PORT=3000
ExecStart=/usr/bin/node /srv/invoice-api/server.js
Restart=on-failure
RestartSec=5
TimeoutStopSec=20
KillSignal=SIGTERM
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/srv/invoice-api/logs
[Install]
WantedBy=multi-user.targetRestart=on-failure أفضل من always في أغلب خدمات الويب، لأنه لا يعيد تشغيل الخدمة لو أنت عملت systemctl stop بإرادتك. ProtectSystem=strict يجعل النظام read-only للخدمة، ثم تسمح فقط بمسار كتابة واضح عبر ReadWritePaths. بتكسب تقليل الضرر لو التطبيق اتخترق، وتخسر مرونة الكتابة العشوائية في أي مجلد.
التشغيل والتحقق
قبل التشغيل، خلّي systemd يراجع الملف. بعد ذلك فعّل الخدمة واقرأ حالتها والـ logs من مكان واحد.
sudo systemd-analyze verify /etc/systemd/system/invoice-api.service
sudo systemctl daemon-reload
sudo systemctl enable --now invoice-api
sudo systemctl status invoice-api --no-pager
journalctl -u invoice-api -fلو عايز تختبر الرجوع بعد الفشل، اقتل العملية الرئيسية ثم راقب وقت الرجوع. لا تعمل هذا على production وقت الذروة.
MAIN_PID=$(systemctl show -p MainPID --value invoice-api)
sudo kill -9 "$MAIN_PID"
sleep 8
systemctl is-active invoice-api
journalctl -u invoice-api -n 40 --no-pagerلو النتيجة active بعد 8 إلى 12 ثانية، الـ restart policy شغالة. لو الخدمة ترجع وتقع كل مرة، لا تزود RestartSec وخلاص. افتح آخر logs وابحث عن السبب الحقيقي: متغير بيئة ناقص، اتصال قاعدة بيانات، أو migration اتنفذت غلط.
hardening وقياسه
أفضل طريقة هنا إنك تقيس قبل وبعد. الأمر systemd-analyze security لا يثبت إن تطبيقك آمن بالكامل، لكنه يعطيك مؤشر سريع عن إعدادات sandboxing التي يطبقها systemd نفسه. قيمة أعلى تعني exposure أكبر، وقيمة أقل تعني قيود أقوى.
systemd-analyze security invoice-api.serviceNoNewPrivileges=true يمنع العملية وأولادها من اكتساب صلاحيات جديدة عبر exec. PrivateTmp=true يعزل /tmp عن باقي النظام. ProtectHome=true يمنع الوصول لمجلدات المستخدمين. الـ trade-off هنا واضح: كل قيد ممكن يكسر مكتبة كانت تكتب في مكان غير متوقع. لذلك طبّق القيود واحدة واحدة، ثم شغّل health check بعد كل تغيير.
متى لا تستخدم هذه الطريقة
لا تستخدم هذا الإعداد وحده لو عندك أكثر من instance تحتاج rolling deploy، أو auto-scaling، أو service discovery. في الحالة دي Kubernetes أو Nomad أو PaaS قد يكونوا أنسب. لا تعتمد عليه أيضًا كبديل للمراقبة. systemd يرجّع الخدمة، لكنه لا يخبرك وحده إن معدل أخطاء API وصل 12% أو إن latency زاد من 90ms إلى 700ms.
ولو تطبيقك يستخدم native addons أو runtime يكتب ملفات مؤقتة كثيرة، ابدأ بقيود أقل. مثلًا جرّب NoNewPrivileges وPrivateTmp أولًا، ثم أضف ProtectSystem=strict بعد ما تحدد مسارات الكتابة المطلوبة.
مصادر اعتمدت عليها
- systemd.service: Restart وWatchdogSec وNotifyAccess
- systemd.exec: NoNewPrivileges وProtectSystem وPrivateTmp
- systemd-analyze security وقياس exposure level
- journalctl لقراءة logs من systemd journal
الخطوة التالية
الخطوة التالية: خذ خدمة واحدة غير حرجة عندك، حوّلها إلى systemd service بنفس القالب، ثم سجل رقمين فقط: زمن الرجوع بعد crash، ونتيجة systemd-analyze security قبل وبعد hardening.