البناء والتوزيع: من Dockerfile إلى طبقات Merkle
من أين تأتي الصورة أصلاً؟ كيف يصير نصّ Dockerfile كومةَ طبقات (III)؟ ولماذا
ترتيب الأسطر يقرّر سرعة بنائك؟ الخيط الجامع: كل شيءٍ مُعنوَن بالمحتوى — وهذا
يربط البناء والكاش والتوزيع في فكرةٍ واحدة.
النبذة
docker build يحوّل Dockerfile إلى صورة OCI. كل تعليمةٍ قد
تنتج طبقة. الكاش يقرّر أيّها يُعاد بناؤه — بناءً على هاش المحتوى. والـ registry يخزّن
ويوزّع بنفس العنونة، فيتشارك الطبقات عبر كل الصور والمستخدمين. البنّاء الحديث
BuildKit أعاد تصميم هذا كـ رسمٍ موجّه لا دوري (DAG)
بدل سلسلةٍ خطية.
اللغز المستفزّ — تنبّأ بالكاش، ثم اكسره
dockerfileFROM python:3.12-slim COPY . /app RUN pip install -r /app/requirements.txt CMD ["python", "/app/main.py"]
- بنيتَه مرة. غيّرت سطراً في
main.pyفقط. تنبّأ قبل البناء: هل سيُعاد تشغيلpip installالثقيل؟ لماذا؟ - نفّذ وتحقّق. إن أُعيد
pip installرغم أنك لم تلمسrequirements.txt— اكتشفت «خطأ الـ Dockerfile» الأشهر. شخّص جذره: أي خطوةٍ في منطق الكاش جعلت تغييرmain.pyيُبطل طبقة الاعتماديات؟ - أعد ترتيب الأسطر بحيث لا يُعاد
pip installإلا حين تتغيّر الاعتماديات فعلاً. أثبت بالقياس الزمني أن بناءك الثاني صار شبه فوري.
الكاش متسلسل — كل طبقةٍ تعتمد على هاش كل ما قبلها. إذا كان COPY . /app (الذي يشمل main.py) قبل RUN pip install، فأي تغييرٍ في أي ملف يُبدّل هاش طبقة COPY، فيُبطل كل ما بعدها. ما الترتيب الذي يفصل «ما يتغيّر نادراً» عن «ما يتغيّر كثيراً»؟
هذا اللغز فكريّ، وهو أكثر شيءٍ ستستعمله عملياً. لا حلّ مكتوب — استنتجه من نموذج «هاش متسلسل».
الدرس — البناء كاشتقاق من العنونة بالمحتوى
ليش كل تعليمةٍ قد تكون طبقة — النموذج القديم
البنّاء التقليدي حرفيّ: لكل تعليمة، يشغّل كونتينراً مؤقتاً من الطبقة السابقة، ينفّذها، ثم يلتقط الفرق كطبقةٍ جديدة. هذا يفسّر قواعد كتبتها يدوياً:
- لماذا
RUN apt update && apt install && rm -rf /var/lib/apt/lists/*في سطرٍ واحد؟ الفصل لثلاثةRUN= ثلاث طبقات، والحذف في الثالثة مجرد whiteout فوق طبقتي التحميل (III). سطرٌ واحد = الحذف قبل التقاط الـ diff فلا تُخزَّن الملفات. FROMليست طبقة؛ هي تعيين lowerdirs.CMD/ENV/WORKDIRتعدّل config فقط (لا طبقات FS) — لهذا تغييرCMDلا يُبطل طبقات FS.
ليش الكاش يعمل هكذا: الهاش المتسلسل
مفتاح الكاش لكل طبقة = دالة في (هاش الطبقة السابقة) + (نصّ التعليمة) + (للـ
COPY/ADD: هاش محتوى الملفات). لأنه متسلسل، إبطال
طبقةٍ يُبطل كل ما بعدها حتماً. خطأ اللغز جذره: وضع المحتوى متقلّب التغيّر
(COPY .) مبكراً يسمّم السلسلة كلها.
النتيجة العملية: رتّب من الثابت إلى المتغيّر. انسخ requirements.txt وثبّت الاعتماديات قبل نسخ شيفرة التطبيق. فطبقة الاعتماديات الثقيلة تبقى مُكاشةً ما لم يتغيّر ملف الاعتماديات.
ليش BuildKit: من السلسلة الخطية إلى الـ DAG
البنّاء القديم خطّي صارم: لا توازي، كاش ساذج، لا أسرار آمنة. BuildKit يحلّل الـ Dockerfile إلى DAG من العمليات، ثم:
- يوازي الفروع المستقلّة (مراحل multi-stage غير المتعلّقة).
- كاش محتوى ذكي: يستورد/يصدّر الكاش من/إلى registry (
--cache-from) لمشاركته بين أجهزة الـ CI. - mounts للبناء:
RUN --mount=type=cache(كاش pip/apt بين البناءات بلا أن يدخل الطبقة)، و--mount=type=secret(سرٌّ أثناءRUNفقط، لا يُخزَّن في أي طبقة — حلّ تسرّب الأسرار جذرياً).
multi-stage: عدّة FROM، تنسخ من مرحلةٍ لأخرى بـ COPY --from=builder. تبني في صورةٍ ثقيلة ثم تنقل الناتج فقط لصورةٍ نحيلة (scratch/distroless) = سطح هجومٍ أقل (وصلٌ بـ IV) وحجمٌ أقل.
ليش العنونة بالمحتوى تُتقن التوزيع: الـ registry
الـ registry (تنفيذٌ لـ OCI Distribution Spec) يعيد استخدام فكرة III: كل شيءٍ مخاطَبٌ بالـ digest. البروتوكول (HTTP) من جزأين: blobs (الطبقات والـ config) وmanifests.
قبل رفع طبقة، يسأل العميل: HEAD /v2/<name>/blobs/sha256:<digest>
— «أتملك هذه؟». إن «نعم» (لأن صورةً أخرى لأي مستخدمٍ تشاركها)، لا تُرفع.
لهذا دفع صورةٍ على python:3.12 لا يرفع إلا طبقتك الصغيرة. content-addressing
= إزالة تكرارٍ محلياً وعبر الشبكة وبين كل المستخدمين. الحصاد الأكبر لبذرة III.
تدفّق docker pull nginx: حلّ الاسم → GET manifests/latest (قد يكون index متعدد المعماريات) → اختر manifest معماريتك → اسحب الطبقات الغائبة فقط → اسحب config، فُكّ، جهّز overlay snapshot (III) ⇒ جاهز (V). multi-arch اشتقاقٌ من الـ index: docker buildx build --platform linux/amd64,linux/arm64.
اللغز — ابنِ صورةً بلا Dockerfile، وأثبت إزالة التكرار
- كاش جراحي: أصلح ترتيب الـ Dockerfile، وأثبت بـ
time docker buildالفرق بين تغييرٍ في الشيفرة (كاش يُصيب) وتغييرٍ فيrequirements.txt(كاش يُبطل من تلك النقطة). فسّر كل نتيجةٍ بنموذج الهاش المتسلسل. - صورة بلا Dockerfile: بـ
skopeo/أوامر OCI، جمّع صورةً يدوياً: ابدأ من طبقةalpine، أضف طبقتك، اكتب config و manifest بـ digests صحيحة، وادفعها إلى registry محلي (registry:2). - أثبت إزالة التكرار: ادفع صورتين تشتركان في الأساس، وراقب أن الطبقات المشتركة خُزِّنت مرة واحدة، وأن الدفع الثاني يتخطّى الطبقات الموجودة بـ
HEAD blobs/....
OCI Distribution Spec (opencontainers/distribution-spec): أقسام «Pulling» و«Pushing». لـ BuildKit اقرأ moby/buildkit قسم «LLB» (التمثيل الوسيط كـ DAG). شغّل DOCKER_BUILDKIT=1 docker build --progress=plain لترى الـ DAG ينفّذ.
الخلاصة — الدورة اكتملت
أغلقت الحلقة: الصور التي شغّلناها (V) ووصلناها (VI) تأتي من بناءٍ وتوزيعٍ مبنيين كلياً
على العنونة بالمحتوى من III. الكاش = هاشٌ متسلسل؛ رتّب من الثابت للمتغيّر. registry
يخاطب بالـ digest فيزيل التكرار مجاناً. قواعد Dockerfile (&&
للتنظيف، ترتيب الأسطر، multi-stage) صارت اشتقاقاتٍ من III و IV، لا وصفاتٍ محفوظة.
لم يبقَ مفهومٌ جديد من الجذر. كل ما تبقّى هو جمع الخيوط في يقين: أن تنظر لأي إعدادٍ في Docker وتعرف فوراً أي بدائيةٍ يحرّك. هذا هو الإقليم الأخير.