EduFlow

الوحدة 3 · بيئات محلية مع Docker Compose

ترتيب الإقلاع مع الـ Healthchecks

18 دقراءة 4 داكتشف الخطأ

الدرس في لمحة

أعطِ Postgres وnotes-api فحوص healthcheck حقيقية، واجعل الـ API ينتظر قاعدة بيانات سليمة وmigration منتهية باستخدام شروط depends_on، وأبقِ منطق إعادة المحاولة في التطبيق مع ذلك.

  1. 1ما الذي ينتظره depends_on فعلًا
  2. 2الـ Healthchecks
  3. 3الانتظار على شروط
  4. 4إعادة المحاولة مكانها التطبيق مع ذلك

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

كل إقلاع بارد لبيئة notes-api كانت له الدقيقة الأولى المتقلّبة نفسها: يبدأ الـ API، ويحاول الاتصال بـ Postgres، فيحصل على ECONNREFUSED، وينهار. السطر depends_on: [db] في الملف بدا وكأنه كان يجب أن يمنع ذلك. لم يمنعه، لأن «بدأ» و«جاهز» شيئان مختلفان، وCompose لم يكن يعرف إلا الأول.

ما الذي ينتظره depends_on فعلًا

الصيغة المختصرة تقول: شغّل db قبل api.

YAML
services:
  api:
    depends_on:
      - db

يفعل Compose ذلك بالضبط: يشغّل container الـ db ثم container الـ api، بفارق ثانية تقريبًا. لكن Postgres يحتاج إلى عدّة ثوانٍ بعد بدء عمليته ليتهيّأ، وأكثر في التشغيل الأول حين ينشئ قاعدة البيانات ويشغّل سكربتات التهيئة. يتّصل الـ API خلال تلك النافذة فيُرفض.

لكي ينتظر Compose الجاهزية، يحتاج إلى طريقة لاختبارها. وهذا هو الـ healthcheck (فحص الصحة).

الـ Healthchecks

الـ healthcheck أمر يشغّله Docker داخل الـ container وفق جدول زمني. رمز الخروج 0 يعني سليم؛ وأي رمز آخر يُعدّ فشلًا.

compose.yaml
  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:

Terminal
docker inspect notes-db-1 --format '{{json .State.Health}}'

هناك شيء لا تفعله الـ healthchecks في Docker أو Compose العاديين: إنها لا تعيد تشغيل الـ container غير السليم. الحالة معلومة فقط، تستخدمها شروط depends_on وتستخدمها أنت. ‏Kubernetes مختلف، كما سترى مع الـ liveness probes في القسم الخامس، حيث يؤدّي الفحص الفاشل فعلًا إلى إعادة التشغيل. معرفة أي أداة تتصرّف عند فشل الفحص وأيها تكتفي بالإبلاغ عنه أمر مهمّ في الثالثة فجرًا.

الانتظار على شروط

الصيغة الطويلة لـ depends_on تتيح لك تحديد ما تنتظره:

compose.yaml
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 أيضًا.

خط زمني: يبدأ db ويصبح سليمًا بعد نجاح فحوصه؛ ثم تعمل migrate وتخرج بالرمز 0؛ ثم يبدأ api. db migrate api يبدأ سليم يعمل exit 0 يخدم الزمن
ترتيب الإقلاع مع الشروط

الـ API يستحق healthcheck هو الآخر. قد تكون image التشغيل من نوع distroless بلا curl، لكنها تحتوي دائمًا على Node، وNode 24 فيه fetch مدمجة. على node:24-slim يكون node ضمن الـ PATH؛ أما على distroless فاستدعِه بمساره الكامل /nodejs/bin/node:

YAML
    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 أصلًا.

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

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

الخدمة api فيها depends_on: [db] بالصيغة المختصرة. عند الإقلاع البارد يكتب الـ API في سجلّه ECONNREFUSED مرة واحدة، ثم يعمل. لماذا؟

السؤال 2 من 4

أي healthcheck يختبر جاهزية Postgres بشكل صحيح في خدمة db؟

السؤال 3 من 4

خدمة migrate تطبّق تغييرات المخطّط ثم تخرج. كيف يجب أن تعتمد api عليها؟

السؤال 4 من 4

بيئتك تستخدم healthchecks وservice_healthy. لماذا يجب أن يعيد notes-api محاولة الاتصالات الفاشلة بقاعدة البيانات مع ذلك؟

تدريب

اكتشف الخطأ

سليم على الورق

مع ملف compose.yaml هذا، تظهر db بحالة (unhealthy) إلى الأبد رغم أن psql يعمل جيدًا، وما زال الـ API ينهار عند الإقلاع البارد برسالة ECONNREFUSED. اختر السطرين الاثنين اللذين يجب إصلاحهما.

حدّد السطر أو الأسطر التي تظنها خاطئة، ثم تحقّق.