أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع
محتاج مسار سريع؟
ابدأ من الدوراتتواصل معناالأسئلة الشائعة

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالعروضالمدونةالدخول
DevOps بالعربي

Docker Build للمتوسط: من 4 دقائق لـ 12 ثانية بـ Layer Caching

📅 ٢٠ مايو ٢٠٢٦⏱ 7 دقائق قراءة
Docker Build للمتوسط: من 4 دقائق لـ 12 ثانية بـ Layer Caching

المستوى المطلوب: متوسط. هذا المقال يفترض أنك كتبت Dockerfile من قبل وشغّلت docker build مرة واحدة على الأقل. لست بحاجة لخبرة عميقة، لكن يجب أن تكون عارفًا الفرق بين الـ image والـ container.

Docker Build بطيء؟ السبب في ترتيب سطور الـ Dockerfile وليس في السيرفر

لو الـ docker build عندك بياخد 4 دقائق كل مرة تعدّل فيها سطر كود واحد، انت بتدفع تكلفة وقت مالهاش لزمة. ترتيب صحيح لسطور الـ Dockerfile مع تفعيل BuildKit ينزّلان نفس البناء لـ 12 ثانية — بدون ما تغيّر سطرًا واحدًا في كود التطبيق نفسه.

المشكلة باختصار

كل مرة تعدّل سطرًا في الكود وتعمل build، يعيد Docker تثبيت كل الـ dependencies من الصفر. على مشروع Node فيه 840 حزمة، ده معناه أن npm install يشتغل من البداية في كل بناء. النتيجة: دورة feedback بطيئة، وخط CI/CD يستهلك دقائق غالية، ومطوّر يقعد يبصّ في الشاشة بدل ما يشتغل. المشكلة ليست في قوة السيرفر — المشكلة أن Docker لا يجد شيئًا يعيد استخدامه من البناء السابق.

رسم توضيحي لطبقات Docker image مع شارات CACHED خضراء و REBUILD صفراء، ومقارنة زمن البناء من 4 دقائق إلى 12 ثانية

ليه الـ build يعيد نفسه: مفهوم الطبقات (Layers)

قبل التعريف العلمي، خد مثالًا بسيطًا. تخيّل أنك تجهّز نفس الساندوتش كل يوم بنفس الترتيب: عيش، ثم جبنة، ثم طماطم، ثم ملح. لو حافظت على الترتيب ده، تقدر تجهّز العيش والجبنة مقدمًا وتسيبهم جاهزين على الرف. بس لو قررت تبدأ بالملح قبل العيش، تضطر تبدأ الطبق كله من أوله في كل مرة، لأن كل خطوة تعتمد على اللي قبلها.

Docker يفكّر بنفس الطريقة بالظبط. كل تعليمة في الـ Dockerfile (زي FROM أو COPY أو RUN) تنتج طبقة (layer)، وهي فرق (diff) في نظام الملفات يُسجَّل بشكل منفصل. Docker يخزّن كل طبقة في الـ build cache. عند إعادة البناء، يمشي Docker على التعليمات بالترتيب: طول ما التعليمة وكل اللي قبلها لم يتغيّروا، يأخذ الطبقة الجاهزة من الـ cache بدل ما ينفّذها. أول ما تتغيّر تعليمة واحدة، ينكسر الـ cache عندها، وكل التعليمات التالية تُنفَّذ من جديد إجباريًا. ده اسمه cache invalidation، والقاعدة الأساسية: الـ cache يعمل بالتسلسل، مش بالقطعة المعزولة.

الخطأ اللي يكسر الـ Cache: ترتيب السطور

خد الـ Dockerfile الشائع ده:

Dockerfile
FROM node:20
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]

