{"id":"l-dk-1-3","courseId":"docker-kubernetes","lang":"ar","kind":"lesson","summary":"رتّب تعليمات الـ Dockerfile بحيث يبقى تثبيت الاعتماديات في الـ cache، واقرأ حالة الـ build cache من <code dir=\"ltr\">docker history</code> ومن مخرجات البناء، وأبقِ الـ cache دافئًا بالـ cache mounts وتصدير الـ cache في الـ CI.","html":"<p>سألني فريق في Skylane مرة لماذا ارتفع زمن البناء في الـ CI لديهم تدريجيًا من أربعين ثانية إلى ثماني دقائق. كان الـ Dockerfile عندهم يشبه الذي كتبته في الدرس السابق: <code dir=\"ltr\">COPY . .</code> ثم <code dir=\"ltr\">npm ci</code>. وبما أن كل commit يغيّر ملفًا ما، كان كل بناء يعيد تثبيت 900 حزمة من الإنترنت. وإعادة ترتيب واحدة أعادته إلى أربعين ثانية.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">الـ Layers ومفاتيح الـ cache</h2>\n<p>كل <code dir=\"ltr\">FROM</code> و<code dir=\"ltr\">COPY</code> و<code dir=\"ltr\">ADD</code> و<code dir=\"ltr\">RUN</code> تنتج layer (طبقة). حين تعيد البناء، يمرّ BuildKit على الـ Dockerfile من الأعلى إلى الأسفل ويسأل عن كل خطوة: «هل نفّذت هذا بالضبط من قبل، وبهذه المُدخلات بالضبط؟»</p>\n<ul>\n<li>في <code dir=\"ltr\">RUN</code>، المفتاح هو نصّ الأمر مع الـ layer التي يعمل فوقها.</li>\n<li>في <code dir=\"ltr\">COPY</code> و<code dir=\"ltr\">ADD</code>، يتضمّن المفتاح checksum للملفات المنسوخة.</li>\n</ul>\n<p>أول خطوة لا يطابق مفتاحها هي <strong>إخفاق الـ cache</strong> (cache miss)، ومن بعدها تُعاد <em>كل</em> خطوة لاحقة، لأن كل واحدة منها تعمل فوق layer صارت مختلفة الآن. لذا فإصابات الـ cache (cache hits) لا تقع إلا في جزء متّصل من بداية الملف. الترتيب هو كل شيء.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 680 290\" role=\"img\" aria-labelledby=\"l-dk-1-3-t1\"><defs><marker id=\"arrow-l-dk-1-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-1-3-t1\">كومتان من الـ layers. حين تأتي COPY . . قبل npm ci، يُخفق تعديل الكود عند COPY ويُعاد npm ci. وحين تُنسخ ملفات الحزم أولًا، يبقى npm ci في الـ cache ولا يُعاد إلا COPY الأخيرة والبناء.</title>\n  <text class=\"d-label-strong\" x=\"165\" y=\"22\" text-anchor=\"middle\">COPY . . أولًا</text>\n  <text class=\"d-label-strong\" x=\"505\" y=\"22\" text-anchor=\"middle\">ملفات الحزم أولًا</text>\n  <rect class=\"d-box-success\" x=\"40\" y=\"236\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"259\" text-anchor=\"middle\">FROM node:24-slim</text>\n  <rect class=\"d-box-success\" x=\"40\" y=\"194\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"217\" text-anchor=\"middle\">WORKDIR /app</text>\n  <rect class=\"d-box-warn\" x=\"40\" y=\"152\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"175\" text-anchor=\"middle\">COPY . .   (src changed)</text>\n  <rect class=\"d-box-warn\" x=\"40\" y=\"110\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"133\" text-anchor=\"middle\">RUN npm ci   (~70 s)</text>\n  <rect class=\"d-box-warn\" x=\"40\" y=\"68\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"165\" y=\"91\" text-anchor=\"middle\">RUN npm run build</text>\n  <rect class=\"d-box-success\" x=\"380\" y=\"236\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"259\" text-anchor=\"middle\">FROM node:24-slim</text>\n  <rect class=\"d-box-success\" x=\"380\" y=\"194\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"217\" text-anchor=\"middle\">COPY package*.json</text>\n  <rect class=\"d-box-success\" x=\"380\" y=\"152\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"175\" text-anchor=\"middle\">RUN npm ci   (cached)</text>\n  <rect class=\"d-box-warn\" x=\"380\" y=\"110\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"133\" text-anchor=\"middle\">COPY . .   (src changed)</text>\n  <rect class=\"d-box-warn\" x=\"380\" y=\"68\" width=\"250\" height=\"36\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"505\" y=\"91\" text-anchor=\"middle\">RUN npm run build</text>\n  <rect class=\"d-box-success\" x=\"200\" y=\"34\" width=\"14\" height=\"14\" rx=\"3\"/>\n  <text class=\"d-label-muted\" x=\"220\" y=\"46\">من الـ cache</text>\n  <rect class=\"d-box-warn\" x=\"320\" y=\"34\" width=\"14\" height=\"14\" rx=\"3\"/>\n  <text class=\"d-label-muted\" x=\"340\" y=\"46\">أُعيد بناؤها</text>\n</svg><figcaption>ملف واحد متغيّر يُبطل الـ layer الخاصة به وكل layer فوقها</figcaption></figure>\n<h2 id=\"sec-2\" tabindex=\"-1\">أعِد الترتيب لصالح الـ cache</h2>\n<p>الاعتماديات تتغيّر أسبوعيًا؛ والكود يتغيّر مع كل commit. لذا انسخ ملفات تعريف الاعتماديات وحدها، ثم ثبّتها، وبعد ذلك فقط انسخ الباقي:</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:#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>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># Changes rarely: only when dependencies change</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> package.json package-lock.json ./</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>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># Changes on every commit</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 run build</span></span>\n<span class=\"line\"></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>عدّل <code dir=\"ltr\">src/server.ts</code> وأعِد البناء. المخرجات تُظهر الخطوات التي أُعيد استخدامها:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"1\"><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> =&gt; CACHED [2/6] WORKDIR /app\n =&gt; CACHED [3/6] COPY package.json package-lock.json ./\n =&gt; CACHED [4/6] RUN npm ci\n =&gt; [5/6] COPY . .\n =&gt; [6/6] RUN npm run build</code></pre></div>\n<p>القاعدة العامة: <strong>رتّب التعليمات من الأقل تغيّرًا إلى الأكثر تغيّرًا.</strong> حزم النظام، ثم اعتماديات اللغة، ثم كودك، ثم كل ما يُشتقّ من كودك.</p>\n<p>الأمر <code dir=\"ltr\">docker history notes-api:dev</code> يعرض الـ layers مع حجم كل منها والتعليمة التي أنشأتها. استخدمه حين تكون الـ image أكبر مما تتوقّع؛ فالمتّهم عادةً layer واحدة متضخّمة.</p>\n<p>والقاعدة نفسها تنطبق على معاملات البناء. سطر مثل <code dir=\"ltr\">ARG GIT_SHA</code> في أعلى الملف، يُستخدم لاحقًا في <code dir=\"ltr\">RUN</code> أو <code dir=\"ltr\">LABEL</code>، يعطي كل خطوة <code dir=\"ltr\">RUN</code> تحته مُدخلًا جديدًا مع كل commit، لأن معاملات البناء تكون مرئية لكل <code dir=\"ltr\">RUN</code> لاحقة على هيئة متغيّرات بيئة. لذا أخّر تعريف مثل هذه القيم قدر الإمكان، وضعه مباشرة قبل الخطوة الوحيدة التي تستخدمها.</p>\n<p>مفتاح <code dir=\"ltr\">COPY</code> مبنيّ على <em>محتوى</em> الملفات لا على تواريخ تعديلها، لذا فإن <code dir=\"ltr\">git clone</code> جديدًا على خادم بناء يصيب الـ cache نفسه الذي على جهازك ما دامت البايتات متطابقة. ولهذا أيضًا يهمّ أن يكون <code dir=\"ltr\">package-lock.json</code> محدّثًا ومحفوظًا في المستودع: فهو المُدخل الذي يقرّر هل يُعاد استخدام التثبيت المكلف أم لا.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">حزم النظام: RUN واحدة مع التنظيف</h2>\n<p>إن احتجت إلى حزم نظام التشغيل، فثبّتها في <code dir=\"ltr\">RUN</code> واحدة:</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:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> apt-get update \\</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x26;&#x26; apt-get install -y --no-install-recommends ca-certificates \\</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x26;&#x26; rm -rf /var/lib/apt/lists/*</span></span></code></pre></div>\n<p>الخيار <code dir=\"ltr\">--no-install-recommends</code> يتخطّى الإضافات الاختيارية، وحذف قوائم الحزم في الخطوة <em>نفسها</em> يُبقيها خارج الـ layer. حذفها في <code dir=\"ltr\">RUN</code> لاحقة لا يقلّص شيئًا: الـ layer السابقة ما زالت تحتويها.</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>فصل apt-get update عن apt-get install</span></p>\n<p>حين يكون <code dir=\"ltr\">RUN apt-get update</code> في سطر مستقل، تُخزَّن تلك الـ layer في الـ cache حسب نصّها، وهو لا يتغيّر أبدًا. بعد أشهر تضيف حزمة إلى سطر التثبيت؛ فيُعاد التثبيت اعتمادًا على فهرس مخزّن عمره أشهر، فيفشل برسالة “Unable to locate package” أو يثبّت إصدارًا قديمًا. اجمعهما دائمًا في سلسلة واحدة.</p>\n</aside>\n<h2 id=\"sec-4\" tabindex=\"-1\">الـ Cache mounts: إخفاقات أسرع</h2>\n<p>أحيانًا <em>يجب</em> أن يُعاد التثبيت لأنك أضفت اعتمادية. الـ cache mount يحتفظ بذاكرة تنزيلات npm داخل أداة البناء بين عمليات البناء، دون أن يضعها في الـ image:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"dockerfile\" data-index=\"3\"><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\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> package.json package-lock.json ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --mount=type=cache,target=/root/.npm npm ci</span></span></code></pre></div>\n<p>ما زال <code dir=\"ltr\">npm ci</code> يحذف <code dir=\"ltr\">node_modules</code> ويعيد إنشاءه، لكن الملفات المضغوطة تأتي من القرص المحلي بدلًا من الـ registry. في notes-api يحوّل هذا إعادة تثبيت تستغرق 70 ثانية إلى نحو 15 ثانية.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">الـ Cache في الـ CI</h2>\n<p>جهازك يحتفظ بالـ cache بين عمليات البناء. أما معظم الـ runners في الـ CI فتبدأ فارغة، فيبدأ كل بناء باردًا مهما أحسنت ترتيب الملف. صدّر الـ cache إلى مكان دائم واستورده في التشغيل التالي:</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\">docker</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> buildx</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> build</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --cache-from</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> type=registry,ref=ghcr.io/skylane/notes-api:buildcache</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  --cache-to</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> type=registry,ref=ghcr.io/skylane/notes-api:buildcache,mode=max</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> \\</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  -t</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> ghcr.io/skylane/notes-api:dev</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> .</span></span></code></pre></div>\n<p>الخيار <code dir=\"ltr\">mode=max</code> يخزّن الـ layers الوسيطة أيضًا، وهذا مهمّ حين تصبح لديك عدّة build stages. ومستخدمو GitHub Actions يستطيعون استخدام <code dir=\"ltr\">type=gha</code> بدلًا من registry.</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\">--progress=plain</code> وقارن. إذا وجدت خطوة توقّعت أن تكون <code dir=\"ltr\">CACHED</code> ولم تكن كذلك، فانظر إلى التعليمة التي فوقها مباشرة: فهي التي تغيّرت مُدخلاتها.</p>\n</aside>\n<p>البناء سريع الآن. في الدرس التالي ستألف جانب التشغيل: المنافذ، ومتغيّرات البيئة، والسجلات، وما الذي يحدث فعلًا حين يتوقّف الـ container.</p>\n","toc":[{"id":"sec-1","text":"الـ Layers ومفاتيح الـ cache","level":2},{"id":"sec-2","text":"أعِد الترتيب لصالح الـ cache","level":2},{"id":"sec-3","text":"حزم النظام: RUN واحدة مع التنظيف","level":2},{"id":"sec-4","text":"الـ Cache mounts: إخفاقات أسرع","level":2},{"id":"sec-5","text":"الـ Cache في الـ CI","level":2}],"takeaways":["مفتاح الـ cache لكل تعليمة يعتمد على التعليمة نفسها، وفي حالة <code dir=\"ltr\">COPY</code> على checksum الملفات المنسوخة؛ وإخفاق واحد يعيد بناء كل الخطوات التالية.","انسخ <code dir=\"ltr\">package.json</code> و<code dir=\"ltr\">package-lock.json</code> أولًا، وشغّل <code dir=\"ltr\">npm ci</code>، ثم انسخ الكود المصدري، حتى لا تعيد تعديلات الكود تثبيت الاعتماديات.","ضع التعليمات التي نادرًا ما تتغيّر في الأعلى، والتعليمات التي تتغيّر مع كل commit في الأسفل.","شغّل <code dir=\"ltr\">apt-get update</code> و<code dir=\"ltr\">apt-get install</code> داخل <code dir=\"ltr\">RUN</code> واحدة حتى لا يكون فهرس الحزم قديمًا أبدًا.","الـ cache mount ‏(<code dir=\"ltr\">RUN --mount=type=cache</code>) يحتفظ بذاكرة تنزيلات npm بين عمليات البناء، حتى حين يجب إعادة بناء الـ layer نفسها."],"further":[{"title":"Docker build cache","url":"https://docs.docker.com/build/cache/"},{"title":"Optimize cache usage in builds","url":"https://docs.docker.com/build/cache/optimize/"},{"title":"Cache storage backends","url":"https://docs.docker.com/build/cache/backends/"}],"quiz":[{"q":"<p>في هذا الـ Dockerfile تعدّل الملف <code dir=\"ltr\">src/routes.ts</code> فقط ثم تعيد البناء. أي الخطوات تُنفَّذ من جديد؟</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\">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\"> package.json package-lock.json ./</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\">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 run build</span></span></code></pre></div>\n","options":[{"text":"كل الخطوات، لأن الـ build context تغيّر.","why":"تغيّر الـ context لا يُبطل كل شيء. كل خطوة تُفحص وحدها حتى أول إخفاق."},{"text":"فقط <code dir=\"ltr\">RUN npm run build</code>، لأنها الخطوة التي تترجم TypeScript.","why":"التعليمة <code dir=\"ltr\">COPY . .</code> تنسخ <code dir=\"ltr\">src/routes.ts</code>، فيتغيّر الـ checksum الخاص بها وتكون هي أول إخفاق؛ ثم تُعاد خطوة البناء لأنها تأتي بعد إخفاق."},{"text":"الخطوة <code dir=\"ltr\">RUN npm ci</code> وكل ما بعدها.","why":"الأمر <code dir=\"ltr\">npm ci</code> يأتي بعد <code dir=\"ltr\">COPY</code> لملفَّي الحزم فقط، وهما لم يتغيّرا، فيبقى في الـ cache."},{"text":"الخطوتان <code dir=\"ltr\">COPY . .</code> و<code dir=\"ltr\">RUN npm run build</code>؛ ويبقى تثبيت الاعتماديات في الـ cache.","why":"صحيح. أول مُدخل تغيّر هو عند <code dir=\"ltr\">COPY . .</code>، فتُعاد هي وكل خطوة بعدها. والتثبيت المكلف فوقها يُعاد استخدامه."}],"answer":3},{"q":"<p>لماذا يسبّب وضع <code dir=\"ltr\">RUN apt-get update</code> في سطر مستقل، يليه <code dir=\"ltr\">RUN apt-get install -y curl</code>، مشاكل بعد أشهر؟</p>","options":[{"text":"لأن Docker يرفض تخزين أوامر <code dir=\"ltr\">apt-get</code> في الـ cache.","why":"يخزّن Docker خطوات <code dir=\"ltr\">RUN</code> في الـ cache حسب نصّها، سواء كانت apt أم لا. وهذه بالضبط هي المشكلة هنا."},{"text":"لأن الـ layer الخاصة بـ <code dir=\"ltr\">update</code> تبقى في الـ cache بفهرس حزم قديم، فأي تغيير لاحق في سطر التثبيت يجلب إصدارات قديمة أو مفقودة.","why":"صحيح. مفتاح الـ cache هو نصّ الأمر، وهو لا يتغيّر أبدًا، فيكون الفهرس من يوم إنشاء الـ cache. اجمعهما في <code dir=\"ltr\">RUN</code> واحدة."},{"text":"لأن تعليمتَي <code dir=\"ltr\">RUN</code> تضاعفان حجم الـ image.","why":"الـ layers الإضافية تضيف قليلًا من البيانات الوصفية، ولا تضاعف الحجم. المشكلة الحقيقية فهرس قديم في الـ cache."},{"text":"لأن <code dir=\"ltr\">apt-get update</code> يحتاج إلى root بينما تُسقط الـ <code dir=\"ltr\">RUN</code> الثانية الصلاحيات.","why":"كلتاهما تعملان بالمستخدم نفسه ما لم تأتِ تعليمة <code dir=\"ltr\">USER</code> بينهما."}],"answer":1},{"q":"<p>الـ runner في الـ CI يبدأ بـ cache فارغ لـ Docker في كل مهمة، فكل بناء يبدأ باردًا. ما الذي يساعد أكثر؟</p>","options":[{"text":"إضافة المزيد من تعليمات <code dir=\"ltr\">RUN</code> حتى توجد layers أكثر للتخزين.","why":"الـ layers الإضافية لا تفيد حين لا يوجد cache تقرأ منه أصلًا."},{"text":"التحويل من <code dir=\"ltr\">npm ci</code> إلى <code dir=\"ltr\">npm install</code> لأنه أسرع.","why":"قد يعيد <code dir=\"ltr\">npm install</code> كتابة الـ lockfile، ولا يحلّ مشكلة الـ cache الفارغ. أبقِ <code dir=\"ltr\">npm ci</code> من أجل قابلية التكرار."},{"text":"تصدير الـ build cache واستيراده، مثلًا باستخدام <code dir=\"ltr\">--cache-to</code> و<code dir=\"ltr\">--cache-from</code> موجّهَين إلى registry.","why":"صحيح. واجهات تخزين الـ cache تسمح للـ runners الجديدة بإعادة استخدام الـ layers التي بنتها مهام سابقة."},{"text":"استخدام <code dir=\"ltr\">docker build --no-cache</code> حتى تكون النتيجة متوقّعة.","why":"هذا يتخلّص من الـ cache كليًا، وهو عكس ما تريده من أجل السرعة."}],"answer":2},{"q":"<p>ما الذي يعطيك إياه <code dir=\"ltr\">RUN --mount=type=cache,target=/root/.npm npm ci</code> ولا يعطيك إياه الـ layer cache؟</p>","options":[{"text":"تبقى ذاكرة تنزيلات npm بين عمليات البناء، فحتى حين يجب إعادة هذه الخطوة، تأتي الحزم من القرص المحلي بدلًا من الشبكة.","why":"صحيح. الـ layer cache إمّا كل شيء أو لا شيء؛ أما الـ cache mount فيصمد أمام إخفاق الـ layer ولا يُخزَّن في الـ image."},{"text":"يُركَّب مجلد <code dir=\"ltr\">node_modules</code> من جهازك داخل الـ image.","why":"الـ cache mounts تعيش داخل أداة البناء، لا على جهازك، وتخزّن مستودع تنزيلات npm، لا <code dir=\"ltr\">node_modules</code>."},{"text":"تُستبعد الحزم المثبّتة من الـ image النهائية لتوفير المساحة.","why":"المجلد <code dir=\"ltr\">node_modules</code> ما زال يُكتب داخل الـ layer. وحده المجلد المُركَّب <code dir=\"ltr\">/root/.npm</code> يبقى خارج الـ image."},{"text":"لا تُعاد الخطوة أبدًا، حتى لو تغيّر الـ lockfile.","why":"تُعاد الخطوة كلما تغيّرت مُدخلاتها. الـ mount يجعل الإعادة أسرع فقط."}],"answer":0}],"exercise":{"kind":"spot-bug","language":"dockerfile","code":"# syntax=docker/dockerfile:1\nFROM node:24-slim\nRUN apt-get update\nRUN apt-get install -y --no-install-recommends ca-certificates\nWORKDIR /app\nCOPY . .\nRUN npm ci\nRUN npm run build\nEXPOSE 3000\nCMD [\"node\", \"dist/server.js\"]","bugLines":[3,6],"title":"اعثر على قاتلَي الـ cache","prompt":"<p>البناء في الـ CI لهذا الـ Dockerfile يستغرق ثماني دقائق حتى حين يغيّر الـ commit سطرًا واحدًا من TypeScript، وفي الأسبوع الماضي فشلت إضافة حزمة نظام ثانية برسالة <code dir=\"ltr\">Unable to locate package</code>. اختر السطرين <strong>الاثنين</strong> المسؤولين.</p>\n","hints":["أي سطر يتغيّر مفتاح الـ cache الخاص به مع كل commit، وما الذي يُنفَّذ بعده؟","أي <code dir=\"ltr\">RUN</code> تُخزَّن في الـ cache حسب نصّها وحده، فقد يكون عمر نتيجتها أشهرًا؟"],"explanation":"<p>السطر 6 ينسخ الـ context كاملًا قبل <code dir=\"ltr\">npm ci</code>، فأي تغيير في الكود يُخفق في الـ cache هناك ويعيد تثبيت كل الاعتماديات. انسخ ملفات التعريف وحدها أولًا، ثم ثبّت الاعتماديات، ثم انسخ الباقي.</p>\n<p>السطر 3 يخزّن <code dir=\"ltr\">apt-get update</code> وحده في الـ cache؛ فحين يتغيّر سطر التثبيت، يعمل اعتمادًا على فهرس من يوم إنشاء الـ cache. اجمع التحديث والتثبيت في <code dir=\"ltr\">RUN</code> واحدة:</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\">RUN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> apt-get update \\</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x26;&#x26; apt-get install -y --no-install-recommends ca-certificates \\</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> &#x26;&#x26; rm -rf /var/lib/apt/lists/*</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\"> package.json package-lock.json ./</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\">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 run build</span></span></code></pre></div>\n","checkLabels":{}},"words":659,"readMinutes":4}