{"id":"l-dk-3-3","courseId":"docker-kubernetes","lang":"ar","kind":"lesson","summary":"أعطِ Postgres وnotes-api فحوص healthcheck حقيقية، واجعل الـ API ينتظر قاعدة بيانات سليمة وmigration منتهية باستخدام شروط <code dir=\"ltr\">depends_on</code>، وأبقِ منطق إعادة المحاولة في التطبيق مع ذلك.","html":"<p>كل إقلاع بارد لبيئة notes-api كانت له الدقيقة الأولى المتقلّبة نفسها: يبدأ الـ API، ويحاول الاتصال بـ Postgres، فيحصل على <code dir=\"ltr\">ECONNREFUSED</code>، وينهار. السطر <code dir=\"ltr\">depends_on: [db]</code> في الملف بدا وكأنه كان يجب أن يمنع ذلك. لم يمنعه، لأن «بدأ» و«جاهز» شيئان مختلفان، وCompose لم يكن يعرف إلا الأول.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">ما الذي ينتظره depends_on فعلًا</h2>\n<p>الصيغة المختصرة تقول: شغّل <code dir=\"ltr\">db</code> قبل <code dir=\"ltr\">api</code>.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    depends_on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">db</span></span></code></pre></div>\n<p>يفعل Compose ذلك بالضبط: يشغّل container الـ <code dir=\"ltr\">db</code> ثم container الـ <code dir=\"ltr\">api</code>، بفارق ثانية تقريبًا. لكن Postgres يحتاج إلى عدّة ثوانٍ بعد بدء عمليته ليتهيّأ، وأكثر في التشغيل الأول حين ينشئ قاعدة البيانات ويشغّل سكربتات التهيئة. يتّصل الـ API خلال تلك النافذة فيُرفض.</p>\n<p>لكي ينتظر Compose <em>الجاهزية</em>، يحتاج إلى طريقة لاختبارها. وهذا هو الـ healthcheck (فحص الصحة).</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">الـ Healthchecks</h2>\n<p>الـ healthcheck أمر يشغّله Docker داخل الـ container وفق جدول زمني. رمز الخروج 0 يعني سليم؛ وأي رمز آخر يُعدّ فشلًا.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">compose.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres:17</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_USER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_PASSWORD</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      POSTGRES_DB</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    volumes</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">pgdata:/var/lib/postgresql/data</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    healthcheck</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"CMD-SHELL\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pg_isready -U notes -d notes\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      interval</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">10s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      timeout</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">3s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      retries</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">5</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      start_period</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">30s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      start_interval</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">1s</span></span></code></pre></div>\n<p>يأتي <code dir=\"ltr\">pg_isready</code> مع image ‏Postgres ويخرج بالرمز 0 حالما يقبل الخادم الاتصالات. حقول التوقيت:</p>\n<ul>\n<li><code dir=\"ltr\">interval</code>: كم مرة يُشغَّل الفحص بعد أن يعمل الـ container بشكل طبيعي.</li>\n<li><code dir=\"ltr\">timeout</code>: المدة التي قد يستغرقها فحص واحد قبل أن يُعدّ فاشلًا.</li>\n<li><code dir=\"ltr\">retries</code>: عدد الإخفاقات المتتالية قبل أن يُعلَّم الـ container بأنه <code dir=\"ltr\">unhealthy</code>.</li>\n<li><code dir=\"ltr\">start_period</code>: مهلة سماح بعد البدء لا تُحتسب خلالها الإخفاقات ضمن <code dir=\"ltr\">retries</code>.</li>\n<li><code dir=\"ltr\">start_interval</code>: كم مرة يُفحص <em>خلال</em> مهلة البدء، حتى يُكتشف البدء السريع بسرعة بدلًا من انتظار <code dir=\"ltr\">interval</code> كامل.</li>\n</ul>\n<p>يعرض <code dir=\"ltr\">docker compose ps</code> الآن <code dir=\"ltr\">(healthy)</code> أو <code dir=\"ltr\">(unhealthy)</code> بجوار الحالة. وحين يفشل فحص وتريد معرفة السبب، تُحفظ آخر بضع نتائج، مع مخرجاتها، على الـ container:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Terminal</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> inspect</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-db-1</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --format</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '{{json .State.Health}}'</span></span></code></pre></div>\n<p>هناك شيء لا تفعله الـ healthchecks في Docker أو Compose العاديين: إنها لا تعيد تشغيل الـ container غير السليم. الحالة معلومة فقط، تستخدمها شروط <code dir=\"ltr\">depends_on</code> وتستخدمها أنت. ‏Kubernetes مختلف، كما سترى مع الـ liveness probes في القسم الخامس، حيث يؤدّي الفحص الفاشل فعلًا إلى إعادة التشغيل. معرفة أي أداة تتصرّف عند فشل الفحص وأيها تكتفي بالإبلاغ عنه أمر مهمّ في الثالثة فجرًا.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الفرق بين CMD وCMD-SHELL</span></p>\n<p>الصيغة <code dir=\"ltr\">[&quot;CMD&quot;, &quot;pg_isready -U notes -d notes&quot;]</code> تبدو صحيحة وتفشل دائمًا. مع <code dir=\"ltr\">CMD</code> يكون كل عنصر في القائمة معاملًا واحدًا، فيحاول Docker تشغيل برنامج اسمه النصّ بأكمله. إمّا أن تقسّمه، <code dir=\"ltr\">[&quot;CMD&quot;, &quot;pg_isready&quot;, &quot;-U&quot;, &quot;notes&quot;, &quot;-d&quot;, &quot;notes&quot;]</code>، أو تستخدم <code dir=\"ltr\">CMD-SHELL</code> مع نصّ واحد. والعلامة التي تفضح الخطأ: container يبقى <code dir=\"ltr\">unhealthy</code> إلى الأبد بينما تعمل قاعدة البيانات على أكمل وجه.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">الانتظار على شروط</h2>\n<p>الصيغة الطويلة لـ <code dir=\"ltr\">depends_on</code> تتيح لك تحديد ما تنتظره:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"3\"><div class=\"code-head\"><span class=\"code-title\">compose.yaml</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">services</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  migrate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes-api:dev</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    command</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"node\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"dist/migrate.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      DATABASE_URL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres://notes:notes@db:5432/notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    depends_on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        condition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">service_healthy</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    build</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">.</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    image</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">notes-api:dev</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    ports</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">      - </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"8080:3000\"</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    environment</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      DATABASE_URL</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">postgres://notes:notes@db:5432/notes</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    depends_on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        condition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">service_healthy</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        restart</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">true</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      migrate</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        condition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">service_completed_successfully</span></span></code></pre></div>\n<p>هناك ثلاثة شروط: <code dir=\"ltr\">service_started</code> (سلوك الصيغة المختصرة)، و<code dir=\"ltr\">service_healthy</code>، و<code dir=\"ltr\">service_completed_successfully</code> الذي ينتظر container يُنفَّذ مرة واحدة حتى يخرج بالرمز 0. هنا تعمل الـ migration حالما يصبح Postgres سليمًا، ولا يبدأ الـ API إلا بعد نجاح الـ migration. إذا فشلت الـ migration فلن يبدأ <code dir=\"ltr\">api</code> أبدًا، وسترى السبب في <code dir=\"ltr\">docker compose logs migrate</code>. أما <code dir=\"ltr\">restart: true</code> فيعني أن Compose إذا أعاد تشغيل <code dir=\"ltr\">db</code> (مثلًا بعد تغيير إعداداته)، فسيعيد تشغيل <code dir=\"ltr\">api</code> أيضًا.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 220\" role=\"img\" aria-labelledby=\"l-dk-3-3-t1\"><defs><marker id=\"arrow-l-dk-3-3-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-dk-3-3-t1\">خط زمني: يبدأ db ويصبح سليمًا بعد نجاح فحوصه؛ ثم تعمل migrate وتخرج بالرمز 0؛ ثم يبدأ api.</title>\n  <text class=\"d-label-strong\" x=\"20\" y=\"54\">db</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"114\">migrate</text>\n  <text class=\"d-label-strong\" x=\"20\" y=\"174\">api</text>\n  <rect class=\"d-box-warn\" x=\"110\" y=\"34\" width=\"170\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"195\" y=\"55\" text-anchor=\"middle\">يبدأ</text>\n  <rect class=\"d-box-success\" x=\"280\" y=\"34\" width=\"400\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"480\" y=\"55\" text-anchor=\"middle\">سليم</text>\n  <rect class=\"d-box-accent\" x=\"290\" y=\"94\" width=\"150\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"365\" y=\"115\" text-anchor=\"middle\">يعمل</text>\n  <text class=\"d-code\" x=\"450\" y=\"115\">exit 0</text>\n  <rect class=\"d-box-primary\" x=\"510\" y=\"154\" width=\"170\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-label\" x=\"595\" y=\"175\" text-anchor=\"middle\">يخدم</text>\n  <path class=\"d-arrow\" d=\"M282 68 L292 92\" marker-end=\"url(#arrow-l-dk-3-3-1)\"/>\n  <path class=\"d-arrow\" d=\"M440 128 L508 160\" marker-end=\"url(#arrow-l-dk-3-3-1)\"/>\n  <path class=\"d-line d-dashed\" d=\"M110 200 L680 200\"/>\n  <text class=\"d-label-muted\" x=\"680\" y=\"216\" text-anchor=\"end\">الزمن</text>\n</svg><figcaption>ترتيب الإقلاع مع الشروط</figcaption></figure>\n<p>الـ API يستحق healthcheck هو الآخر. قد تكون image التشغيل من نوع distroless بلا <code dir=\"ltr\">curl</code>، لكنها تحتوي دائمًا على Node، وNode 24 فيه <code dir=\"ltr\">fetch</code> مدمجة. على <code dir=\"ltr\">node:24-slim</code> يكون <code dir=\"ltr\">node</code> ضمن الـ <code dir=\"ltr\">PATH</code>؛ أما على distroless فاستدعِه بمساره الكامل <code dir=\"ltr\">/nodejs/bin/node</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"4\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    healthcheck</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"CMD\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"node\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"-e\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"fetch('http://localhost:3000/healthz').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      interval</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">10s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      timeout</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">3s</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      retries</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">3</span></span></code></pre></div>\n<p>في الـ CI، الأمر <code dir=\"ltr\">docker compose up -d --wait</code> لا يعود إلا حين تصبح كل خدمة لها healthcheck سليمة، ويفشل إن صارت إحداها غير سليمة. هذه هي الطريقة الصحيحة لتشغيل البيئة قبل اختبارات التكامل، بدلًا من <code dir=\"ltr\">sleep 15</code>.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">إعادة المحاولة مكانها التطبيق مع ذلك</h2>\n<p>كل هذا يغطّي التشغيل الأول. لا شيء يمنع Postgres من إعادة التشغيل في الثالثة عصرًا من يوم ثلاثاء، وحين تنتقل إلى Kubernetes في القسم الرابع، لن تجد <code dir=\"ltr\">depends_on</code> أصلًا: تبدأ الـ Pods بأي ترتيب تُجدوَل به. لذا يتّصل notes-api مع إعادة محاولة وbackoff، ويُبلغ «غير جاهز» على <code dir=\"ltr\">/readyz</code> حتى تجيب قاعدة البيانات. الـ healthchecks والشروط تجعل الإقلاع المعتاد سريعًا؛ وإعادة المحاولة تجعل التطبيق يصمد حين تسوء الأمور.</p>\n<aside class=\"callout callout-why\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><circle cx=\"12\" cy=\"12\" r=\"10\"/><circle cx=\"12\" cy=\"12\" r=\"6\"/><circle cx=\"12\" cy=\"12\" r=\"2\"/></svg><span><span class=\"callout-kind\">لماذا يهمّ هذا</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الجاهزية فكرة متكرّرة</span></p>\n<p>السؤال «هل بدأ؟» مقابل «هل يستطيع أداء عمله؟» هو نفسه الذي ستجيب عنه مجددًا مع الـ readiness probes في Kubernetes في القسم الخامس. الـ Pod الذي يعمل لكنه لا يصل إلى قاعدة بياناته يجب ألا يستقبل أي حركة. فهم الفكرة جيدًا هنا يجعل نسختها في Kubernetes تبدو بديهية.</p>\n</aside>\n<p>البيئة الآن تقلع بالترتيب الصحيح في كل مرة. درس Compose الأخير يرتّب الإعدادات ويعطيك حلقة سريعة للتعديل وإعادة التحميل.</p>\n","toc":[{"id":"sec-1","text":"ما الذي ينتظره depends_on فعلًا","level":2},{"id":"sec-2","text":"الـ Healthchecks","level":2},{"id":"sec-3","text":"الانتظار على شروط","level":2},{"id":"sec-4","text":"إعادة المحاولة مكانها التطبيق مع ذلك","level":2}],"takeaways":["الصيغة المختصرة لـ <code dir=\"ltr\">depends_on</code> لا تنتظر إلا بدء container الاعتمادية، لا جاهزيته للخدمة.","الـ <code dir=\"ltr\">healthcheck</code> يخبر Docker كيف يختبر الـ container؛ والشرط <code dir=\"ltr\">condition: service_healthy</code> يجعل الخدمة المعتمِدة تنتظر نجاح ذلك الاختبار.","الشرط <code dir=\"ltr\">condition: service_completed_successfully</code> يشغّل المهام التي تُنفَّذ مرة واحدة، مثل الـ migrations، قبل بدء التطبيق.","في صيغة exec يكون كل معامل عنصرًا مستقلًا في القائمة؛ واستخدم <code dir=\"ltr\">CMD-SHELL</code> حين يكون الاختبار نصّ أمر shell واحدًا.","ترتيب الإقلاع لا يغطّي إلا التشغيل الأول، لذا يجب أن يعيد التطبيق محاولة الاتصال بقاعدة البيانات؛ وKubernetes ليس فيه <code dir=\"ltr\">depends_on</code> أصلًا."],"further":[{"title":"Control startup order in Compose","url":"https://docs.docker.com/compose/how-tos/startup-order/"},{"title":"Compose services reference (healthcheck)","url":"https://docs.docker.com/reference/compose-file/services/#healthcheck"},{"title":"docker compose up","url":"https://docs.docker.com/reference/cli/docker/compose/up/"}],"quiz":[{"q":"<p>الخدمة <code dir=\"ltr\">api</code> فيها <code dir=\"ltr\">depends_on: [db]</code> بالصيغة المختصرة. عند الإقلاع البارد يكتب الـ API في سجلّه <code dir=\"ltr\">ECONNREFUSED</code> مرة واحدة، ثم يعمل. لماذا؟</p>","options":[{"text":"الصيغة المختصرة تُتجاهل ما لم تتشارك الخدمات شبكة واحدة.","why":"كل الخدمات في المشروع تتشارك الشبكة الافتراضية، والصيغة المختصرة لا تُتجاهل. إنها تفعل بالضبط ما تعد به، وهو أقل مما يتوقّعه الناس."},{"text":"يشغّل Compose الخدمات المعتمِدة بترتيب عشوائي.","why":"الترتيب حتمي؛ ‏<code dir=\"ltr\">db</code> تبدأ أولًا فعلًا. لكن البدء ليس هو الجاهزية."},{"text":"الصيغة المختصرة تنتظر بدء container الـ <code dir=\"ltr\">db</code>، لا قبول Postgres للاتصالات.","why":"صحيح. يحتاج Postgres إلى بضع ثوانٍ للتهيئة. أضف healthcheck والشرط <code dir=\"ltr\">condition: service_healthy</code>."},{"text":"كلمة مرور قاعدة البيانات خاطئة في المحاولة الأولى.","why":"كلمة المرور الخاطئة تعطي خطأ مصادقة، لا اتصالًا مرفوضًا، ولن تُصلح نفسها."}],"answer":2},{"q":"<p>أي healthcheck يختبر جاهزية Postgres بشكل صحيح في خدمة <code dir=\"ltr\">db</code>؟</p>","options":[{"text":"<code dir=\"ltr\">test: [&quot;CMD-SHELL&quot;, &quot;pg_isready -U notes -d notes&quot;]</code>","why":"صحيح. ‏<code dir=\"ltr\">CMD-SHELL</code> يشغّل النصّ عبر shell الـ container، و<code dir=\"ltr\">pg_isready</code> يخرج بالرمز 0 حالما يقبل الخادم الاتصالات."},{"text":"<code dir=\"ltr\">test: [&quot;CMD&quot;, &quot;pg_isready -U notes -d notes&quot;]</code>","why":"صيغة exec تعامل النصّ كاملًا كاسم للبرنامج، فيبحث Docker عن ملف تنفيذي اسمه حرفيًا <code dir=\"ltr\">pg_isready -U notes -d notes</code> ويفشل الفحص دائمًا."},{"text":"<code dir=\"ltr\">test: [&quot;CMD&quot;, &quot;curl&quot;, &quot;-f&quot;, &quot;http://localhost:5432&quot;]</code>","why":"لا يتحدّث Postgres بـ HTTP، وimage ‏Postgres لا تشحن <code dir=\"ltr\">curl</code>."},{"text":"<code dir=\"ltr\">test: [&quot;CMD&quot;, &quot;ping&quot;, &quot;-c&quot;, &quot;1&quot;, &quot;db&quot;]</code>","why":"الـ ping يثبت أن الشبكة تعمل، لا أن Postgres يقبل الاستعلامات."}],"answer":0},{"q":"<p>خدمة <code dir=\"ltr\">migrate</code> تطبّق تغييرات المخطّط ثم تخرج. كيف يجب أن تعتمد <code dir=\"ltr\">api</code> عليها؟</p>","options":[{"text":"<code dir=\"ltr\">condition: service_healthy</code>","why":"المهمة التي تخرج لا تصبح سليمة أبدًا، فستنتظر <code dir=\"ltr\">api</code> حالة لا تأتي أبدًا."},{"text":"<code dir=\"ltr\">condition: service_completed_successfully</code>","why":"صحيح. ينتظر Compose أن تخرج <code dir=\"ltr\">migrate</code> بالرمز 0، ويرفض تشغيل <code dir=\"ltr\">api</code> إن فشلت."},{"text":"<code dir=\"ltr\">condition: service_started</code>","why":"هذا لا ينتظر إلا بداية الـ migration، فقد يبدأ الـ API على مخطّط نصف مُرحَّل."},{"text":"<code dir=\"ltr\">restart: always</code> على <code dir=\"ltr\">migrate</code>","why":"هذا يعيد تشغيل المهمة إلى الأبد بعد انتهائها؛ ولا يرتّب أي شيء."}],"answer":1},{"q":"<p>بيئتك تستخدم healthchecks و<code dir=\"ltr\">service_healthy</code>. لماذا يجب أن يعيد notes-api محاولة الاتصالات الفاشلة بقاعدة البيانات مع ذلك؟</p>","options":[{"text":"لأن الـ healthchecks لا تعمل إلا خلال <code dir=\"ltr\">docker compose build</code>.","why":"إنها تعمل باستمرار ما دام الـ container يعمل، بالفاصل الزمني المضبوط."},{"text":"لأن إعادة المحاولة شرط لنجاح <code dir=\"ltr\">pg_isready</code>.","why":"الأمر <code dir=\"ltr\">pg_isready</code> يعمل داخل container الـ <code dir=\"ltr\">db</code> ولا علاقة له بمنطق الاتصال في الـ API."},{"text":"لأن Compose يعطّل <code dir=\"ltr\">depends_on</code> بعد الدقيقة الأولى.","why":"لا شيء يُعطَّل. إنها ببساطة ميزة خاصة بوقت الإقلاع."},{"text":"لأن الترتيب لا يسري إلا عند الإقلاع؛ قد يُعاد تشغيل قاعدة البيانات لاحقًا، وKubernetes لا يرتّب الإقلاع أصلًا.","why":"صحيح. التطبيق المرن يعيد المحاولة مع backoff؛ وترتيب الإقلاع راحة إضافية، لا ضمانة."}],"answer":3}],"exercise":{"kind":"spot-bug","language":"yaml","code":"services:\n  api:\n    build: .\n    depends_on:\n      - db\n    environment:\n      DATABASE_URL: postgres://notes:notes@db:5432/notes\n  db:\n    image: postgres:17\n    environment:\n      POSTGRES_USER: notes\n      POSTGRES_PASSWORD: notes\n      POSTGRES_DB: notes\n    healthcheck:\n      test: [\"CMD\", \"pg_isready -U notes -d notes\"]\n      interval: 10s\n      retries: 5","bugLines":[5,15],"title":"سليم على الورق","prompt":"<p>مع ملف <code dir=\"ltr\">compose.yaml</code> هذا، تظهر <code dir=\"ltr\">db</code> بحالة <code dir=\"ltr\">(unhealthy)</code> إلى الأبد رغم أن <code dir=\"ltr\">psql</code> يعمل جيدًا، وما زال الـ API ينهار عند الإقلاع البارد برسالة <code dir=\"ltr\">ECONNREFUSED</code>. اختر السطرين <strong>الاثنين</strong> اللذين يجب إصلاحهما.</p>\n","hints":["في صيغة exec ‏(<code dir=\"ltr\">CMD</code>)، كم معاملًا يرى Docker في تلك القائمة؟","أي صيغة من <code dir=\"ltr\">depends_on</code> لا تنتظر إلا بدء الـ container؟"],"explanation":"<p>السطر 15 يستخدم صيغة exec مع نصّ واحد، فيبحث Docker عن برنامج اسمه حرفيًا <code dir=\"ltr\">pg_isready -U notes -d notes</code> ولا ينجح الفحص أبدًا. استخدم <code dir=\"ltr\">CMD-SHELL</code> مع النصّ، أو قسّم المعاملات.</p>\n<p>السطر 5 يستخدم الصيغة المختصرة لـ <code dir=\"ltr\">depends_on</code>، التي لا تنتظر إلا بدء <code dir=\"ltr\">db</code>. انتظر الـ healthcheck بدلًا من ذلك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"yaml\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">YAML</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  api</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    depends_on</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">        condition</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">service_healthy</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">  db</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">    healthcheck</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">:</span></span>\n<span class=\"line\"><span style=\"color:#116329;--shiki-dark:#7EE787\">      test</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">: [</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"CMD-SHELL\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">\"pg_isready -U notes -d notes\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n","checkLabels":{}},"words":686,"readMinutes":4}