المشكلة في السطر COPY . . — هو ينسخ كل ملفات المشروع. أي تعديل في أي ملف source، حتى لو مسافة أو تعليق، يغيّر محتوى الطبقة دي. وبما أن RUN npm install جاية بعدها مباشرة، ينكسر الـ cache بتاعها تلقائيًا، فيعيد npm تثبيت 840 حزمة في كل بناء. الافتراض الخاطئ هنا أن "نسخ الكود" و"تثبيت الحزم" خطوة واحدة. هما خطوتان بإيقاع تغيير مختلف تمامًا: الكود يتغيّر عشرات المرات في اليوم، والـ dependencies تتغيّر مرة كل أسبوعين تقريبًا.

مقارنة بين Dockerfile بترتيب خاطئ تنهار فيه الـ cache مع كل تعديل، وترتيب صحيح تبقى فيه طبقة RUN npm ci مخزّنة، مع زمن بناء 4 دقائق مقابل 12 ثانية

الحل: رتّب الـ Dockerfile وفعّل BuildKit

  1. افصل نسخ ملفات الـ dependencies عن نسخ الكود. انسخ package.json و package-lock.json أولًا لوحدهما.
  2. نفّذ التثبيت بعدهما مباشرة، قبل ما تنسخ باقي الكود.
  3. انسخ باقي الكود في النهاية بـ COPY . .
  4. فعّل BuildKit — وهو الـ build backend الافتراضي من Docker Engine 23.0 (فبراير 2023). لو على نسخة أقدم، صدّر DOCKER_BUILDKIT=1 أو استخدم docker buildx build.
  5. أضف ملف .dockerignore يستبعد node_modules و .git و dist.

والنتيجة Dockerfile بالشكل ده:

Dockerfile
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
CMD ["node", "server.js"]

دلوقتي طبقة RUN npm ci تعتمد فقط على package.json و package-lock.json. تعديل في كود التطبيق يكسر الـ cache عند COPY . . فقط — وهي طبقة تأخذ أقل من ثانية. أما npm ci فيبقى من الـ cache.

استخدم npm ci وليس npm install داخل الـ Dockerfile: هو يثبّت بالضبط النسخ المكتوبة في package-lock.json، أسرع وأكثر حتمية، ويفشل لو الملفان غير متطابقين. أما .dockerignore فبدونه ينسخ COPY . . مجلد node_modules المحلي (قد يكون 380 ميجابايت و60 ألف ملف) داخل الـ build context، وده يبطّئ نقل الـ context ويكسر الـ cache كل ما يتغيّر node_modules عندك محليًا.

الخطوة الأقوى: cache mount للـ dependencies

الترتيب الصحيح يحل أغلب المشكلة. لكن تبقى حالة واحدة بطيئة: لما تضيف dependency جديدة فعلًا. ساعتها يتغيّر package-lock.json، فينكسر cache طبقة npm ci، ويعيد npm تنزيل الـ 840 حزمة من الشبكة من البداية.

الحل هو cache mount. ده feature في BuildKit يخلّي مجلد الـ cache الخاص بـ npm نفسه (مش الطبقة) يبقى محفوظًا بين البناءات:

Dockerfile
# syntax=docker/dockerfile:1
FROM node:20-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm \
    npm ci
COPY . .
CMD ["node", "server.js"]

دلوقتي حتى لو تغيّرت الحزم وانكسرت الطبقة، يجد npm الحزم اللي نزّلها قبل كده في /root/.npm ويأخذها من القرص المحلي بدل الشبكة. لـ Python نفس الفكرة مع pip:

Dockerfile
RUN --mount=type=cache,target=/root/.cache/pip \
    pip install -r requirements.txt

لاحظ سطر # syntax=docker/dockerfile:1 في أول الملف — هو ضروري لتفعيل خصائص BuildKit الحديثة زي الـ cache mount.

الأرقام: قبل وبعد

