النشر9 دقيقة قراءة

depends_on لا يكفي: healthcheck لـ PostgreSQL في Compose

تنطلق حاوياتك Docker، وتنتقل قاعدة البيانات إلى حالة `running` — ثم تتعطل تطبيقاتك فوراً برسالة `connection refused` أو `FATAL: role does not exist`. السبب في الغالب واحد: الشكل الافتراضي لـ `depends_on` ينتظر انطلاق الحاوية فقط، لا جاهزية الخدمة فعلياً. يوضح هذا الدليل كيفية إعداد healthcheck موثوق لـ PostgreSQL في ملف `docker-compose.yml` للتخلص من race condition مرة واحدة وإلى الأبد.

لماذا يفشل `depends_on` الافتراضي

يستخدم depends_on افتراضياً شرط service_started، مما يعني أن Docker ينتظر فقط انطلاق الحاوية المستهدفة — أي أن عمليتها الرئيسية قد بدأت. هذا لا يقول شيئاً عن الحالة الداخلية للخدمة.

يمرّ PostgreSQL، كمعظم قواعد البيانات، بعدة مراحل عند التهيئة: تُنفّذ الصورة الرسمية سكريبتات الإقلاع، وتنشئ الأدوار، وتُهيّئ الإضافات، وتُعدّ الـ cluster قبل أن تبدأ في قبول الاتصالات. يمكن أن تستغرق هذه العملية من ثوانٍ إلى أكثر من ثلاثين ثانية على VPS بقرص دافئ أو حجم غير محضَّر أو إضافات كثيرة.

خلال هذا الوقت، يحاول تطبيقك — الذي يحترم توجيه depends_on — الاتصال بقاعدة البيانات، فيتلقى رفضاً صريحاً.

العواقب العملية لـ race condition عند الإقلاع

  • connection refused — مقبس TCP الخاص بـ PostgreSQL لم يُفتح بعد، ويفشل التطبيق عند أول استدعاء PDO أو SQLAlchemy.
  • FATAL: role does not exist — PostgreSQL يستمع، لكن سكريبتات التهيئة (docker-entrypoint-initdb.d) لم تُنشئ بعد الدور أو قاعدة البيانات.
  • FATAL: the database system is starting up — الـ cluster في طور الاسترداد بعد إيقاف نظيف؛ الاتصالات مرفوضة مؤقتاً.
  • حلقة تعطل صامتة — يعيد Docker تشغيل التطبيق باستمرار بفضل restart: unless-stopped، وتتكرر السجلات، ويبدو الأمر كخطأ في التطبيق.
  • نتائج كاذبة في CI — تفشل اختبارات التكامل بصورة متقطعة بحسب سرعة الـ runner.
  • تبعيات متسلسلة — إذا كانت واجهة برمجية تعتمد على تطبيق يعتمد على قاعدة بيانات، توارثت المشكلة ذاتها إن لم تكن سلسلة depends_on صحيحة في كل حلقة.

المتطلبات الأساسية: Docker Compose v2 والإضافة الرسمية

شرط service_healthy غير متاح في Docker Compose v1 (ثنائي Python المُهمَل docker-compose). مدعوم منذ Docker Compose v2، الموزَّع كإضافة Go تحت أمر docker compose (بدون شرطة).

للتحقق من إصدارك:

docker compose version

يجب أن تُظهر المخرجات Docker Compose version v2.x.x أو أعلى. على Debian 12 وUbuntu 22.04+، الإضافة متاحة من مستودعات Docker الرسمية. إن كان لديك docker-compose (v1) بعد، انتقل: المشروع مُؤرشَف ولا يتلقى إصلاحات أمنية.

لا تحتاج إلى أي تبعية إضافية لـ PostgreSQL: ‏pg_isready أداة أصيلة في الصورة الرسمية postgres، موجودة في جميع الـ tags منذ سنوات.

إعداد healthcheck موثوق لـ PostgreSQL خطوة بخطوة

01

فهم الفرق بين service_started و service_healthy

يقبل depends_on ثلاثة شروط:

- service_started (افتراضي) — ينتظر انطلاق الحاوية فقط.
- service_healthy — ينتظر حتى يُعيد healthcheck الحاوية healthy.
- service_completed_successfully — للحاويات قصيرة العمر (jobs، migrations).

لأي قاعدة بيانات، service_healthy هو الشرط الوحيد الذي يضمن أن الخدمة تقبل الاتصالات فعلاً.

02

كتابة healthcheck الخاص بـ PostgreSQL في خدمة `db`

أضف كتلة healthcheck مباشرة في تعريف خدمة db:

