حجم الصورة ونظافة البناء
حجم الصورة ونظافة البناء
الصورة المتضخمة في Docker ليست مجرد مشكلة جمالية — بل تؤثر مباشرةً على زمن السحب عند النشر البارد، وتوسّع سطح الثغرات الأمنية، وترفع تكاليف تخزين سجل الحاويات، وتُبطئ كل تشغيل لـpipeline الـCI. على نطاق شركات Google وAmazon وNetflix، يوفّر تخفيض 200 ميغابايت من الصورة الأساسية آلاف الدولارات من نفقات النطاق الترددي ويُسرّع عمليات النشر بشكل ملموس. تتناول هذه الدرس الأعمدة الأربعة لنظافة الصور في بيئة الإنتاج: اختيار الصورة الأساسية المناسبة، واستبعاد الملفات غير الضرورية عبر .dockerignore، وفهم البناء متعدد المراحل كاستراتيجية لتقليص الحجم، وفحص Dockerfiles لاكتشاف الأخطاء قبل وصولها إلى الإنتاج.
اختيار الصورة الأساسية المناسبة
القرار الأعلى تأثيرًا على حجم الصورة هو الصورة الأساسية. التطبيق ذاته على node:20 مقابل node:20-alpine مقابل node:20-slim قد يختلف بمقدار 400 ميغابايت — أي 400 ميغابايت من الثنائيات التي لا يُشغّلها مستخدموك قط لكن سجلك يخزنها وعقدك يسحبها.
- متغيرات
-alpineمبنية على Alpine Linux (نحو 5 ميغابايت) وتستخدمmusl libcبدلًا منglibc. هي الخيار الافتراضي للغات المترجمة ثابتًا (Go، Rust) ومعظم تطبيقات Node.js أو Python. المقايضة: بعض الامتدادات الأصلية المرتبطة بـglibc مباشرةً قد تفشل في البناء — تحقق من هذا قبل الالتزام بـAlpine في الإنتاج. - متغيرات
-slimتستخدم Debian الكامل مع حذف معظم الحزم غير الأساسية. هي البديل الأكثر أمانًا حين تكسر Alpine التبعيات الأصلية — أكبر من Alpine لكنها متوافقة تمامًا مع glibc. - صور
-distroless(من Google) تحتوي فقط على بيئة تشغيل التطبيق وتبعياته الأساسية من نظام التشغيل — لا shell ولا مدير حزم ولا أدوات مساعدة. مهاجم يحقق تنفيذ كود داخل حاوية distroless لا يستطيع تشغيلbashأوcurlأوapt. مستخدمة على نطاق واسع في Google وتتزايد استخدامها في الفرق المهتمة بالأمان. - Scratch هي صورة أساسية فارغة تمامًا (صفر بايت). تستخدم للبرامج الثنائية الواحدة في Go أو Rust التي تُترجم بشكل ثابت — الصورة الناتجة هي حرفيًا ملفك الثنائي فقط.
node:20-alpine قابل للتغيير — يمكن تحديثه بصمت إلى صورة جديدة تحتوي ثغرة أو تراجعًا. التثبيت بـ@sha256:... يضمن تشغيل البايتات ذاتها في CI والإنتاج. يجب أن تكون أداة تحديث الصور (Dependabot أو Renovate) هي ما يرفع قيمة الـdigest، لا مفاجأة عند أول docker pull.
ملف .dockerignore
كل ملف في سياق البناء يُرسل إلى Docker daemon قبل تنفيذ أول تعليمة RUN. في مشروع Node.js أو Laravel نموذجي، يشمل السياق الافتراضي (المستودع بأكمله) الـnode_modules (مئات الميغابايتات)، والـ.git (عشرات الميغابايتات من التاريخ)، وملفات اختبار، وأسرار .env المحلية، وملفات إعداد بيئة التطوير، وملفات YAML للـCI. كل هذا يصل إلى المجلد المؤقت للـdaemon، ويُبطئ البناء، ويخاطر بتسريب الأسرار في الطبقات الوسيطة إذا نُفّذت تعليمة COPY . . قبل أن تدرك حجم المشكلة.
ملف .dockerignore في جذر المشروع يتبع صيغة glob ذاتها لـ.gitignore ويحل هذه المشكلة تمامًا:
.dockerignore قد يُسرّب الأسرار في طبقات صورتك. إذا تضمّن سياق البناء ملفات .env ونفّذ Dockerfile تعليمة COPY . . مبكرًا، تلك الأسرار محفورة في الطبقة ويستطيع أي شخص يسحب الصورة استخراجها — حتى لو حذفتها تعليمة لاحقة. أنشئ .dockerignore دائمًا قبل كتابة أي تعليمة COPY.
البناء متعدد المراحل كاستراتيجية لتقليص الحجم
البناء متعدد المراحل هو أقوى تقنية متاحة لتقليص الحجم. المفهوم: استخدام صورة بانية ضخمة تحتوي كل سلاسل الترجمة ومشغّلات الاختبار وتبعيات التطوير لإنتاج مخرج التطبيق، ثم نسخ هذا المخرج فقط إلى صورة تشغيل خفيفة. مرحلة البانية لا تصل إلى المستخدمين أبدًا — تختفي بعد اكتمال docker build.
Dockerfile التالي يتبع هذا النمط لتطبيق Node.js وينتج صورة نهائية أقل من 150 ميغابايت من أساس يبدأ بأكثر من 1 ميغابايت:
ما يجعل هذا النمط يعمل على نطاق واسع:
- تحسين ذاكرة التخزين المؤقت للطبقات — نسخ
package.jsonوpackage-lock.jsonقبل بقية الكود المصدري يعني أن طبقةnpm ciتُبطل فقط عند تغيير التبعيات لا عند كل تعديل على المصدر. هذا أهم تحسين لذاكرة التخزين في Dockerfile وأكثرها إغفالًا. - فقط تبعيات الإنتاج تُشحن —
npm ci --omit=devيستبعد TypeScript وJest وESLint وكل أداة تطوير أخرى. في المشروع النموذجي ذلك تخفيض 60-80% لحجم node_modules. - مستخدم غير جذر —
USER appuserفي المرحلة النهائية يعني أن اختراق الحاوية لا يمنح صلاحية الجذر للجهاز المضيف. مطلوب بموجب معيار CIS Docker Benchmark ومعظم سياسات الأمان المؤسسية. - لا أدوات بناء في صورة وقت التشغيل — مرحلة البناء تحتوي TypeScript وwebpack؛ مرحلة الإنتاج النهائية لا تحتوي أيًا منهما. المهاجم لا يستطيع استغلال مترجم لا يستطيع الوصول إليه.
فحص Dockerfiles مع Hadolint
Hadolint هو أداة الفحص المعيارية للصناعة في Dockerfiles — أداة تحليل ثابت تفحص Dockerfile بمقابلة مجموعة قواعد أفضل الممارسات الرسمية وقواعد shellcheck للسكربتات المضمنة. تعمل في pipelines الـCI في معظم شركات التقنية الكبرى كبوابة مطلوبة قبل بناء أي صورة.
قواعد Hadolint الأهم معرفتها بأسمائها:
- DL3006 —
FROMبلا وسم محدد (استخدامlatestالمتحرك). استخدم دائمًا إصدارًا مثبّتًا. - DL3007 — استخدام وسم
latestصراحةً. المشكلة ذاتها بالشكل الصريح. - DL3008 / DL3009 / DL3018 —
apt-get installأوapk addبلا تثبيت إصدارات الحزم. يكسر قابلية الاستنساخ عند انتهاء صلاحية الذاكرة المؤقتة. - DL3015 —
apt-get installبلا--no-install-recommends. يسحب عشرات الحزم الانتقالية غير المطلوبة. - DL3025 — عدم استخدام صيغة JSON في
CMD/ENTRYPOINT. الصيغة النصية تُغلّف أمرك بـsh -c، ما يعني أن الإشارات (كـSIGTERMمن Kubernetes) لا تصل مباشرةً لعمليتك — تذهب إلى الـshell وغالبًا تُبتلع، مما يسبب انتظار 30 ثانية عند كل نشر متدحرج. - SC2086 — (من shellcheck) متغير غير محاط بعلامات اقتباس في الـshell — خطأ كامن في تقسيم الكلمات.
pre-commit أو هدف Makefile) يكتشف المشكلات في ثوانٍ. تشغيله في CI كفحص مطلوب يضمن عدم وصول أي Dockerfile فاشل للسجل. كثير من الفرق تضيف أيضًا docker scout cves أو trivy image كبوابة CI ثانية لاكتشاف الثغرات في الصور الأساسية بعد البناء — نظافة الطبقات وفحص الثغرات متكاملان لا بديلان.
ممارسات إضافية لنظافة البناء
إلى جانب اختيار الصورة الأساسية و.dockerignore والبناء متعدد المراحل والفحص، توجد عادات أصغر تميّز Dockerfile الاحترافي:
- دمج تعليمات
RUNالمنتمية معًا (مثلًا:apt-get update && apt-get install && rm -rf /var/lib/apt/lists/*في تعليمةRUNواحدة). كلRUNتُنشئ طبقة؛ فصلها يعني أن التنظيف بـrm -rfفي طبقة لاحقة لا يُقلّص حجم الصورة فعليًا لأن البايتات محفورة في الطبقة الأولى. - تنظيف ذاكرة مدير الحزم في نفس
RUN—apt-get clean،rm -rf /var/lib/apt/lists/*،pip install --no-cache-dir،npm ci && npm cache clean --force. التنظيف في طبقة لاحقة لا ينفع لأن بايتات الذاكرة المؤقتة ملتزمة بالفعل. - تعريف بيانات
LABEL— على الأقلorg.opencontainers.image.sourceو.versionو.revisionحتى تتمكن الأدوات من تتبع الصورة إلى commit المصدر وتشغيل الـpipeline. - استخدام
COPYلاADDإلا إذا احتجت تحديدًا لميزات ADD في جلب URLs أو فك ضغط tar.COPYصريح ويمكن التنبؤ به؛ADDله سلوك فك ضغط تلقائي مفاجئ. - تعريف
HEALTHCHECKحتى يتمكن Docker والمُنسّقون من اكتشاف العملية التي تعمل لكنها لا تخدم الطلبات — مهم لـdepends_on: condition: service_healthyفي Compose ومسابر liveness في Kubernetes.