{"id":"l-dk-1-2","courseId":"docker-kubernetes","lang":"ar","kind":"lesson","summary":"اكتب Dockerfile يعمل لـ notes-api، وابنِه إلى image لها tag، وشغّله مع منفذ منشور، واعرف وظيفة كل تعليمة فيه.","html":"<p>تستطيع أن تصف بيئة تشغيل notes-api في جملة واحدة: «Node.js 24، وكودنا المترجَم، واعتمادياتنا، والتشغيل بالأمر <code dir=\"ltr\">node dist/server.js</code> على المنفذ 3000». الـ Dockerfile هو هذه الجملة مكتوبة بطريقة تسمح لآلة بتكرارها حرفيًا، على أي جهاز أو خادم بناء، وإلى الأبد.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">التطبيق الذي تغلّفه</h2>\n<p>المستودع يبدو هكذا:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Text</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>notes-api/\n  package.json\n  package-lock.json\n  tsconfig.json\n  src/\n    server.ts\n    db.ts</code></pre></div>\n<p>الأمر <code dir=\"ltr\">npm run build</code> يترجم <code dir=\"ltr\">src/</code> إلى <code dir=\"ltr\">dist/</code> باستخدام <code dir=\"ltr\">tsc</code>، والأمر <code dir=\"ltr\">npm start</code> يشغّل <code dir=\"ltr\">node dist/server.js</code>. هذا هو الجزء من الخادم الذي يهمّنا في سياق الـ containers:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"ts\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">src/server.ts</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:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> express </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'express'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">import</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> { pool } </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> './db.js'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> app</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> express</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">();</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">app.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">use</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(express.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">json</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">());</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">app.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/healthz'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">_req</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">res</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> res.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">send</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'ok'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">app.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">get</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'/readyz'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">async</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#953800;--shiki-dark:#FFA657\">_req</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#953800;--shiki-dark:#FFA657\">res</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  try</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">    await</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> pool.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">query</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'SELECT 1'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    res.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">send</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'ready'</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:#CF222E;--shiki-dark:#FF7B72\">catch</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> {</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">    res.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">status</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">503</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">).</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">send</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'database unreachable'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  }</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">});</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">// …notes routes</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">const</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> port</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> Number</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(process.env.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">PORT</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ??</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 3000</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">);</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">app.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">listen</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(port, </span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'0.0.0.0'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, () </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=></span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> console.</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\">log</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">`notes-api listening on ${</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">port</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">}`</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">));</span></span></code></pre></div>\n<p>لاحظ <code dir=\"ltr\">'0.0.0.0'</code>، واحفظها في ذهنك؛ سنعود إليها.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">أول Dockerfile</h2>\n<p>أنشئ ملفًا باسم <code dir=\"ltr\">Dockerfile</code> (بحرف D كبير، ودون امتداد) في جذر المستودع:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</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:#6E7781;--shiki-dark:#8B949E\"># syntax=docker/dockerfile:1</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> node:24-slim</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WORKDIR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /app</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> . .</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm ci</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> npm run build</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">EXPOSE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> 3000</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">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\">\"dist/server.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n<p>سطرًا بسطر:</p>\n<ul>\n<li><code dir=\"ltr\"># syntax=docker/dockerfile:1</code> يثبّت الـ frontend الخاص بالـ Dockerfile على أحدث إصدار من 1.x، فتحصل على الميزات الحديثة مثل أسرار البناء والـ cache mounts.</li>\n<li><code dir=\"ltr\">FROM node:24-slim</code> يبدأ من الـ image الرسمية لـ Node.js 24 فوق أساس Debian مقلّص. كل image تبدأ من image أخرى.</li>\n<li><code dir=\"ltr\">WORKDIR /app</code> ينشئ <code dir=\"ltr\">/app</code> إن لزم، ويجعله مجلد العمل لكل تعليمة بعده، وللـ container أثناء التشغيل أيضًا.</li>\n<li><code dir=\"ltr\">COPY . .</code> ينسخ الـ build context إلى <code dir=\"ltr\">/app</code>.</li>\n<li><code dir=\"ltr\">RUN npm ci</code> يثبّت بالضبط ما يقوله <code dir=\"ltr\">package-lock.json</code>. استخدم <code dir=\"ltr\">ci</code> لا <code dir=\"ltr\">install</code> داخل الـ images: فهو يفشل إذا تعارض الـ lockfile مع <code dir=\"ltr\">package.json</code> بدلًا من أن يعيد كتابة الـ lockfile بصمت.</li>\n<li><code dir=\"ltr\">RUN npm run build</code> يترجم TypeScript إلى <code dir=\"ltr\">dist/</code>.</li>\n<li><code dir=\"ltr\">EXPOSE 3000</code> يسجّل أن التطبيق يستمع على المنفذ 3000، لكنه لا ينشر شيئًا.</li>\n<li><code dir=\"ltr\">CMD</code> هو الأمر الافتراضي حين يبدأ الـ container.</li>\n</ul>\n<p>لماذا <code dir=\"ltr\">node:24-slim</code>؟ لأن Node.js 24 خط دعم طويل الأمد تصله إصلاحات أمنية حتى 2028، والـ image الرسمية يُعاد بناؤها كلما أصدر Node أو Debian تصحيحًا. نسخة <code dir=\"ltr\">-slim</code> تستبعد المترجِمات وصفحات الـ man وعشرات الحزم التي تضمّها الـ image الكاملة <code dir=\"ltr\">node:24</code> لبناء الوحدات الأصلية (native modules). لا تحتاج إليها لتشغيل JavaScript، وكل حزمة لا تشحنها هي حزمة لن يشتكي منها ماسح الثغرات. وإن احتاجت إحدى الاعتماديات يومًا إلى ترجمة كود أصلي، فستتعامل مع ذلك في build stage منفصلة في القسم القادم.</p>\n<p>هذا الملف يعمل، لكن فيه مشكلة أداء ستصلحها في الدرس التالي. شغّله أولًا.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">البناء والتشغيل</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"3\"><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\"> build</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -t</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> .</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --rm</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> -p</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 3000:3000</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span></span></code></pre></div>\n<p>الخيار <code dir=\"ltr\">-t notes-api:dev</code> يسمّي الـ image ‏(<code dir=\"ltr\">repository:tag</code>). والنقطة <code dir=\"ltr\">.</code> في النهاية هي الـ <strong>build context</strong> (سياق البناء): المجلد الذي يسلّمه Docker إلى أداة البناء. لا تستطيع <code dir=\"ltr\">COPY</code> قراءة إلا الملفات الموجودة داخله، ولهذا يفشل <code dir=\"ltr\">COPY ../shared .</code>.</p>\n<p>الأمر <code dir=\"ltr\">docker run</code> ينشئ container من الـ image. الخيار <code dir=\"ltr\">--rm</code> يحذفه عند توقّفه، فلا تتراكم لديك containers ميتة. والخيار <code dir=\"ltr\">-p 3000:3000</code> يربط المنفذ 3000 على جهازك بالمنفذ 3000 داخل الـ container. في terminal آخر:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"4\"><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\">curl</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> localhost:3000/healthz</span></span></code></pre></div>\n<p>تحصل على <code dir=\"ltr\">ok</code>. أما <code dir=\"ltr\">/readyz</code> فيُرجع 503 لأنه لا توجد قاعدة بيانات بعد؛ وسيحلّ Compose ذلك في القسم الثالث.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">صيغة exec، ولماذا تهمّ</h2>\n<p>للتعليمة <code dir=\"ltr\">CMD</code> صيغتان. صيغة exec، ‏<code dir=\"ltr\">CMD [&quot;node&quot;, &quot;dist/server.js&quot;]</code>، تشغّل <code dir=\"ltr\">node</code> مباشرة كـ PID 1. وصيغة shell، ‏<code dir=\"ltr\">CMD node dist/server.js</code>، تشغّل <code dir=\"ltr\">/bin/sh -c &quot;node dist/server.js&quot;</code>، ومرجع الـ Dockerfile يحذّر من أن الأمر المُشغَّل بهذه الطريقة لن يستقبل إشارات Unix. حين يطلب Docker أو Kubernetes من تطبيقك أن يتوقّف، يرسل <code dir=\"ltr\">SIGTERM</code> إلى PID 1. فإذا كان PID 1 هو shell لا يمرّر الإشارة، فلن يسمعها تطبيقك أبدًا، وسيُقتل قسرًا بعد انتهاء المهلة، ويُسقط الطلبات التي كان يخدمها.</p>\n<p>و<code dir=\"ltr\">CMD npm start</code> هي المشكلة نفسها مع عملية إضافية في السلسلة. شغّل <code dir=\"ltr\">node</code> مباشرة.</p>\n<aside class=\"callout callout-tip\" 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=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>دع أداة البناء تراجع عملك</span></p>\n<p>شغّل <code dir=\"ltr\">docker build --check .</code> لتقييم الـ Dockerfile وفق build checks الخاصة بـ Docker دون بناء فعلي. ستنبّهك إلى <code dir=\"ltr\">CMD</code> المكتوبة بصيغة shell تحت الاسم <code dir=\"ltr\">JSONArgsRecommended</code>، وإلى أسماء الـ stages غير المتطابقة، وصيغة <code dir=\"ltr\">KEY value</code> القديمة لمتغيّرات البيئة، وغير ذلك. يستغرق الأمر ثانية واحدة؛ أضفه إلى الـ CI.</p>\n</aside>\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>الاستماع على localhost داخل الـ container</span></p>\n<p>كثير من خوادم التطوير تستمع افتراضيًا على <code dir=\"ltr\">127.0.0.1</code>. داخل الـ container هذا هو الـ loopback الخاص بالـ container نفسه، بينما تصل المنافذ المنشورة إلى واجهة الشبكة. والعَرَض يثير الجنون: المنفذ مربوط، والتطبيق يكتب «listening»، ومع ذلك يحصل كل طلب على <code dir=\"ltr\">connection reset</code>. اجعل التطبيق يستمع على <code dir=\"ltr\">0.0.0.0</code>.</p>\n</aside>\n<h2 id=\"sec-5\" tabindex=\"-1\">انظر إلى ما بنيته</h2>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"5\"><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\"> image</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ls</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> image</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> inspect</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --format</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '{{.Config.Cmd}} {{.Config.ExposedPorts}}'</span></span></code></pre></div>\n<p>الـ <code dir=\"ltr\">CMD</code> قيمة افتراضية فقط. أي شيء تكتبه بعد اسم الـ image يحلّ محلّها، وهذا مفيد للفحوص السريعة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"6\"><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\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --rm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> node</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --version</span></span>\n<span class=\"line\"><span style=\"color:#953800;--shiki-dark:#FFA657\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> run</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> --rm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ls</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> dist</span></span></code></pre></div>\n<p>هناك أيضًا <code dir=\"ltr\">ENTRYPOINT</code>، التي تثبّت الملف التنفيذي وتعامل <code dir=\"ltr\">CMD</code> كمعاملاته الافتراضية. بعض الـ images تستخدمها (ستقابل في القسم القادم image نقطة دخولها هي <code dir=\"ltr\">node</code> نفسه). أما في images تطبيقاتك أنت، فتعليمة <code dir=\"ltr\">CMD</code> عادية بصيغة exec هي أبسط حلّ يؤدّي الغرض، وتُبقي استبدالها سهلًا كما في الأمثلة أعلاه.</p>\n<p>حجم الـ image بضع مئات من الميغابايتات: Node.js، وأساس Debian، واعتماديات التطوير (بما فيها TypeScript)، والكود المصدري. القسم الثاني سيقلّص ذلك. لكن قبل هذا، كل تغيير في <code dir=\"ltr\">server.ts</code> يعيد حاليًا تثبيت كل الاعتماديات، وهذا موضوع الدرس التالي.</p>\n","toc":[{"id":"sec-1","text":"التطبيق الذي تغلّفه","level":2},{"id":"sec-2","text":"أول Dockerfile","level":2},{"id":"sec-3","text":"البناء والتشغيل","level":2},{"id":"sec-4","text":"صيغة exec، ولماذا تهمّ","level":2},{"id":"sec-5","text":"انظر إلى ما بنيته","level":2}],"takeaways":["الـ Dockerfile وصفة من التعليمات؛ وكل <code dir=\"ltr\">FROM</code> و<code dir=\"ltr\">COPY</code> و<code dir=\"ltr\">RUN</code> تنتج layer من نظام الملفات داخل الـ image.","الـ build context هو المجلد الذي تمرّره إلى <code dir=\"ltr\">docker build</code>، و<code dir=\"ltr\">COPY</code> لا ترى إلا الملفات الموجودة داخله.","التعليمة <code dir=\"ltr\">EXPOSE</code> توثّق المنفذ فقط؛ أما الخيار <code dir=\"ltr\">-p host:container</code> في <code dir=\"ltr\">docker run</code> فهو ما يجعله قابلًا للوصول.","اكتب <code dir=\"ltr\">CMD</code> بصيغة exec ‏(<code dir=\"ltr\">[&quot;node&quot;, &quot;dist/server.js&quot;]</code>) حتى يكون تطبيقك هو PID 1 ويستقبل إشارات الإيقاف.","الخادم داخل الـ container يجب أن يستمع على <code dir=\"ltr\">0.0.0.0</code> لا على <code dir=\"ltr\">127.0.0.1</code>، وإلا فلن تصل إليه المنافذ المنشورة."],"further":[{"title":"Dockerfile reference","url":"https://docs.docker.com/reference/dockerfile/"},{"title":"Writing a Dockerfile","url":"https://docs.docker.com/get-started/docker-concepts/building-images/writing-a-dockerfile/"},{"title":"Build checks","url":"https://docs.docker.com/build/checks/"}],"quiz":[{"q":"<p>تشغّل <code dir=\"ltr\">docker build -t notes-api:dev .</code> من جذر المستودع. ماذا تعني النقطة <code dir=\"ltr\">.</code> في النهاية؟</p>","options":[{"text":"سمِّ الـ image بـ tag يحمل اسم المجلد الحالي.","why":"الـ tag يأتي من <code dir=\"ltr\">-t</code>. أما المعامل الأخير فيخصّ الملفات، لا الأسماء."},{"text":"استخدم المجلد الحالي كـ build context، أي مجموعة الملفات التي تستطيع <code dir=\"ltr\">COPY</code> قراءتها.","why":"صحيح. يرسل Docker هذا المجلد إلى أداة البناء؛ ومسارات <code dir=\"ltr\">COPY</code> نسبية إليه، ولا شيء خارجه مرئي."},{"text":"اكتب الـ image النهائية داخل المجلد الحالي.","why":"تُحفظ الـ images في مخزن Docker المحلي، لا في مجلد عملك. والأمر <code dir=\"ltr\">docker image ls</code> يعرضها."},{"text":"ابحث عن الـ Dockerfile في المجلد الحالي، لكن خذ الملفات من <code dir=\"ltr\">/</code>.","why":"الـ context وموقع الـ Dockerfile شيئان منفصلان (<code dir=\"ltr\">-f</code> يحدّد الـ Dockerfile)، لكن <code dir=\"ltr\">COPY</code> لا تقرأ أبدًا من <code dir=\"ltr\">/</code> على المضيف."}],"answer":1},{"q":"<p>في الـ Dockerfile السطر <code dir=\"ltr\">EXPOSE 3000</code>. تشغّل <code dir=\"ltr\">docker run notes-api:dev</code> ثم يفشل <code dir=\"ltr\">curl localhost:3000</code> على جهازك. لماذا؟</p>","options":[{"text":"التعليمة <code dir=\"ltr\">EXPOSE</code> تحتاج إلى اللاحقة <code dir=\"ltr\">tcp</code> حتى تعمل.","why":"البروتوكول <code dir=\"ltr\">tcp</code> هو الافتراضي أصلًا. إضافته لا تغيّر شيئًا في إمكانية الوصول."},{"text":"الـ container يحتاج إلى إعادة تشغيل قبل أن تُفتح المنافذ المعلنة.","why":"إعادة التشغيل لا تنشر شيئًا. المنافذ تُنشر لحظة إنشاء الـ container."},{"text":"المنفذ 3000 محجوز من Docker على المضيف.","why":"لا يحجز Docker منفذًا كهذا. المشكلة أنه لم يُنشر أي منفذ."},{"text":"التعليمة <code dir=\"ltr\">EXPOSE</code> مجرد توثيق؛ ما زلت تحتاج إلى <code dir=\"ltr\">-p 3000:3000</code> لنشر المنفذ على المضيف.","why":"صحيح. الخيار <code dir=\"ltr\">-p host:container</code> هو الذي ينشئ ربط المنافذ الفعلي. أما <code dir=\"ltr\">EXPOSE</code> فتخبر القرّاء والأدوات بالمنفذ الذي يستخدمه التطبيق."}],"answer":3},{"q":"<p>أي <code dir=\"ltr\">CMD</code> تسمح لعملية Node.js باستقبال <code dir=\"ltr\">SIGTERM</code> مباشرة عند إيقاف الـ container؟</p>","options":[{"text":"<code dir=\"ltr\">CMD [&quot;node&quot;, &quot;dist/server.js&quot;]</code>","why":"صحيح. صيغة exec تشغّل <code dir=\"ltr\">node</code> كـ PID 1 دون shell في المنتصف، فتصل الإشارات مباشرة إلى تطبيقك."},{"text":"<code dir=\"ltr\">CMD node dist/server.js</code>","why":"صيغة shell تغلّف الأمر بـ <code dir=\"ltr\">/bin/sh -c</code>، والـ shell لا يمرّر الإشارات. ولهذا السبب تنبّه عليها build checks في Docker."},{"text":"<code dir=\"ltr\">CMD [&quot;sh&quot;, &quot;-c&quot;, &quot;node dist/server.js&quot;]</code>","why":"هذه صيغة exec، لكن البرنامج الذي تشغّله هو shell، فتعود إلى shell يقف بين Docker وNode."},{"text":"<code dir=\"ltr\">CMD npm start</code>","why":"صيغة shell مع npm تضع عمليتين بين الإشارة وخادمك. شغّل <code dir=\"ltr\">node</code> مباشرة."}],"answer":0},{"q":"<p>يكتب notes-api في سجلّه <code dir=\"ltr\">listening on 127.0.0.1:3000</code> داخل الـ container. نشرت المنفذ بـ <code dir=\"ltr\">-p 3000:3000</code>، لكن كل طلب يحصل على <code dir=\"ltr\">connection reset</code>. ما الحل؟</p>","options":[{"text":"انشر المنفذ بالصيغة <code dir=\"ltr\">-p 127.0.0.1:3000:3000</code>.","why":"هذا يقيّد عنوان المضيف الذي يقبل الاتصالات فقط؛ والتطبيق في الداخل ما زال يستمع على الـ loopback الخاص بالـ container، الذي لا يصل إليه ربط المنافذ."},{"text":"أضف <code dir=\"ltr\">EXPOSE 3000/tcp</code> إلى الـ Dockerfile.","why":"التعليمة <code dir=\"ltr\">EXPOSE</code> لا تغيّر العنوان الذي يستمع عليه التطبيق."},{"text":"اجعل التطبيق يستمع على <code dir=\"ltr\">0.0.0.0</code> حتى يقبل الاتصالات من واجهة الشبكة الخاصة بالـ container.","why":"صحيح. الحركة المنشورة تصل إلى واجهة الشبكة الخاصة بالـ container، لا إلى الـ loopback. الاستماع على كل الواجهات يحلّ المشكلة."},{"text":"شغّل الـ container بالخيار <code dir=\"ltr\">--network host</code> بشكل دائم.","why":"هذا يلتفّ على العَرَض بالتخلّي عن عزل الشبكة. أصلح عنوان الاستماع بدلًا من ذلك."}],"answer":2}],"exercise":{"kind":"spot-bug","language":"dockerfile","code":"# syntax=docker/dockerfile:1\nFROM node:24-slim\nRUN mkdir /app\nRUN cd /app\nCOPY . .\nRUN npm ci\nRUN npm run build\nEXPOSE 3000\nCMD npm start","bugLines":[4,9],"title":"راجع أول Dockerfile كتبه زميلك","prompt":"<p>هذا الـ Dockerfile يُبنى بنجاح، لكن الملفات تنتهي في المكان الخطأ، والتطبيق يُقتل قسرًا في كل مرة يتوقّف فيها الـ container. اختر السطرين <strong>الاثنين</strong> المسؤولين عن هاتين المشكلتين.</p>\n","hints":["كل <code dir=\"ltr\">RUN</code> تُنفَّذ في shell جديد. هل يؤثّر <code dir=\"ltr\">cd</code> داخل <code dir=\"ltr\">RUN</code> واحدة في التعليمة التالية؟","أي تعليمة تحدّد ما سيصبح PID 1، وبأي صيغة كُتبت؟"],"explanation":"<p>السطر 4: ‏<code dir=\"ltr\">RUN cd /app</code> يغيّر المجلد داخل الـ shell الخاص بخطوة <code dir=\"ltr\">RUN</code> تلك فقط، فتبقى <code dir=\"ltr\">COPY . .</code> و<code dir=\"ltr\">npm ci</code> تعملان في <code dir=\"ltr\">/</code>، وينتهي المشروع مبعثرًا في جذر الـ image. استخدم <code dir=\"ltr\">WORKDIR /app</code>، التي تنشئ المجلد أيضًا (فيمكن حذف السطر 3 كذلك).</p>\n<p>السطر 9: ‏<code dir=\"ltr\">CMD npm start</code> مكتوبة بصيغة shell، فيقف <code dir=\"ltr\">/bin/sh -c</code> ثم npm بين Docker وNode. لا تصل <code dir=\"ltr\">SIGTERM</code> إلى الخادم، وبعد انتهاء مهلة السماح يُقتل في منتصف الطلبات. استخدم صيغة exec وشغّل Node مباشرة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">Dockerfile</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:#CF222E;--shiki-dark:#FF7B72\">WORKDIR</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> /app</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># …</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">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\">\"dist/server.js\"</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">]</span></span></code></pre></div>\n","checkLabels":{}},"words":727,"readMinutes":4}