EduFlow

الوحدة 1 · الـ Containers من المبادئ الأولى

تشغيل الـ Containers: المنافذ ومتغيّرات البيئة والسجلات والإشارات

18 دقراءة 4 داملأ الفراغات

الدرس في لمحة

شغّل notes-api في الخلفية مع المنافذ ومتغيّرات البيئة وحدود الموارد، واقرأ سجلاته، وافتح shell داخله، واجعله يتوقّف توقّفًا نظيفًا عند استقبال SIGTERM.

  1. 1شغّله في الخلفية
  2. 2انظر إلى ما يعمل وما يقوله
  3. 3الإيقاف: SIGTERM ثم SIGKILL
  4. 4سياسات إعادة التشغيل ونموّ السجلات
  5. 5التنظيف

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

بناء الـ image نصف العمل؛ والنصف الآخر هو تشغيلها بالطريقة التي سيشغّلها بها الإنتاج. في العام الماضي، كانت إحدى خدمات Skylane تُسقط بضع مئات من الطلبات مع كل deploy. لم يكن في الكود أي خلل؛ كل ما في الأمر أن العملية كانت تتجاهل إشارة «من فضلك توقّف» المهذّبة، فتُقتل بعد عشر ثوانٍ في منتصف الطلبات. يتناول هذا الدرس جانب التشغيل، ويختم بمسألة الإيقاف تلك.

شغّله في الخلفية

Terminal
docker run -d --name notes \
  -p 8080:3000 \
  -e PORT=3000 \
  -e DATABASE_URL=postgres://notes:notes@host.docker.internal:5432/notes \
  --memory 512m --cpus 1 \
  notes-api:dev
  • -d يشغّل الـ container منفصلًا (detached)، فيعمل في الخلفية ويطبع Docker معرّفه.
  • --name notes يعطيه اسمًا تكتبه بدلًا من المعرّف.
  • -p 8080:3000 يربط المنفذ 8080 على المضيف بالمنفذ 3000 في الـ container. اقرأه «خارج:داخل». ويمكن لاثنين من الـ containers أن يستمعا كلاهما على المنفذ 3000 في الداخل، ما داما منشورَين على منفذين مختلفين في المضيف.
  • -e يضبط متغيّرات البيئة. استخدم --env-file .env.local حين تكثر.
  • --memory و--cpus يضبطان حدود الـ cgroups. إذا احتاجت العملية إلى ذاكرة أكثر من ذلك (إضافةً إلى ما يسمح به Docker من swap على المضيفات التي فيها swap)، ينهيها الـ OOM killer في النواة.

الاسم host.docker.internal يُحلّ إلى عنوان جهازك من داخل الـ container على Docker Desktop؛ وعلى Linux أضف --add-host=host.docker.internal:host-gateway. ستستبدل هذا بشبكة Compose حقيقية في القسم الثالث.

انظر إلى ما يعمل وما يقوله

Terminal
docker ps                      # running containers
docker logs -f notes           # follow stdout/stderr
docker logs --since 10m notes  # recent output only
docker inspect notes --format '{{.State.Status}} {{.NetworkSettings.Ports}}'

الأمر docker logs يعرض كل ما كتبته العملية إلى stdout وstderr. هذا هو العقد مع الـ containers: اكتب السجلات إلى التدفّقات القياسية، سطرًا لكل حدث، ودع المنصّة تجمعها. الكتابة إلى ملف سجلات داخل الـ container تخفي السجلات عن كل الأدوات وتضيّعها حين يُحذف الـ container.

ولإلقاء نظرة في الداخل:

Terminal
docker exec -it notes sh

الأمر exec يبدأ عملية إضافية في الـ container الذي يعمل؛ و-it يعطيك terminal تفاعليًا. هذا للنظر، لا للإصلاح. في القسم الثاني ستنتقل إلى base image لا تحتوي على shell أصلًا، وستتعلّم طرقًا أخرى للدخول.

الإيقاف: SIGTERM ثم SIGKILL

الأمر docker stop notes يرسل SIGTERM إلى PID 1 وينتظر 10 ثوانٍ (غيّرها بـ -t). إذا بقيت العملية حيّة، يرسل Docker إشارة SIGKILL، التي لا يمكن التقاطها. رموز الخروج تخبرك بما حدث:

رمز الخروج المعنى
0 خرج التطبيق بنظافة من تلقاء نفسه
143 ‏128 + 15: ماتت العملية بسبب SIGTERM دون أن تعالجها
137 ‏128 + 9: قُتلت بـ SIGKILL (انتهاء مهلة الإيقاف أو نفاد الذاكرة)

