Docker والحاويات

حجم الصورة ونظافة البناء

18 دقيقة الدرس 9 من 30

حجم الصورة ونظافة البناء

الصورة المتضخمة في 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.js بسيط (أرقام 2025) # node:20 ~1.1 GB # node:20-bookworm-slim ~240 MB # node:20-alpine ~65 MB # gcr.io/distroless/nodejs20-debian12 ~115 MB # ثبّت الصور الأساسية دائمًا بـdigest في الإنتاج، لا بالوسم المتحرك FROM node:20-alpine@sha256:a4e5e9fa4e7e4dcf30e5e9cd36c5b6f67c208d80e7c6e93ce4d3a06e0f7d9f3 AS base
ثبّت الصور الأساسية بـdigest لا بالوسم. الوسم 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 — قالب احترافي لمشروع Node.js / Next.js # التحكم بالإصدار — غير مطلوب في الصورة أبدًا .git .gitignore .gitattributes # أسرار البيئة المحلية — يجب ألا تدخل طبقة الصورة قط .env .env.* !.env.example # التبعيات (تُعاد داخل الصورة من package-lock.json) node_modules npm-debug.log* yarn-error.log* # مخرجات الاختبار والجودة __tests__ *.test.ts *.spec.ts coverage .nyc_output jest.config.* cypress # مخرجات البناء من الجهاز المضيف dist build .next out # ضوضاء بيئة التطوير ونظام التشغيل .vscode .idea *.DS_Store Thumbs.db # إعدادات CI/CD — غير مطلوبة في وقت التشغيل .github .gitlab-ci.yml Jenkinsfile Makefile docker-compose*.yaml # التوثيق docs *.md LICENSE
غياب .dockerignore قد يُسرّب الأسرار في طبقات صورتك. إذا تضمّن سياق البناء ملفات .env ونفّذ Dockerfile تعليمة COPY . . مبكرًا، تلك الأسرار محفورة في الطبقة ويستطيع أي شخص يسحب الصورة استخراجها — حتى لو حذفتها تعليمة لاحقة. أنشئ .dockerignore دائمًا قبل كتابة أي تعليمة COPY.

البناء متعدد المراحل كاستراتيجية لتقليص الحجم

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

Dockerfile التالي يتبع هذا النمط لتطبيق Node.js وينتج صورة نهائية أقل من 150 ميغابايت من أساس يبدأ بأكثر من 1 ميغابايت:

# syntax=docker/dockerfile:1.7 # Dockerfile لواجهة Node.js برمجية في الإنتاج # ── المرحلة 1: تثبيت التبعيات ─────────────────────────────────────────────── FROM node:20-alpine AS deps WORKDIR /app # نسخ ملفات البيان فقط — الطبقة تُخزَّن مؤقتًا ما لم تتغير هذه الملفات COPY package.json package-lock.json ./ RUN npm ci --omit=dev # ── المرحلة 2: البناء ──────────────────────────────────────────────────────── FROM node:20-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ # تثبيت جميع التبعيات بما فيها devDependencies (مترجم TypeScript، إلخ) RUN npm ci COPY . . RUN npm run build # يُصدر JS مترجمًا إلى /app/dist # ── المرحلة 3: بيئة تشغيل الإنتاج ────────────────────────────────────────── FROM node:20-alpine AS production WORKDIR /app # التشغيل بمستخدم غير جذر (مبدأ الصلاحية الأدنى) RUN addgroup -S appgroup && adduser -S appuser -G appgroup # نسخ ما يحتاجه وقت التشغيل فقط COPY --from=deps /app/node_modules ./node_modules COPY --from=build /app/dist ./dist COPY --from=build /app/package.json ./ USER appuser EXPOSE 3000 CMD ["node", "dist/index.js"]

ما يجعل هذا النمط يعمل على نطاق واسع:

  • تحسين ذاكرة التخزين المؤقت للطبقات — نسخ 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؛ مرحلة الإنتاج النهائية لا تحتوي أيًا منهما. المهاجم لا يستطيع استغلال مترجم لا يستطيع الوصول إليه.
Multi-stage build: deps, build, and production stages Stage: deps node:20-alpine COPY package*.json npm ci --omit=dev node_modules (prod only) Stage: build node:20-alpine npm ci (all deps) COPY . . npm run build dist/ (compiled JS) TypeScript removed Stage: production node:20-alpine COPY --from=deps COPY --from=build USER appuser Final image ~130 MB no build tools, non-root node_modules (prod) dist/ مراحل البانية — لا تُرسل إلى السجل أبدًا
بناء ثلاثي المراحل: تبعيات الإنتاج والمخرج المترجم يتدفقان إلى صورة تشغيل خفيفة. مراحل البانية تُهمل بعد اكتمال البناء.

فحص Dockerfiles مع Hadolint

Hadolint هو أداة الفحص المعيارية للصناعة في Dockerfiles — أداة تحليل ثابت تفحص Dockerfile بمقابلة مجموعة قواعد أفضل الممارسات الرسمية وقواعد shellcheck للسكربتات المضمنة. تعمل في pipelines الـCI في معظم شركات التقنية الكبرى كبوابة مطلوبة قبل بناء أي صورة.

# تثبيت hadolint (macOS / Linux) brew install hadolint # macOS # أو سحب الحاوية — لا حاجة للتثبيت docker run --rm -i hadolint/hadolint < Dockerfile # الاستخدام النموذجي في CI (خطوة GitHub Actions) - name: Lint Dockerfile run: docker run --rm -i hadolint/hadolint hadolint \ --failure-threshold warning \ - < Dockerfile # التشغيل مع إعداد مخصص لتجاهل قواعد بعينها cat > .hadolint.yaml << 'EOF' failure-threshold: warning ignore: - DL3008 # تثبيت apt-get بلا تحديد نسخة (مقبول أحيانًا) - DL3018 # apk add بلا تحديد نسخة (نفس الحالة) EOF hadolint Dockerfile

قواعد Hadolint الأهم معرفتها بأسمائها:

  • DL3006FROM بلا وسم محدد (استخدام latest المتحرك). استخدم دائمًا إصدارًا مثبّتًا.
  • DL3007 — استخدام وسم latest صراحةً. المشكلة ذاتها بالشكل الصريح.
  • DL3008 / DL3009 / DL3018apt-get install أو apk add بلا تثبيت إصدارات الحزم. يكسر قابلية الاستنساخ عند انتهاء صلاحية الذاكرة المؤقتة.
  • DL3015apt-get install بلا --no-install-recommends. يسحب عشرات الحزم الانتقالية غير المطلوبة.
  • DL3025 — عدم استخدام صيغة JSON في CMD / ENTRYPOINT. الصيغة النصية تُغلّف أمرك بـsh -c، ما يعني أن الإشارات (كـSIGTERM من Kubernetes) لا تصل مباشرةً لعمليتك — تذهب إلى الـshell وغالبًا تُبتلع، مما يسبب انتظار 30 ثانية عند كل نشر متدحرج.
  • SC2086 — (من shellcheck) متغير غير محاط بعلامات اقتباس في الـshell — خطأ كامن في تقسيم الكلمات.
أضف Hadolint كخطاف pre-commit وفحص مطلوب في CI. تشغيله محليًا (عبر 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 في طبقة لاحقة لا يُقلّص حجم الصورة فعليًا لأن البايتات محفورة في الطبقة الأولى.
  • تنظيف ذاكرة مدير الحزم في نفس RUNapt-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.