الوحدة 1 · الـ Containers من المبادئ الأولى
تشغيل الـ Containers: المنافذ ومتغيّرات البيئة والسجلات والإشارات
18 دقراءة 4 داملأ الفراغات
الدرس في لمحة
شغّل notes-api في الخلفية مع المنافذ ومتغيّرات البيئة وحدود الموارد، واقرأ سجلاته، وافتح shell داخله، واجعله يتوقّف توقّفًا نظيفًا عند استقبال SIGTERM.
- 1شغّله في الخلفية
- 2انظر إلى ما يعمل وما يقوله
- 3الإيقاف: SIGTERM ثم SIGKILL
- 4سياسات إعادة التشغيل ونموّ السجلات
- 5التنظيف
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
بناء الـ image نصف العمل؛ والنصف الآخر هو تشغيلها بالطريقة التي سيشغّلها بها الإنتاج. في العام الماضي، كانت إحدى خدمات Skylane تُسقط بضع مئات من الطلبات مع كل deploy. لم يكن في الكود أي خلل؛ كل ما في الأمر أن العملية كانت تتجاهل إشارة «من فضلك توقّف» المهذّبة، فتُقتل بعد عشر ثوانٍ في منتصف الطلبات. يتناول هذا الدرس جانب التشغيل، ويختم بمسألة الإيقاف تلك.
شغّله في الخلفية
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 حقيقية في القسم الثالث.
انظر إلى ما يعمل وما يقوله
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.
ولإلقاء نظرة في الداخل:
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 ثوانٍ. وتموت معه الطلبات الجارية.
عالِج الإشارة: توقّف عن قبول الاتصالات، وأنهِ ما هو جارٍ، وأغلق مجمّع اتصالات قاعدة البيانات، ثم اخرج.
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 ثرثار قرصًا كاملًا في عطلة نهاية أسبوع واحدة؛ وقد استُدعيت في مناوبة لهذا السبب بالضبط.
التنظيف
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 خاص بها، وإلا فستتجاهل الإشارة وتُقتل في منتصف الطلبات.