SSD Nodes Learn 8GB RAM — $66/سنة
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-08-01

فحوص صحة Docker Compose التي تعمل فعلًا

تعرّف إلى كيفية تقييم Docker Compose لفحص الصحة، ولماذا لا ينتظر depends_on جاهزية مفيدة، وكيف تكتب فحوصًا صحيحة لـ Postgres وتطبيقك.

ما الذي يفعله فحص الصحة في Docker Compose فعليًا

فحص الصحة في Docker Compose هو أمر واحد يشغّله Docker داخل الحاوية وفق مؤقت. لا يقرأ Docker سجلاتك، ولا يراقب منفذك، ولا يفحص قائمة عملياتك. يشغّل الأمر، ويقرأ رمز الخروج، ويخزّن حالة واحدة للحاوية: starting أو healthy أو unhealthy. يعني رمز الخروج 0 أن الحاوية سليمة. ويعني أي رمز خروج آخر أنها غير سليمة. أما رمز الخروج 2 فهو محجوز لدى Docker، لذلك لا تُرجعه عمدًا.

هذه هي الآلية كاملة. تكاد تكون كل مشكلة في فحص الصحة ناتجة عن السبب نفسه: الأمر الذي كتبته يجيب عن سؤال مختلف عن السؤال الذي قصدت طرحه. يفترض هذا الدليل أنك تعرف مسبقًا كيفية كتابة ملف compose على VPS، وينتقل إلى النقطة التي تبدأ فيها المجموعة بالترتيب الخطأ.

services:
  api:
    image: ghcr.io/example/api:1.4.0
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s

تأخذ قيمة test شكلين مفيدين. تشغّل القائمة التي تبدأ بـ CMD الأمر مباشرةً، من دون shell، لذلك لا تعمل الأنابيب ولا && ولا توسيع المتغيرات. تمرّر القائمة التي تبدأ بـ CMD-SHELL بقية العناصر كسلسلة واحدة إلى /bin/sh -c داخل الحاوية. وهذا ما تريده عندما يحتاج الفحص إلى صياغة shell. وتُعامل السلسلة النصية البسيطة على أنها CMD-SHELL. وتزيل قائمة تتكون من ["NONE"] فقط فحص الصحة الذي أُدرج في الصورة من خلال Dockerfile.

يعمل الفحص داخل الحاوية، لذلك يجب أن تكون كل ثنائية يذكرها موجودة في تلك الصورة. تحقّق من ذلك أولًا، لأن الصورة الخفيفة التي لا تحتوي على curl تنشئ حاوية تظل غير سليمة دائمًا، من دون أن يظهر السبب في سجل التطبيق. اختبر ذلك يدويًا:

docker compose exec api curl --version

تُرجع الثنائية المفقودة OCI runtime exec failed: exec: "curl": executable file not found in $PATH: unknown. وعادةً ما تتضمن الصور المبنية على Alpine wget من BusyBox بدلًا منها، لذلك يصبح الفحص ["CMD", "wget", "-q", "-O", "-", "http://localhost:8080/healthz"].

كيفية اجتماع interval وretries وstart_period

تتحكم خمسة إعدادات في التوقيت. تأتي قيمها الافتراضية من Docker Engine، وليس من Compose.

  • interval: المدة بين فحصين بعد تجاوز الحاوية فترة البدء. القيمة الافتراضية 30s.
  • timeout: المدة القصوى لتنفيذ الفحص الواحد قبل أن ينهي Docker التنفيذ ويحتسبه فشلًا. القيمة الافتراضية 30s.
  • retries: عدد حالات الفشل المتتالية المطلوبة قبل انتقال الحالة إلى unhealthy. القيمة الافتراضية 3.
  • start_period: فترة سماح بعد بدء الحاوية. القيمة الافتراضية 0s.
  • start_interval: معدل تشغيل الفحص أثناء فترة البدء. القيمة الافتراضية 5s، ويتطلب ذلك Docker Engine 25.0 أو إصدارًا أحدث.

القاعدة المهمة هي أن الفحص الفاشل أثناء فترة البدء لا يُحتسب ضمن retries، وتبقى الحاوية في starting. عند نجاح الفحص للمرة الأولى، تصبح الحاوية healthy وتنتهي فترة البدء فورًا، حتى إذا لم تُستهلك معظم مدتها. إذا انتهت فترة البدء بينما لا يزال الفحص يفشل، يبدأ العدّاد العادي، وتحتاج الحاوية إلى retries حالات فشل متتالية قبل وضع علامة unhealthy عليها.