services:
  db:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: secret
      POSTGRES_DB: appdb
    healthcheck:
      test: ["CMD", "pg_isready", "-U", "app", "-d", "appdb"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 30s

يستقبل الحقل test قائمة: العنصر الأول CMD (يُنفّذ Docker الأمر مباشرة)، تليه الوسائط. يُعيد pg_isready القيمة 0 إذا كان PostgreSQL جاهزاً لقبول الاتصالات بالمستخدم وقاعدة البيانات المحددَين، وكوداً غير صفري إذا لم يكن كذلك — وهو ما يُفسّره Docker على أنه healthy أو unhealthy.

03

فهم دور `start_period`

start_period هو فترة السماح الممنوحة للحاوية للتهيئة قبل أن تبدأ إخفاقات healthcheck في الاحتساب ضمن retries. خلال هذه الفترة، ينفّذ Docker healthcheck، لكن الإخفاق لا يزيد العداد.

بدون start_period، فإن PostgreSQL الذي يستغرق 15 ثانية للتهيئة سيُخفق في أول 5 فحوصات (interval: 5s × 5 محاولات = 25 ثانية) وينتهي unhealthy قبل أن يصبح تشغيلياً.

القيمة الموصى بها هي 30 ثانية لـ PostgreSQL القياسي: كافية لاستيعاب التهيئات البطيئة (أول إقلاع بحجم فارغ، إضافات ثقيلة) دون تأخير غير ضروري عند الإقلاع في الظروف العادية. interval وstart_period متمايزان: interval يُوقّت الفحوصات في التشغيل الطبيعي، وstart_period يحمي مرحلة الإقلاع.

04

كتابة `depends_on` مع `condition: service_healthy`

في كل خدمة تعتمد على قاعدة البيانات، استبدل الشكل المختصر لـ depends_on بالشكل المفصّل مع الشرط:

services:
  app:
    image: myapp:latest
    depends_on:
      db:
        condition: service_healthy
    environment:
      DATABASE_URL: postgresql://app:secret@db:5432/appdb

بهذا الإعداد، ينتظر Docker حتى يُعيد healthcheck خدمة db القيمة healthy قبل تشغيل app. إذا أصبح db بحالة unhealthy بعد retries إخفاق، لا تنطلق app.

05

الاختبار باستخدام `docker compose up`

أطلق الـ stack وراقب التسلسل:

docker compose up

سترى في السجلات أسطراً من قبيل:

db  | database system is ready to accept connections
app | Waiting for db to be healthy...
app | Starting application server

للتحقق من حالة healthcheck في أي وقت:

docker inspect <db_container_name> | grep -A 5 '"Health"'

تُظهر المخرجات Status: healthy أو starting أو unhealthy، وتسرد مخرجات آخر الفحوصات.

06

حالة Redis: healthcheck مُكيَّف

لا يمتلك Redis أداة redis-isready، لكن معادلها هو redis-cli ping:

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

يُعيد redis-cli ping القيمة PONG وكود الخروج 0 إذا كان Redis يقبل الاتصالات. بما أن Redis يبدأ أسرع من PostgreSQL، تكفي start_period: 10s في الغالب.

07

حالة MySQL / MariaDB: استخدام `mysqladmin ping`

لـ MySQL أو MariaDB، استخدم mysqladmin ping:

  mysql:
    image: mariadb:11
    environment:
      MYSQL_ROOT_PASSWORD: secret
      MYSQL_DATABASE: appdb
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-psecret"]
      interval: 5s
      timeout: 5s
      retries: 5
      start_period: 30s

تنبيه: يُدمج كلمة المرور مباشرة مع -p بلا مسافة (-psecret)، وهو السلوك المتوقع لـ mysqladmin. يظهر هذا الأمر في مخرجات docker inspect، لذا فضّل استخدام Compose secret أو متغير بيئة إن كانت السرية مطلوبة.

08

استكشاف الأخطاء: أربعة أخطاء شائعة

pg_isready: command not found — لا تستخدم الصورة الرسمية postgres (أو صورة مشتقة تتضمنها). تحقق باستخدام: docker compose exec db which pg_isready.

healthcheck في حلقة، لا يصل أبداً إلى healthy — لا يُعيد test القيمة 0. اختبر يدوياً: docker compose exec db pg_isready -U app -d appdb. إذا فشل الأمر، تحقق من متغيرات POSTGRES_USER وPOSTGRES_DB.

start_period قصير جداً — عند أول إقلاع بحجم فارغ، قد يستغرق PostgreSQL أكثر من 30 ثانية. ارفع القيمة إلى 60s أو راقب السجلات: database system was shut down at … LOG: database system is ready to accept connections يُشير إلى التأخير الفعلي.

الحاوية في حالة unhealthy دائمة — بعد retries إخفاق، يُصنّف Docker الحاوية unhealthy لكنه لا يُعيد تشغيلها (ذلك دور restart). راجع docker inspect لمعرفة مخرجات آخر الفحوصات وتحديد الأمر الفاشل.

السلوك الافتراضي مقابل السلوك مع healthcheck

الحالةالسلوك الافتراضي (`service_started`)مع `service_healthy`
أول إقلاع، حجم فارغيبدأ التطبيق قبل جاهزية قاعدة البيانات → حلقة تعطلينتظر التطبيق حتى تكتمل تهيئة PostgreSQL ويقبل الاتصالات
إعادة التشغيل بعد إيقاف نظيفقد يبدأ التطبيق خلال مرحلة استرداد PostgreSQLينتظر التطبيق حتى اكتمال الاسترداد
قاعدة بيانات بطيئة (إضافات، تهيئة ثقيلة)race condition تعتمد على سرعة المضيفلا race condition: healthcheck يتحقق من الحالة الفعلية
تبعيات متسلسلة (app → worker → db)كل مكوّن يُدير محاولات إعادة الاتصال بنفسهسلسلة الشروط تضمن ترتيب الإقلاع
اختبارات التكامل في CIنتائج متقطعة تعتمد على سرعة الـ runnerنتائج حتمية ومتسقة
Redis أو MySQL بدلاً من PostgreSQLالمشكلة ذاتها، `depends_on` الافتراضي لا يُميّزالحل ذاته، أمر الفحص مُكيَّف لكل محرك

`pg_isready` أم `SELECT 1`: أيهما تختار؟

يظهر نوعان من healthcheck لـ PostgreSQL في الاستخدام الشائع:

- ["CMD", "pg_isready", "-U", "postgres"]
- ["CMD-SHELL", "psql -U postgres -c 'SELECT 1'"]"

pg_isready أكثر موثوقية لسبب بسيط: يختبر فقط قدرة الخادم على قبول اتصالات TCP، دون فتح جلسة SQL. يُعيد القيمة 0 حالما يكون الخادم مستمعاً ويقبل المصافحة، وهو بالضبط ما يحتاجه التطبيق لمحاولة اتصاله الخاص.

أما SELECT 1 عبر psql، فيفتح جلسة SQL حقيقية وينفّذ استعلاماً. هو اختبار أعمق، لكنه قد يفشل لأسباب لا علاقة لها بتوافر الخادم (استنفاد حصة الاتصالات، pg_hba.conf غير مُعدَّ بصواب). للـ healthcheck، الاختبار الأدنى والمباشر هو الأفضل.

تكييف فحص الصحة لـ Redis وMySQL

بالنسبة لـ ‎Redis، استبدل ‎pg_isready بـ ‎CMD redis-cli PING — تُرجع ‎PONG فور قبول الخادم للاتصالات. بالنسبة لـ ‎MySQL أو ‎MariaDB، استخدم ‎CMD mysqladmin ping -h localhost -u root --password=$$MYSQL_ROOT_PASSWORD مع رفع ‎start_period إلى ‎60s، إذ يستغرق تهيئة ‎MySQL وقتاً أطول من ‎PostgreSQL. نمط ‎condition: service_healthy متطابق بغض النظر عن الخدمة المستهدفة.

الخطوات التالية

يُعدّ healthcheck مع service_healthy أحد إعدادات المتانة الواجب تفعيلها في الإنتاج. تستحق عدة نقاط أخرى الاهتمام ذاته قبل أي نشر مستدام: سياسة إعادة التشغيل restart: unless-stopped، حدود الموارد deploy.resources.limits، وتدوير السجلات logging.options. يمكنك الاطلاع على القائمة الكاملة في دليل Docker Compose في الإنتاج: قائمة التحقق من 10 نقاط.

إذا نما الـ stack — خدمات متعددة أو مضيفون متعددون — فإن reverse proxy كـ Caddy أو Traefik ضروري لإدارة توجيه HTTPS. يفصّل دليل Caddy أو Traefik أو Nginx Proxy Manager معايير الاختيار حسب ملفك.

لأتمتة نشر بنيتك التحتية بالكامل (VPS، Docker، الإعداد) بطريقة قابلة للتكرار، يرشدك دليل Ansible لأتمتة خوادم VPS خطوة بخطوة.

استضف stack Docker الخاص بك على VPS مخصص

يمنحك VPS ServOrbit وصولاً بصلاحيات root وعنوان IPv4 مخصصاً والموارد اللازمة لتشغيل stacks Docker Compose في الإنتاج. انشر في دقائق.

بحاجة إلى مساعدة؟

تصفّح مركز المساعدة والأسئلة الشائعة، أو تواصل مع فريقنا — معاودة اتصال أو WhatsApp أو بريد إلكتروني. الدعم بـ العربية والفرنسية والإنجليزية.