EduFlow

الوحدة 1 · الـ Containers من المبادئ الأولى

الـ Layers والـ Build Cache

18 دقراءة 4 داكتشف الخطأ

الدرس في لمحة

رتّب تعليمات الـ Dockerfile بحيث يبقى تثبيت الاعتماديات في الـ cache، واقرأ حالة الـ build cache من docker history ومن مخرجات البناء، وأبقِ الـ cache دافئًا بالـ cache mounts وتصدير الـ cache في الـ CI.

  1. 1الـ Layers ومفاتيح الـ cache
  2. 2أعِد الترتيب لصالح الـ cache
  3. 3حزم النظام: RUN واحدة مع التنظيف
  4. 4الـ Cache mounts: إخفاقات أسرع
  5. 5الـ Cache في الـ CI

لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.

سألني فريق في Skylane مرة لماذا ارتفع زمن البناء في الـ CI لديهم تدريجيًا من أربعين ثانية إلى ثماني دقائق. كان الـ Dockerfile عندهم يشبه الذي كتبته في الدرس السابق: COPY . . ثم npm ci. وبما أن كل commit يغيّر ملفًا ما، كان كل بناء يعيد تثبيت 900 حزمة من الإنترنت. وإعادة ترتيب واحدة أعادته إلى أربعين ثانية.

الـ Layers ومفاتيح الـ cache

كل FROM وCOPY وADD وRUN تنتج layer (طبقة). حين تعيد البناء، يمرّ BuildKit على الـ Dockerfile من الأعلى إلى الأسفل ويسأل عن كل خطوة: «هل نفّذت هذا بالضبط من قبل، وبهذه المُدخلات بالضبط؟»

  • في RUN، المفتاح هو نصّ الأمر مع الـ layer التي يعمل فوقها.
  • في COPY وADD، يتضمّن المفتاح checksum للملفات المنسوخة.

أول خطوة لا يطابق مفتاحها هي إخفاق الـ cache (cache miss)، ومن بعدها تُعاد كل خطوة لاحقة، لأن كل واحدة منها تعمل فوق layer صارت مختلفة الآن. لذا فإصابات الـ cache (cache hits) لا تقع إلا في جزء متّصل من بداية الملف. الترتيب هو كل شيء.

كومتان من الـ layers. حين تأتي COPY . . قبل npm ci، يُخفق تعديل الكود عند COPY ويُعاد npm ci. وحين تُنسخ ملفات الحزم أولًا، يبقى npm ci في الـ cache ولا يُعاد إلا COPY الأخيرة والبناء. COPY . . أولًا ملفات الحزم أولًا FROM node:24-slim WORKDIR /app COPY . . (src changed) RUN npm ci (~70 s) RUN npm run build FROM node:24-slim COPY package*.json RUN npm ci (cached) COPY . . (src changed) RUN npm run build من الـ cache أُعيد بناؤها
ملف واحد متغيّر يُبطل الـ layer الخاصة به وكل layer فوقها

أعِد الترتيب لصالح الـ cache

الاعتماديات تتغيّر أسبوعيًا؛ والكود يتغيّر مع كل commit. لذا انسخ ملفات تعريف الاعتماديات وحدها، ثم ثبّتها، وبعد ذلك فقط انسخ الباقي:

Dockerfile
# syntax=docker/dockerfile:1
FROM node:24-slim
WORKDIR /app

# Changes rarely: only when dependencies change
COPY package.json package-lock.json ./
RUN npm ci

# Changes on every commit
COPY . .
RUN npm run build

EXPOSE 3000
CMD ["node", "dist/server.js"]

عدّل src/server.ts وأعِد البناء. المخرجات تُظهر الخطوات التي أُعيد استخدامها:

Text
 => CACHED [2/6] WORKDIR /app
 => CACHED [3/6] COPY package.json package-lock.json ./
 => CACHED [4/6] RUN npm ci
 => [5/6] COPY . .
 => [6/6] RUN npm run build

القاعدة العامة: رتّب التعليمات من الأقل تغيّرًا إلى الأكثر تغيّرًا. حزم النظام، ثم اعتماديات اللغة، ثم كودك، ثم كل ما يُشتقّ من كودك.

الأمر docker history notes-api:dev يعرض الـ layers مع حجم كل منها والتعليمة التي أنشأتها. استخدمه حين تكون الـ image أكبر مما تتوقّع؛ فالمتّهم عادةً layer واحدة متضخّمة.

والقاعدة نفسها تنطبق على معاملات البناء. سطر مثل ARG GIT_SHA في أعلى الملف، يُستخدم لاحقًا في RUN أو LABEL، يعطي كل خطوة RUN تحته مُدخلًا جديدًا مع كل commit، لأن معاملات البناء تكون مرئية لكل RUN لاحقة على هيئة متغيّرات بيئة. لذا أخّر تعريف مثل هذه القيم قدر الإمكان، وضعه مباشرة قبل الخطوة الوحيدة التي تستخدمها.