لذلك، فإن أسوأ مدة من بدء الحاوية إلى وصولها إلى unhealthy هي start_period زائد retries مضروبة في interval زائد timeout. وبالقيم الموجودة في الملف أعلاه، تكون المدة 30 زائد 5 مضروبة في 13، أي 95 ثانية. دوّن هذا الرقم قبل ضبط مهلة النشر، لأن عملية نشر تتوقف بعد 60 ثانية لن ترى هذه الحاوية تصل إلى حالة نهائية.

الخطأ الشائع هنا هو زيادة retries لاستيعاب البدء البطيء. ينجح ذلك مرة واحدة، ثم يسبب مشكلة دائمًا: فخدمة احتاجت إلى 8 محاولات لإقلاعها ستتحمل الآن 8 حالات فشل متتالية في بيئة الإنتاج قبل أن يلاحظ أي شيء المشكلة. استخدم start_period بدلًا من ذلك، لأنه يُطبَّق فقط قبل النجاح الأول.

لماذا لا يضمن depends_on بمفرده أي شيء

الصيغة المختصرة من depends_on هي مصدر معظم الالتباس.

  api:
    depends_on:
      - db

هذا يعني شيئًا واحدًا: شغّل الحاوية db قبل الحاوية api. ينتظر Compose إنشاء الحاوية وتشغيلها. لكنه لا ينتظر انتهاء PostgreSQL من التهيئة الأولى، ولا ينتظر أن يقبل المنفذ 5432 اتصالًا. يبدأ تطبيقك بعد نحو ثانية، ويحاول الاتصال بمنفذ لا يستمع إليه أي شيء بعد، ثم يتوقف. سترى في السجل Connection refused، أو FATAL: the database system is starting up عندما يكون الخادم قيد التشغيل لكنه لا يزال في مرحلة الاسترداد.

الصيغة الطويلة هي ما يحتاج إليه المستخدمون فعليًا:

  api:
    depends_on:
      db:
        condition: service_healthy
        restart: true
      migrate:
        condition: service_completed_successfully

يحتوي condition على ثلاث قيم. service_started مطابقة للصيغة المختصرة. تؤخر service_healthy تشغيل الخدمة التابعة إلى أن تبلغ التبعية عن حالتها السليمة، وهذا لا يكون ذا معنى إلا عندما تعرّف تلك التبعية healthcheck، سواء في ملف Compose أو في صورتها. ينتظر service_completed_successfully حاوية تعمل مرة واحدة، مثل ترحيل قاعدة بيانات، إلى أن تنتهي بحالة 0.

يوجد حقلاّن إضافيان بجوار condition. يخبر restart: true Compose بإعادة تشغيل هذه الخدمة بعد تحديث خدمة التبعية. يحوّل required: false التبعية المفقودة من خطأ إلى تحذير.

والآن نصل إلى الحد الذي يسبب المشكلات. تُقيَّم هذه الشروط عند بدء الحزمة. إنها تحدد ترتيب البدء، وليست قاعدة للإشراف. إذا أعادت قاعدة البيانات التشغيل عند الساعة الثالثة صباحًا، فلن يعيد أي شيء تقييم service_healthy، ولن يعيد تشغيل تطبيقك لتحقيق الشرط مرة أخرى. يجب أن يتولى رمز تطبيقك إعادة الاتصال بنفسه. يتجاوز docker compose up --no-deps api الآلية بأكملها عن قصد، وينطبق الأمر نفسه على تشغيل حاوية مباشرة باستخدام docker start.

اكتب فحصًا يختبر الجاهزية، لا مجرد وجود عملية

يثبت فحص مثل pgrep nginx وجود إدخال للعملية في جدول العمليات. لكنه لا يثبت أن الخدمة تستطيع الاستجابة لطلب. قد يحتفظ تطبيق ويب بمقبس الاستماع مفتوحًا بعد تعطل تجمع اتصالات قاعدة البيانات بوقت طويل، ويظل فحص العملية ناجحًا طوال فترة الانقطاع.

اطلب من الحاوية تنفيذ المهمة التي أنشئت من أجلها:

  • بالنسبة إلى خدمة HTTP، اطلب نقطة نهاية فعلية. يخرج curl -fsS بحالة غير صفرية عند أي حالة تساوي 400 أو تتجاوزها بسبب -f، ولذلك تُعد استجابة 500 من تطبيق معطل فحصًا فاشلًا.
  • بالنسبة إلى PostgreSQL، استخدم pg_isready. يخرج بحالة 0 عندما يقبل الخادم الاتصالات، وبحالة 1 عندما يرفضها، وبحالة 2 عندما لا يستجيب إطلاقًا، وبحالة 3 عندما تكون المعلمات التي مررتها غير صحيحة.
  • بالنسبة إلى Redis، استخدم redis-cli ping. يطبع PONG ويخرج بحالة 0.
  • بالنسبة إلى MariaDB، تتضمن الصورة الرسمية البرنامج النصي healthcheck.sh، وhealthcheck.sh --connect --innodb_initialized هي الصيغة التي يوثقها مشرفوها.

