الوحدة 3 · بيئات محلية مع Docker Compose
ترتيب الإقلاع مع الـ Healthchecks
18 دقراءة 4 داكتشف الخطأ
الدرس في لمحة
أعطِ Postgres وnotes-api فحوص healthcheck حقيقية، واجعل الـ API ينتظر قاعدة بيانات سليمة وmigration منتهية باستخدام شروط depends_on، وأبقِ منطق إعادة المحاولة في التطبيق مع ذلك.
- 1ما الذي ينتظره depends_on فعلًا
- 2الـ Healthchecks
- 3الانتظار على شروط
- 4إعادة المحاولة مكانها التطبيق مع ذلك
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
كل إقلاع بارد لبيئة notes-api كانت له الدقيقة الأولى المتقلّبة نفسها: يبدأ الـ API، ويحاول الاتصال بـ Postgres، فيحصل على ECONNREFUSED، وينهار. السطر depends_on: [db] في الملف بدا وكأنه كان يجب أن يمنع ذلك. لم يمنعه، لأن «بدأ» و«جاهز» شيئان مختلفان، وCompose لم يكن يعرف إلا الأول.
ما الذي ينتظره depends_on فعلًا
الصيغة المختصرة تقول: شغّل db قبل api.
services:
api:
depends_on:
- dbيفعل Compose ذلك بالضبط: يشغّل container الـ db ثم container الـ api، بفارق ثانية تقريبًا. لكن Postgres يحتاج إلى عدّة ثوانٍ بعد بدء عمليته ليتهيّأ، وأكثر في التشغيل الأول حين ينشئ قاعدة البيانات ويشغّل سكربتات التهيئة. يتّصل الـ API خلال تلك النافذة فيُرفض.
لكي ينتظر Compose الجاهزية، يحتاج إلى طريقة لاختبارها. وهذا هو الـ healthcheck (فحص الصحة).
الـ Healthchecks
الـ healthcheck أمر يشغّله Docker داخل الـ container وفق جدول زمني. رمز الخروج 0 يعني سليم؛ وأي رمز آخر يُعدّ فشلًا.
db:
image: postgres:17
environment:
POSTGRES_USER: notes
POSTGRES_PASSWORD: notes
POSTGRES_DB: notes
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U notes -d notes"]
interval: 10s
timeout: 3s
retries: 5
start_period: 30s
start_interval: 1sيأتي pg_isready مع image Postgres ويخرج بالرمز 0 حالما يقبل الخادم الاتصالات. حقول التوقيت:
interval: كم مرة يُشغَّل الفحص بعد أن يعمل الـ container بشكل طبيعي.timeout: المدة التي قد يستغرقها فحص واحد قبل أن يُعدّ فاشلًا.retries: عدد الإخفاقات المتتالية قبل أن يُعلَّم الـ container بأنهunhealthy.start_period: مهلة سماح بعد البدء لا تُحتسب خلالها الإخفاقات ضمنretries.start_interval: كم مرة يُفحص خلال مهلة البدء، حتى يُكتشف البدء السريع بسرعة بدلًا من انتظارintervalكامل.
يعرض docker compose ps الآن (healthy) أو (unhealthy) بجوار الحالة. وحين يفشل فحص وتريد معرفة السبب، تُحفظ آخر بضع نتائج، مع مخرجاتها، على الـ container:
docker inspect notes-db-1 --format '{{json .State.Health}}'هناك شيء لا تفعله الـ healthchecks في Docker أو Compose العاديين: إنها لا تعيد تشغيل الـ container غير السليم. الحالة معلومة فقط، تستخدمها شروط depends_on وتستخدمها أنت. Kubernetes مختلف، كما سترى مع الـ liveness probes في القسم الخامس، حيث يؤدّي الفحص الفاشل فعلًا إلى إعادة التشغيل. معرفة أي أداة تتصرّف عند فشل الفحص وأيها تكتفي بالإبلاغ عنه أمر مهمّ في الثالثة فجرًا.
الانتظار على شروط
الصيغة الطويلة لـ depends_on تتيح لك تحديد ما تنتظره:
services:
migrate:
image: notes-api:dev
command: ["node", "dist/migrate.js"]
environment:
DATABASE_URL: postgres://notes:notes@db:5432/notes
depends_on:
db:
condition: service_healthy
api:
build: .
image: notes-api:dev
ports:
- "8080:3000"
environment:
DATABASE_URL: postgres://notes:notes@db:5432/notes
depends_on:
db:
condition: service_healthy
restart: true
migrate:
condition: service_completed_successfullyهناك ثلاثة شروط: service_started (سلوك الصيغة المختصرة)، وservice_healthy، وservice_completed_successfully الذي ينتظر container يُنفَّذ مرة واحدة حتى يخرج بالرمز 0. هنا تعمل الـ migration حالما يصبح Postgres سليمًا، ولا يبدأ الـ API إلا بعد نجاح الـ migration. إذا فشلت الـ migration فلن يبدأ api أبدًا، وسترى السبب في docker compose logs migrate. أما restart: true فيعني أن Compose إذا أعاد تشغيل db (مثلًا بعد تغيير إعداداته)، فسيعيد تشغيل api أيضًا.
الـ API يستحق healthcheck هو الآخر. قد تكون image التشغيل من نوع distroless بلا curl، لكنها تحتوي دائمًا على Node، وNode 24 فيه fetch مدمجة. على node:24-slim يكون node ضمن الـ PATH؛ أما على distroless فاستدعِه بمساره الكامل /nodejs/bin/node:
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:3000/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
interval: 10s
timeout: 3s
retries: 3في الـ CI، الأمر docker compose up -d --wait لا يعود إلا حين تصبح كل خدمة لها healthcheck سليمة، ويفشل إن صارت إحداها غير سليمة. هذه هي الطريقة الصحيحة لتشغيل البيئة قبل اختبارات التكامل، بدلًا من sleep 15.
إعادة المحاولة مكانها التطبيق مع ذلك
كل هذا يغطّي التشغيل الأول. لا شيء يمنع Postgres من إعادة التشغيل في الثالثة عصرًا من يوم ثلاثاء، وحين تنتقل إلى Kubernetes في القسم الرابع، لن تجد depends_on أصلًا: تبدأ الـ Pods بأي ترتيب تُجدوَل به. لذا يتّصل notes-api مع إعادة محاولة وbackoff، ويُبلغ «غير جاهز» على /readyz حتى تجيب قاعدة البيانات. الـ healthchecks والشروط تجعل الإقلاع المعتاد سريعًا؛ وإعادة المحاولة تجعل التطبيق يصمد حين تسوء الأمور.
البيئة الآن تقلع بالترتيب الصحيح في كل مرة. درس Compose الأخير يرتّب الإعدادات ويعطيك حلقة سريعة للتعديل وإعادة التحميل.
أهم النقاط
- الصيغة المختصرة لـ
depends_onلا تنتظر إلا بدء container الاعتمادية، لا جاهزيته للخدمة. - الـ
healthcheckيخبر Docker كيف يختبر الـ container؛ والشرطcondition: service_healthyيجعل الخدمة المعتمِدة تنتظر نجاح ذلك الاختبار. - الشرط
condition: service_completed_successfullyيشغّل المهام التي تُنفَّذ مرة واحدة، مثل الـ migrations، قبل بدء التطبيق. - في صيغة exec يكون كل معامل عنصرًا مستقلًا في القائمة؛ واستخدم
CMD-SHELLحين يكون الاختبار نصّ أمر shell واحدًا. - ترتيب الإقلاع لا يغطّي إلا التشغيل الأول، لذا يجب أن يعيد التطبيق محاولة الاتصال بقاعدة البيانات؛ وKubernetes ليس فيه
depends_onأصلًا.