مفتاح COPY مبنيّ على محتوى الملفات لا على تواريخ تعديلها، لذا فإن git clone جديدًا على خادم بناء يصيب الـ cache نفسه الذي على جهازك ما دامت البايتات متطابقة. ولهذا أيضًا يهمّ أن يكون package-lock.json محدّثًا ومحفوظًا في المستودع: فهو المُدخل الذي يقرّر هل يُعاد استخدام التثبيت المكلف أم لا.

حزم النظام: RUN واحدة مع التنظيف

إن احتجت إلى حزم نظام التشغيل، فثبّتها في RUN واحدة:

Dockerfile
RUN apt-get update \
 && apt-get install -y --no-install-recommends ca-certificates \
 && rm -rf /var/lib/apt/lists/*

الخيار --no-install-recommends يتخطّى الإضافات الاختيارية، وحذف قوائم الحزم في الخطوة نفسها يُبقيها خارج الـ layer. حذفها في RUN لاحقة لا يقلّص شيئًا: الـ layer السابقة ما زالت تحتويها.

الـ Cache mounts: إخفاقات أسرع

أحيانًا يجب أن يُعاد التثبيت لأنك أضفت اعتمادية. الـ cache mount يحتفظ بذاكرة تنزيلات npm داخل أداة البناء بين عمليات البناء، دون أن يضعها في الـ image:

Dockerfile
COPY package.json package-lock.json ./
RUN --mount=type=cache,target=/root/.npm npm ci

ما زال npm ci يحذف node_modules ويعيد إنشاءه، لكن الملفات المضغوطة تأتي من القرص المحلي بدلًا من الـ registry. في notes-api يحوّل هذا إعادة تثبيت تستغرق 70 ثانية إلى نحو 15 ثانية.

الـ Cache في الـ CI

جهازك يحتفظ بالـ cache بين عمليات البناء. أما معظم الـ runners في الـ CI فتبدأ فارغة، فيبدأ كل بناء باردًا مهما أحسنت ترتيب الملف. صدّر الـ cache إلى مكان دائم واستورده في التشغيل التالي:

Terminal
docker buildx build \
  --cache-from type=registry,ref=ghcr.io/skylane/notes-api:buildcache \
  --cache-to type=registry,ref=ghcr.io/skylane/notes-api:buildcache,mode=max \
  -t ghcr.io/skylane/notes-api:dev .

الخيار mode=max يخزّن الـ layers الوسيطة أيضًا، وهذا مهمّ حين تصبح لديك عدّة build stages. ومستخدمو GitHub Actions يستطيعون استخدام type=gha بدلًا من registry.

البناء سريع الآن. في الدرس التالي ستألف جانب التشغيل: المنافذ، ومتغيّرات البيئة، والسجلات، وما الذي يحدث فعلًا حين يتوقّف الـ container.

أهم النقاط

  • مفتاح الـ cache لكل تعليمة يعتمد على التعليمة نفسها، وفي حالة COPY على checksum الملفات المنسوخة؛ وإخفاق واحد يعيد بناء كل الخطوات التالية.
  • انسخ package.json وpackage-lock.json أولًا، وشغّل npm ci، ثم انسخ الكود المصدري، حتى لا تعيد تعديلات الكود تثبيت الاعتماديات.
  • ضع التعليمات التي نادرًا ما تتغيّر في الأعلى، والتعليمات التي تتغيّر مع كل commit في الأسفل.
  • شغّل apt-get update وapt-get install داخل RUN واحدة حتى لا يكون فهرس الحزم قديمًا أبدًا.
  • الـ cache mount ‏(RUN --mount=type=cache) يحتفظ بذاكرة تنزيلات npm بين عمليات البناء، حتى حين يجب إعادة بناء الـ layer نفسها.

قراءات إضافية

تحقّق من فهمك

4 أسئلة

السؤال 1 من 4

في هذا الـ Dockerfile تعدّل الملف src/routes.ts فقط ثم تعيد البناء. أي الخطوات تُنفَّذ من جديد؟

Dockerfile
FROM node:24-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
السؤال 2 من 4

لماذا يسبّب وضع RUN apt-get update في سطر مستقل، يليه RUN apt-get install -y curl، مشاكل بعد أشهر؟

السؤال 3 من 4

الـ runner في الـ CI يبدأ بـ cache فارغ لـ Docker في كل مهمة، فكل بناء يبدأ باردًا. ما الذي يساعد أكثر؟

السؤال 4 من 4

ما الذي يعطيك إياه RUN --mount=type=cache,target=/root/.npm npm ci ولا يعطيك إياه الـ layer cache؟

تدريب

اكتشف الخطأ

اعثر على قاتلَي الـ cache

البناء في الـ CI لهذا الـ Dockerfile يستغرق ثماني دقائق حتى حين يغيّر الـ commit سطرًا واحدًا من TypeScript، وفي الأسبوع الماضي فشلت إضافة حزمة نظام ثانية برسالة Unable to locate package. اختر السطرين الاثنين المسؤولين.

حدّد السطر أو الأسطر التي تظنها خاطئة، ثم تحقّق.