يتضمن pg_isready نقطة مهمة يجب معرفتها. عند أول تشغيل له مع دليل بيانات فارغ، تشغّل صورة postgres الرسمية عملية التهيئة على خادم مؤقت يستمع إلى مقبس Unix فقط. يستخدم pg_isready، عند عدم تمرير وسيطة المضيف، ذلك المقبس، ولذلك قد يجيب بأن الاتصالات مقبولة بينما يظل منفذ TCP 5432 مغلقًا أمام تطبيقك. وجّه الفحص إلى TCP صراحةً، وتنتهي المشكلة، لأن الخادم المؤقت لا يستجيب عبره.

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s

علامتا الدولار المزدوجتان ليستا خطأً مطبعيًا. يوسّع Compose قيمة $VAR بنفسه أثناء قراءة الملف، مما يضمّن قيمة من بيئة المضيف في الفحص. يهرب $$ هذه القيمة لتصبح $ واحدة، ولذلك توسّعها الصدفة داخل الحاوية باستخدام بيئة الحاوية نفسها.

مكدس PostgreSQL والتطبيق الذي يبدأ بالترتيب الصحيح

services:
  db:
    image: postgres:17.5
    environment:
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
      POSTGRES_DB: appdb
    volumes:
      - pgdata:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -h 127.0.0.1 -p 5432 -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 30s
    restart: unless-stopped

  api:
    image: ghcr.io/example/api:1.4.0
    environment:
      DATABASE_URL: postgres://appuser:${DB_PASSWORD}@db:5432/appdb
    depends_on:
      db:
        condition: service_healthy
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/healthz"]
      interval: 10s
      timeout: 3s
      retries: 5
      start_period: 30s
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

volumes:
  pgdata:

شغّل المكدس وراقب تغيّر الحالات:

docker compose up -d
docker compose ps

يحمل العمود STATUS حالة الصحة بين قوسين. يظهر الزوج السليم بالقيمة Up 41 seconds (healthy) في كلا الصفين. أثناء استمرار تهيئة قاعدة البيانات، تكون قيمة db هي Up 4 seconds (health: starting)، ولا يظهر api في القائمة لأن Compose لم ينشئه بعد.

لمعرفة سبب نجاح الفحص أو فشله، اقرأ سجل الصحة:

docker inspect --format '{{json .State.Health}}' "$(docker compose ps -q db)"

يحتفظ Docker بالنتائج القليلة الأخيرة، ويحتوي كل منها على وقت بدء، ووقت انتهاء، وExitCode، وOutput الخاص بالأمر. يكون الإخراج المخزّن مبتورًا، لذلك يؤدي الفحص الذي يطبع نص صفحة كبيرًا إلى إدخال سجل غير مفيد. اجعل الفحوصات صامتة.

ما يفعله Docker عندما تصبح حاوية غير سليمة

لا شيء. هذه هي الإجابة التي تفاجئ معظم الناس.

لا يعيد Docker Engine على مضيف واحد تشغيل حاوية غير سليمة. تتفاعل سياسة restart: unless-stopped مع خروج العملية الرئيسية، بينما الحاوية غير السليمة لم تخرج. ويمكن أن تبقى في الحالة unhealthy لمدة أسبوع، في حين يتركها Compose دون تدخل. يستبدل وضع Swarm المهام غير السليمة، لكن مكدس Compose عاديًا على خادم واحد لا يفعل ذلك.

لديك خياران عمليان واضحان. اجعل العملية تخرج عندما تعرف أنها معطلة، لكي تتوفر لسياسة إعادة التشغيل حالة تتعامل معها. أو راقب الحالة من خارج الحاوية وأصدر تنبيهًا عند اكتشافها. إن توجيه مراقب Uptime Kuma إلى نقطة النهاية نفسها التي يستدعيها فحص الصحة يعني أن التبعية المعطلة ستظهر في الموضعين، وستتلقى إشعارًا من المراقب بدلًا من المستخدم. وإذا وصلت حركة الشبكة إلى التطبيق عبر وكيل Traefik العكسي، فتذكر أن طريقة عرض الوكيل لحالة الواجهة الخلفية منفصلة عن حالة صحة Docker، ولذلك لا يغطي أحدهما الآخر.

تصحيح أخطاء فحص لا يصبح سليمًا أبدًا

شغّل الأمر نفسه تمامًا في الحاوية نفسها، وتحقق من رمز الخروج:

docker compose exec api curl -fsS http://localhost:8080/healthz; echo "exit=$?"

