{"id":"l-dk-2-1","courseId":"docker-kubernetes","lang":"ar","kind":"lesson","summary":"قسّم الـ Dockerfile الخاص بـ notes-api إلى مراحل للبناء والاعتماديات والتشغيل، حتى لا تحتوي الـ image المشحونة إلا على الكود المترجَم واعتماديات الإنتاج، وأبعِد الملفات غير الضرورية عن الـ build context بملف .dockerignore.","html":"<p>الـ image من القسم الأول تشحن إلى الإنتاج TypeScript، وتعريفات الأنواع، ومشغّل الاختبارات، ومجلد <code dir=\"ltr\">src/</code>، وكل اعتمادية تطوير أخرى. لا شيء من هذا يعمل في الإنتاج. وكله سطح هجوم إضافي، ووقت سحب إضافي، وضجيج في تقارير الفحص. الحل أن تبني في مكان وتشحن من مكان آخر.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">Dockerfile واحد، ومراحل عدّة</h2>\n<p>كل <code dir=\"ltr\">FROM</code> تبدأ <strong>مرحلة</strong> (stage) جديدة بنظام ملفات جديد. سمِّ المراحل بـ <code dir=\"ltr\">AS</code>، وانسخ الملفات بينها بـ <code dir=\"ltr\">COPY --from</code>. والمرحلة الأخيرة وحدها هي التي تصبح الـ image التي تضع عليها الـ tag.</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>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># 1. Compile TypeScript (needs dev dependencies)</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> build</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\"> --mount=type=cache,target=/root/.npm npm ci</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tsconfig.json ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> src ./src</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:#6E7781;--shiki-dark:#8B949E\"># 2. Production dependencies only</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> deps</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\"> --mount=type=cache,target=/root/.npm npm ci --omit=dev</span></span>\n<span class=\"line\"></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\"># 3. What actually ships</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> runtime</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ENV</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> NODE_ENV=production</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 ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=deps /app/node_modules ./node_modules</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=build /app/dist ./dist</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>مرحلة <code dir=\"ltr\">runtime</code> تحصل على ثلاثة أشياء بالضبط: <code dir=\"ltr\">package.json</code>، و<code dir=\"ltr\">node_modules</code> الخاص بالإنتاج، و<code dir=\"ltr\">dist/</code> المترجَم. أما TypeScript و<code dir=\"ltr\">src/</code> وذاكرة تنزيلات npm فتبقى في مراحل لا يشحنها أحد.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 700 260\" role=\"img\" aria-labelledby=\"l-dk-2-1-t1\"><defs><marker id=\"arrow-l-dk-2-1-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-2-1-t1\">مرحلة build تترجم src إلى dist مع كل الاعتماديات. مرحلة deps تثبّت اعتماديات الإنتاج فقط. مرحلة runtime تنسخ dist من build وnode_modules من deps.</title>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"30\" width=\"250\" height=\"90\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"145\" y=\"56\" text-anchor=\"middle\">build</text>\n  <text class=\"d-code\" x=\"145\" y=\"80\" text-anchor=\"middle\">npm ci  (all deps)</text>\n  <text class=\"d-code\" x=\"145\" y=\"102\" text-anchor=\"middle\">tsc: src → dist</text>\n  <rect class=\"d-box-accent\" x=\"20\" y=\"150\" width=\"250\" height=\"80\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"145\" y=\"178\" text-anchor=\"middle\">deps</text>\n  <text class=\"d-code\" x=\"145\" y=\"204\" text-anchor=\"middle\">npm ci --omit=dev</text>\n  <rect class=\"d-box-success\" x=\"430\" y=\"70\" width=\"250\" height=\"130\" rx=\"10\"/>\n  <text class=\"d-label-strong\" x=\"555\" y=\"98\" text-anchor=\"middle\">runtime (تُشحن)</text>\n  <text class=\"d-code\" x=\"555\" y=\"126\" text-anchor=\"middle\">package.json</text>\n  <text class=\"d-code\" x=\"555\" y=\"150\" text-anchor=\"middle\">node_modules (prod)</text>\n  <text class=\"d-code\" x=\"555\" y=\"174\" text-anchor=\"middle\">dist/</text>\n  <path class=\"d-arrow\" d=\"M270 80 L428 140\" marker-end=\"url(#arrow-l-dk-2-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"350\" y=\"92\" text-anchor=\"middle\">dist/</text>\n  <path class=\"d-arrow\" d=\"M270 190 L428 160\" marker-end=\"url(#arrow-l-dk-2-1-1)\"/>\n  <text class=\"d-label-muted\" x=\"350\" y=\"200\" text-anchor=\"middle\">node_modules</text>\n  <text class=\"d-label-muted\" x=\"145\" y=\"252\" text-anchor=\"middle\">يبقى خلفها: TypeScript وsrc/ وذاكرة تنزيلات npm</text>\n</svg><figcaption>المراحل تُبنى بالتوازي، ومرحلة التشغيل تنسخ ما تحتاج إليه فقط</figcaption></figure>\n<p>يحسب BuildKit مخطّط الاعتماديات بين المراحل بنفسه. لا تعتمد <code dir=\"ltr\">build</code> و<code dir=\"ltr\">deps</code> إحداهما على الأخرى، فتعملان بالتوازي، وأي مرحلة لا يحتاج إليها الهدف تُتخطّى كليًا.</p>\n<p>في notes-api قلّص هذا حجم الـ image بنحو الثلث، وفحص الثغرات الذي ستشغّله لاحقًا في هذا القسم انتقل من عشرات النتائج في أدوات التطوير إلى بضع نتائج فقط في بيئة التشغيل. الدرس التالي يقلّص الـ base image نفسها.</p>\n<p>لماذا ننسخ <code dir=\"ltr\">package.json</code> إلى مرحلة التشغيل أصلًا؟ لأن <code dir=\"ltr\">package.json</code> في notes-api يحتوي على <code dir=\"ltr\">&quot;type&quot;: &quot;module&quot;</code>، الذي يطلب من Node أن يعامل ملفات <code dir=\"ltr\">.js</code> على أنها ES modules. إن حذفته، فسيفشل الـ container عند البدء برسالة <code dir=\"ltr\">SyntaxError: Cannot use import statement outside a module</code>، وهي تبدو خللًا في البناء لكنها في الحقيقة ملف مفقود. إذا كان مشروعك يستخدم CommonJS فيمكنك تخطّيه، لكنه لا يكلّف شيئًا ويُبقي البيانات الوصفية لـ <code dir=\"ltr\">npm</code> متاحة.</p>\n<p>قد ترى نمطًا مختلفًا: مرحلة واحدة تشغّل <code dir=\"ltr\">npm ci</code>، ثم تبني، ثم تشغّل <code dir=\"ltr\">npm prune --omit=dev</code> قبل نسخ <code dir=\"ltr\">node_modules</code> إلى الخارج. هذا يعمل، ويوفّر عملية تثبيت واحدة. أنا أفضّل مرحلة <code dir=\"ltr\">deps</code> المنفصلة لأنها تُخزَّن في الـ cache مستقلةً: ترقية أداة خاصة بالتطوير مثل الـ linter لا تُبطل layer اعتماديات الإنتاج، فيُعاد استخدام تلك الـ layer عبر عمليات بناء كثيرة، وتتغيّر image التشغيل بوتيرة أقل.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">استخدام المراحل الوسيطة عن قصد</h2>\n<p>المراحل أهداف مفيدة أيضًا. شغّل مجموعة الاختبارات في البيئة التي تحتوي على اعتماديات التطوير، دون لمس الـ image المشحونة:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"bash\" data-index=\"1\"><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\"> --target</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:test</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:#0A3069;--shiki-dark:#A5D6FF\"> notes-api:test</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> npm</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> test</span></span></code></pre></div>\n<p>الخيار <code dir=\"ltr\">--target build</code> يتوقّف عند مرحلة <code dir=\"ltr\">build</code>. يستطيع الـ CI تشغيل هذا قبل بناء image التشغيل، ويتشارك البناءان الـ cache نفسه.</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>نسخ مرحلة البناء بأكملها</span></p>\n<p>السطر <code dir=\"ltr\">COPY --from=build /app ./</code> هو أكثر أخطاء الـ multi-stage شيوعًا في المراجعات التي أجريها. يبدو مرتّبًا، لكنه يشحن كل ما حاولت تركه خلفك: اعتماديات التطوير، و<code dir=\"ltr\">src/</code>، وبيانات الاختبار، وأحيانًا ملف <code dir=\"ltr\">.env</code>. انسخ مسارات مسمّاة، ولا تنسخ أبدًا مجلد العمل الكامل لمرحلة ما.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">ملف ‎.dockerignore: تحكّم فيما تراه أداة البناء</h2>\n<p>حين تشغّل <code dir=\"ltr\">docker build .</code>، يُرسل المجلد كاملًا إلى أداة البناء بوصفه الـ context، مطروحًا منه كل ما يطابق <code dir=\"ltr\">.dockerignore</code>. من دونه، ينتقل إلى أداة البناء <code dir=\"ltr\">node_modules</code> المحلي لديك (مئات الميغابايتات، وربما فيه وحدات أصلية (native modules) مترجَمة لـ macOS)، ومجلد <code dir=\"ltr\">.git</code>، وأي ملف <code dir=\"ltr\">.env</code> فيه بيانات اعتماد حقيقية. وأي <code dir=\"ltr\">COPY . .</code> يضعها كلها في layer.</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"text\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">.dockerignore</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>node_modules\ndist\ncoverage\n.git\n.env\n.env.*\n*.log\nDockerfile\ncompose*.yaml</code></pre></div>\n<p>ثلاثة مكاسب دفعة واحدة: يهبط الـ context من مئات الميغابايتات إلى بضع مئات من الكيلوبايتات، ولا تعود تعديلات الملفات المتجاهَلة تُبطل الـ cache، ولا يمكن نسخ الأسرار الموجودة في مجلد عملك إلى image بالخطأ.</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>ملف ‎.env الذي صار عامًا</span></p>\n<p>فريق عملت معه دفع image إلى registry عام، وقد بناها بـ <code dir=\"ltr\">COPY . .</code> ودون <code dir=\"ltr\">.dockerignore</code>. كان ملف <code dir=\"ltr\">.env</code> لديهم يحتوي على كلمة مرور قاعدة بيانات الإنتاج. بقيت الـ image عامة نحو ست ساعات. وتطلّب تدوير بيانات الاعتماد يومًا كاملًا من العمل الحذر. ملف <code dir=\"ltr\">.dockerignore</code> من عشرة أسطر كان سيمنع ذلك كله.</p>\n</aside>\n<p>الصيغة تشبه <code dir=\"ltr\">.gitignore</code>: نمط واحد في كل سطر، والرمزان <code dir=\"ltr\">*</code> و<code dir=\"ltr\">**</code> للمطابقة، و<code dir=\"ltr\">!</code> لإعادة تضمين شيء ما. أبقِه في جذر الـ build context. وإن احتجت رغم ذلك إلى ملف واحد من الملفات المتجاهَلة، فأعِد تضمينه تحت القاعدة العامة، مثلًا <code dir=\"ltr\">.env.*</code> يليه <code dir=\"ltr\">!.env.example</code>، حتى يمكن نسخ ملف القالب بينما لا يمكن نسخ الملفات الحقيقية.</p>\n<p>عند الشك، افحص ما استلمته أداة البناء فعلًا: الأمر <code dir=\"ltr\">docker build --progress=plain .</code> يطبع حجم الـ context المنقول في أسطره الأولى. إذا كان هذا الرقم بالميغابايتات لـ API صغير، فهناك شيء ناقص في <code dir=\"ltr\">.dockerignore</code>.</p>\n<p>في الدرس التالي ستستبدل الـ base image بأخرى أصغر، وتكفّ عن تشغيل التطبيق بالمستخدم root.</p>\n","toc":[{"id":"sec-1","text":"Dockerfile واحد، ومراحل عدّة","level":2},{"id":"sec-2","text":"استخدام المراحل الوسيطة عن قصد","level":2},{"id":"sec-3","text":"ملف ‎.dockerignore: تحكّم فيما تراه أداة البناء","level":2}],"takeaways":["الـ multi-stage build يستخدم عدّة مراحل <code dir=\"ltr\">FROM</code>؛ ولا يتحوّل إلى image إلا المرحلة الأخيرة (أو المرحلة التي تحدّدها بـ <code dir=\"ltr\">--target</code>).","التعليمة <code dir=\"ltr\">COPY --from=&lt;stage&gt;</code> تسحب ملفات محدّدة من مرحلة سابقة، فلا تصل المترجِمات واعتماديات التطوير إلى الإنتاج أبدًا.","ثبّت اعتماديات الإنتاج بـ <code dir=\"ltr\">npm ci --omit=dev</code> في مرحلة خاصة بها، وانسخ <code dir=\"ltr\">node_modules</code> و<code dir=\"ltr\">dist</code> فقط إلى مرحلة التشغيل.","ملف <code dir=\"ltr\">.dockerignore</code> يُبقي <code dir=\"ltr\">node_modules</code> و<code dir=\"ltr\">.git</code> وملفات <code dir=\"ltr\">.env</code> ومخرجات البناء خارج الـ context، فيصير البناء أسرع وأكثر أمانًا."],"further":[{"title":"Multi-stage builds","url":"https://docs.docker.com/build/building/multi-stage/"},{"title":"Build context and .dockerignore","url":"https://docs.docker.com/build/concepts/context/"},{"title":"Dockerfile reference: COPY --from","url":"https://docs.docker.com/reference/dockerfile/#copy---from"}],"quiz":[{"q":"<p>في Dockerfile مراحله <code dir=\"ltr\">build</code> و<code dir=\"ltr\">deps</code> و<code dir=\"ltr\">runtime</code> (بهذا الترتيب)، ماذا ينتج الأمر <code dir=\"ltr\">docker build -t notes-api:1.0.0 .</code>؟</p>","options":[{"text":"ثلاث images، واحدة لكل مرحلة، وكلها تحمل الـ tag ‏<code dir=\"ltr\">notes-api:1.0.0</code>.","why":"البناء الواحد ينتج image واحدة. المراحل السابقة وسيطة ولا توجد إلا في الـ build cache."},{"text":"image مبنية من المرحلة الأخيرة <code dir=\"ltr\">runtime</code>، ولا تحتوي إلا على ما نسخته تلك المرحلة إليها.","why":"صحيح. المرحلة الأخيرة هي الهدف الافتراضي. وكل ما لم يُنسخ إليها يبقى خلفها."},{"text":"image مبنية من المرحلة الأولى، لأن البناء يحدث فيها.","why":"المرحلة الأولى هي الافتراضية فقط حين تكون الوحيدة. مع عدّة مراحل تفوز الأخيرة ما لم تمرّر <code dir=\"ltr\">--target</code>."},{"text":"image واحدة تحتوي على layers المراحل الثلاث مكدّسة معًا.","why":"المراحل لا تتكدّس. كل <code dir=\"ltr\">FROM</code> تبدأ نظام ملفات جديدًا؛ ولا ينقل الملفات بينها إلا <code dir=\"ltr\">COPY --from</code> صريحة."}],"answer":1},{"q":"<p>لماذا نثبّت اعتماديات الإنتاج في مرحلة <code dir=\"ltr\">deps</code> منفصلة بدلًا من تقليمها في مرحلة <code dir=\"ltr\">build</code>؟</p>","options":[{"text":"لأن <code dir=\"ltr\">npm ci --omit=dev</code> لا يعمل إلا في مرحلة اسمها <code dir=\"ltr\">deps</code>.","why":"أسماء المراحل مجرد تسميات اعتباطية. الخيار يعمل في أي مرحلة."},{"text":"لأن المراحل المنفصلة دائمًا أصغر من مرحلة واحدة.","why":"عدد المراحل وحده لا يغيّر الحجم. المهمّ هو ما تنسخه إلى المرحلة الأخيرة."},{"text":"لأن مراحل البناء لا تستطيع تشغيل npm scripts.","why":"أي مرحلة تستطيع تشغيل أي أمر؛ ومرحلة البناء نفسها تشغّل <code dir=\"ltr\">npm run build</code>."},{"text":"لأن مرحلة البناء تحتاج إلى اعتماديات التطوير مثل TypeScript، بينما لا يحتاج التشغيل إلا إلى اعتماديات الإنتاج؛ والتثبيت النظيف بـ <code dir=\"ltr\">--omit=dev</code> يعطي هذه المجموعة بالضبط.","why":"صحيح. كما أن هذه المرحلة تُخزَّن في الـ cache مستقلةً، فتغيير اعتمادية خاصة بالتطوير لا يُبطل تثبيت الإنتاج."}],"answer":3},{"q":"<p>حجم الـ build context لديك 1.2 غيغابايت، وكل بناء يبدأ بعبارة “transferring context” لمدة 40 ثانية. ما الحل الأرجح؟</p>","options":[{"text":"أضف ملف <code dir=\"ltr\">.dockerignore</code> يستبعد <code dir=\"ltr\">node_modules</code> و<code dir=\"ltr\">.git</code> ومخرجات البناء.","why":"صحيح. هذه المجلدات تشكّل عادةً معظم الـ context، ولا ينبغي إرسال أي منها إلى أداة البناء."},{"text":"استخدم <code dir=\"ltr\">COPY src ./src</code> بدلًا من <code dir=\"ltr\">COPY . .</code>.","why":"النسخ الأضيق يفيد الـ cache ومحتوى الـ image، لكن الـ context كاملًا ما زال يُرسل ما لم تتجاهل الملفات."},{"text":"انقل الـ Dockerfile إلى مجلد فرعي.","why":"الـ context هو المجلد الذي تمرّره أيًّا كان، بغضّ النظر عن مكان الـ Dockerfile."},{"text":"أضف <code dir=\"ltr\">--no-cache</code> لتخطّي نقل الـ context.","why":"الخيار <code dir=\"ltr\">--no-cache</code> يعطّل إعادة استخدام الـ layers. والـ context ما زال يُنقل."}],"answer":0},{"q":"<p>كيف تشغّل اختبارات الوحدات لـ notes-api داخل بيئة بناء الـ image دون أن تشحن أدوات الاختبار؟</p>","options":[{"text":"أضف <code dir=\"ltr\">RUN npm test</code> إلى مرحلة التشغيل.","why":"مرحلة التشغيل لا تحتوي على اعتماديات التطوير، فلن يكون مشغّل الاختبارات موجودًا؛ وأنت تريد إبقاء الاختبارات خارج الـ image المشحونة على أي حال."},{"text":"انسخ ملفات الاختبار إلى مرحلة التشغيل وشغّلها بـ <code dir=\"ltr\">docker exec</code>.","why":"هذا يشحن الاختبارات وأدواتها إلى الإنتاج، وهو بالضبط ما تتجنّبه الـ multi-stage builds."},{"text":"ابنِ مرحلة <code dir=\"ltr\">build</code> وحدها بـ <code dir=\"ltr\">--target build</code> وشغّل الاختبارات في container منها.","why":"صحيح. ‏<code dir=\"ltr\">--target</code> يتوقّف عند المرحلة المسمّاة، التي تحتوي على اعتماديات التطوير والكود المصدري، ولا يتسرّب منها شيء إلى image التشغيل."},{"text":"استخدم Dockerfile ثانيًا ينسخ image التشغيل.","why":"image التشغيل تفتقر عمدًا إلى اعتماديات التطوير، فلا يمكن تشغيل الاختبارات فيها."}],"answer":2}],"exercise":{"kind":"spot-bug","language":"dockerfile","code":"# syntax=docker/dockerfile:1\nFROM node:24-slim AS build\nWORKDIR /app\nCOPY package.json package-lock.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\nFROM node:24-slim AS runtime\nENV NODE_ENV=production\nWORKDIR /app\nCOPY --from=build /app ./\nEXPOSE 3000\nCMD [\"node\", \"dist/server.js\"]","bugLines":[12],"title":"الـ multi-stage build الذي يشحن كل شيء","prompt":"<p>كان الهدف من هذا الـ Dockerfile ذي المرحلتين إبقاء TypeScript واعتماديات التطوير خارج الإنتاج. لكن الـ image بالكاد أصغر من قبل، وما زال الماسح يُبلغ عن ثغرات CVE في أدوات الاختبار. اختر السطر الذي يُفسد الغرض من المرحلة الثانية.</p>\n","hints":["انظر إلى ما تنسخه مرحلة التشغيل من مرحلة البناء.","المجلد <code dir=\"ltr\">/app</code> في مرحلة البناء يحتوي على <code dir=\"ltr\">src/</code> و<code dir=\"ltr\">dist/</code> و<code dir=\"ltr\">node_modules</code> فيه كل اعتماديات التطوير."],"explanation":"<p>السطر 12 ينسخ <code dir=\"ltr\">/app</code> الكامل من مرحلة البناء إلى image التشغيل، فيُشحن <code dir=\"ltr\">src/</code> واعتماديات التطوير وكل ما كان في الـ context. أضف مرحلة لاعتماديات الإنتاج فقط وانسخ مسارات مسمّاة:</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> deps</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 --omit=dev</span></span>\n<span class=\"line\"></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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> runtime</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ENV</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> NODE_ENV=production</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 ./</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=deps /app/node_modules ./node_modules</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">COPY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> --from=build /app/dist ./dist</span></span></code></pre></div>\n","checkLabels":{}},"words":670,"readMinutes":4}