يعامل Linux العملية ذات المعرّف PID 1 معاملة خاصة: الإشارة التي لم تسجّل لها العملية معالجًا تُتجاهل بدلًا من أن تقتلها. ولا يسجّل Node.js معالجًا لـ SIGTERM افتراضيًا، لذا فإن node dist/server.js في موقع PID 1 يتجاهل docker stop ويُقتل بعد 10 ثوانٍ. وتموت معه الطلبات الجارية.

عالِج الإشارة: توقّف عن قبول الاتصالات، وأنهِ ما هو جارٍ، وأغلق مجمّع اتصالات قاعدة البيانات، ثم اخرج.

src/server.ts
const server = app.listen(port, '0.0.0.0', () => console.log(`notes-api listening on ${port}`));

process.on('SIGTERM', () => {
  console.log('SIGTERM received, draining');
  server.close(async () => {
    await pool.end();
    process.exit(0);
  });
  // Safety net: don't hang forever on a stuck keep-alive connection
  setTimeout(() => process.exit(1), 8000).unref();
});

صار docker stop ينتهي الآن في أقل بكثير من ثانية، ويخرج الـ container بالرمز 0. وهذا المعالج أهمّ في Kubernetes، الذي يرسل الإشارة نفسها مع كل deploy، وكل تقليص لعدد النسخ (scale-down)، وكل تفريغ (drain) لأحد الـ nodes.

سياسات إعادة التشغيل ونموّ السجلات

على خادم واحد دون منسّق، يطلب --restart unless-stopped من Docker إعادة الـ container بعد انهياره أو بعد إعادة تشغيل المضيف، إلا إذا أوقفته أنت بنفسك. لـ Kubernetes وCompose إعدادات إعادة تشغيل خاصة بهما، لذا نادرًا ما ستضبط هذا على جهازك، لكنه أول ما تفحصه على تلك الآلة الافتراضية القديمة التي لا يريد أحد لمسها.

على مضيفات Linux طويلة العمر، راقب حجم السجلات أيضًا. برنامج تشغيل السجلات (logging driver) الافتراضي json-file يحتفظ بكل ما يطبعه الـ container ما لم تضبط max-size وmax-file في إعدادات الـ daemon. وقد يملأ container ثرثار قرصًا كاملًا في عطلة نهاية أسبوع واحدة؛ وقد استُدعيت في مناوبة لهذا السبب بالضبط.

التنظيف

Terminal
docker rm -f notes          # stop and remove
docker container prune      # remove all stopped containers
docker image prune          # remove dangling images
docker system df            # what's using disk

تستطيع الآن بناء container وتشغيله ومراقبته وإيقافه كما يجب. القسم الثاني يحوّل هذه الـ image إلى image ترتاح لشحنها: أصغر، وبمستخدم غير root، وذات إصدار واضح، ومفحوصة.

أهم النقاط

  • الخيار -p 8080:3000 يربط المنفذ 8080 على المضيف بالمنفذ 3000 في الـ container؛ الجهة اليسرى جهازك، والجهة اليمنى التطبيق.
  • مرّر الإعدادات وقت التشغيل بـ -e أو --env-file حتى تخدم image واحدة كل البيئات.
  • على الـ containers أن تكتب سجلاتها إلى stdout وstderr؛ فمن هناك يجمعها docker logs وكل منسّق (orchestrator).
  • الأمر docker stop يرسل SIGTERM، وينتظر 10 ثوانٍ، ثم يرسل SIGKILL؛ ورمز الخروج 137 يعني أن العملية قُتلت.
  • عملية Node.js في موقع PID 1 تحتاج إلى معالج SIGTERM خاص بها، وإلا فستتجاهل الإشارة وتُقتل في منتصف الطلبات.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

تشغّل docker run -d -p 8080:3000 notes-api:dev. أي عنوان يصل إلى الـ API من جهازك؟

السؤال 2 من 4

الأمر docker stop notes يستغرق عشر ثوانٍ بالضبط، وdocker ps -a يعرض Exited (137). ماذا حدث؟

السؤال 3 من 4

أين يجب أن يكتب notes-api سجلات الطلبات داخل الـ container؟

السؤال 4 من 4

يريد زميلك أن يستخدم docker exec لإصلاح خطأ إملائي في ملف إعدادات داخل container الإنتاج الذي يعمل الآن. ما المشكلة؟

تدريب

املأ الفراغات

أوامر الـ containers اليومية

املأ الفراغات لتشغيل notes-api في الخلفية على المنفذ 8080 في المضيف، ومتابعة سجلاته، وفتح shell داخله، وإيقافه مع مهلة سماح مدّتها 30 ثانية.

# Start notes-api in the background, reachable at localhost:8080 docker run --name notes -p :3000 notes-api:dev # Follow its output docker -f notes # Open a shell inside the running container docker -it notes sh # Stop it, allowing 30 seconds to drain docker stop 30 notes