وجود exit=0 هنا بينما تظل الحاوية تبلغ عن أنها غير سليمة يعني أن test في Compose يختلف عما كتبته للتو، ويحدث ذلك عادةً لأنك استخدمت CMD بدلًا من صياغة shell المطلوبة.

هناك خطآن يفسران معظم الحالات الأخرى. الأول هو استخدام المنفذ الخطأ. يعمل فحص الصحة داخل الحاوية، لذلك يجب أن يستخدم منفذ الحاوية، وليس منفذ المضيف المنشور. مع ports: - "8080:3000" يستمع التطبيق على المنفذ 3000، ويفشل الفحص باستخدام http://localhost:8080 باستمرار، بينما يعمل الموقع بشكل طبيعي في المتصفح. والثاني هو استخدام المضيف الخطأ. داخل الفحص، يشير localhost إلى الحاوية نفسها، وهذا صحيح لفحصها، لكنه خطأ عند فحص حاوية مجاورة؛ في هذه الحالة تحتاج إلى اسم الخدمة، مثل db.

هناك حالة أخيرة تستحق التمييز: ينجح فحص الصحة بينما يرى المستخدمون أخطاء. يحدث ذلك عندما تعيد نقطة النهاية الحالة 200 بصورة ثابتة من دون الوصول إلى أي مكوّن فعلي. لا يمكن لنقطة نهاية الجاهزية التي لا تستعلم من قاعدة البيانات أن تخبرك بأن قاعدة البيانات توقفت. اجعلها تنفّذ استعلامًا حقيقيًا بسيطًا واحدًا.

FAQ

لماذا يفشل تطبيقي في الاتصال رغم أن depends_on يشير إلى أن قاعدة البيانات سليمة؟

لأن condition: service_healthy يُقيَّم مرة واحدة عند بدء المكدس. ولا يراقب أي شيء بعد ذلك. إذا أعيد تشغيل حاوية قاعدة البيانات لاحقًا، فلن يعيد Compose تشغيل تطبيقك لتلبية الشرط مرة أخرى. لذلك يحتاج كود التطبيق إلى منطق خاص به لإعادة الاتصال وإعادة المحاولة. ولا يفعل الشرط شيئًا أيضًا عند بدء حاوية واحدة باستخدام docker start أو docker compose up --no-deps.

هل أحتاج إلى healthcheck إذا كانت الصورة تعرّف واحدًا بالفعل؟

غالبًا لا. ويؤدي تجاوز التعريف الموجود غالبًا إلى نتيجة أسوأ، لأن مسؤول صيانة الصورة يعرف معنى الجاهزية لذلك البرنامج. أضف تعريفًا خاصًا بك فقط عندما يكون فحص الصورة غير مناسب لإعدادك، مثلًا عندما يفحص منفذًا نقلته. لتعطيل healthcheck المعرّف في الصورة، عيّن test: ["NONE"] أو disable: true للخدمة.

هل ينبغي أن يستخدم healthcheck الأمر curl أم wget؟

استخدم الموجود منهما في الصورة، وتحقق من ذلك باستخدام docker compose exec <service> curl --version قبل الاعتماد عليه. لا تحتوي كثير من الصور المبنية على Debian على أي منهما. وتحتوي الصور المبنية على Alpine على wget من BusyBox. لا تضف حزمة إلى الصورة لمجرد تشغيل healthcheck عندما يوفّر البرنامج عميله الخاص، مثل pg_isready أو redis-cli.

هل تُعاد حاوية حالتها غير سليمة إلى التشغيل تلقائيًا؟

ليس بواسطة Docker Engine على مضيف واحد. تستجيب سياسات إعادة التشغيل لخروج العملية، لا لحالة السلامة. لذلك تظل الحاوية غير السليمة قيد التشغيل وتظل معطلة إلى أن يتخذ شيء آخر إجراءً. إما أن تجعل العملية تخرج عند اكتشاف الفشل، أو شغّل أداة مراقبة خارجية تنبّه عند تغير الحالة.

ما المدة المناسبة لـ start_period؟

اجعلها طويلة بما يكفي لتغطية أبطأ بدء أول مشروع قسته، مع إضافة هامش. قِس المدة باستخدام docker compose up مع وحدة تخزين فارغة، لأن بدء قاعدة البيانات لأول مرة أبطأ بكثير من كل عملية بدء لاحقة. تؤدي مدة البدء الطويلة جدًا فقط إلى تأخير حكم unhealthy الأول. أما رفع عدد مرات إعادة المحاولة أكثر من اللازم فيضعف الفحص طوال عمر الحاوية، وهذا هو الفشل الأسوأ.

#docker-compose#healthcheck#depends-on#Docker#reliability