القياسات دي على تطبيق Express API متوسط (840 dependency، حوالي 24 ألف سطر كود)، على جهاز بـ 8 cores و16 جيجابايت RAM:

  • البناء الأول (بارد، بدون cache): حوالي 6 دقائق — سحب الـ base image مع npm ci مع نسخ الكود.
  • Dockerfile الخاطئ، بعد تعديل سطر كود واحد: 3 دقائق و52 ثانية، لأن npm install يعيد نفسه بالكامل.
  • Dockerfile الصحيح، بعد نفس التعديل: 12 ثانية. تحسّن 19 ضعفًا.
  • إضافة dependency جديدة، بدون cache mount: 95 ثانية (إعادة تنزيل كامل من الشبكة).
  • نفس الحالة مع cache mount: 24 ثانية. تحسّن حوالي 4 أضعاف.

اعتبر الأرقام دي تقديرات معقولة؛ الرقم الدقيق يختلف حسب مشروعك وسرعة شبكتك، لكن النسبة بين الترتيب الصحيح والخاطئ تبقى ثابتة تقريبًا.

الـ trade-offs اللي لازم تنتبه لها

  • الـ cache يستهلك مساحة قرص. كل طبقة محفوظة تأخذ مكانًا. على جهاز التطوير عادي، لكن على CI runner محدود المساحة قد تحتاج docker builder prune كل فترة. تكسب سرعة، تخسر مساحة.
  • cache mount لا ينتقل مع الـ image. هو cache بناء محلي فقط. على CI يبدأ نظيفًا كل مرة، يبدأ الـ cache فارغًا؛ تحتاج cache backend خارجي (زي --cache-to و --cache-from على registry) عشان تستفيد منه هناك.
  • ترتيب الطبقات يفرض انضباطًا. لو حطّيت سطرًا كثير التغيّر (زي ARG يتغيّر كل بناء) فوق التثبيت، ترجع تكسر الـ cache. الترتيب الصحيح يتطلب التفكير في "إيقاع التغيير" لكل سطر.
  • npm ci أكثر صرامة. يفشل لو package.json و package-lock.json غير متطابقين. ده مكسب (بناء حتمي) لكنه يتطلب أن تـ commit الـ lockfile دائمًا.

متى لا تستخدم هذه الطريقة

ترتيب السطور مفيد دائمًا تقريبًا، لكن المجهود الإضافي (cache mount، تعقيد CI) ليس له معنى دائمًا:

  • لو الـ build عندك أصلًا أقل من 20 ثانية، التحسين ده يوفّر ثوانٍ معدودة. ركّز على شيء آخر.
  • لو تبني الـ image مرة واحدة فقط عند الإصدار ولا تكرّر البناء، الـ cache قيمته محدودة — البناء البارد هو البناء الوحيد.
  • في بيئات CI ترمي الـ cache المحلي بعد كل job، الـ cache mount وحده لن يفيدك؛ تحتاج registry cache، وده يضيف تعقيدًا. لو الفريق صغير والبناءات قليلة، التعقيد ده قد لا يستاهل.

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

افتح الـ Dockerfile عندك الآن وبصّ على ترتيب سطرين: الـ COPY والـ RUN install. لو COPY . . جاي قبل تثبيت الـ dependencies، ده بالظبط السطر اللي يكلّفك الدقائق. انقل نسخ ملفات الـ dependencies لأعلى، حط التثبيت بعدها، وسيب COPY . . في النهاية. بعدها اعمل build مرتين متتاليتين بعد تعديل سطر كود بسيط — لو البناء الثاني خلص في ثوانٍ، يبقى الـ cache اشتغل.

المصادر

  • Docker Docs — Build cache: docs.docker.com/build/cache
  • Docker Docs — Building best practices: docs.docker.com/build/building/best-practices
  • Docker Docs — Dockerfile reference (RUN --mount=type=cache): docs.docker.com/reference/dockerfile
  • Docker Docs — BuildKit: docs.docker.com/build/buildkit
  • npm Docs — npm ci: docs.npmjs.com/cli/v10/commands/npm-ci

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة