مستوى المقال: متوسط. محتاج تكون عامل Dockerfile قبل كده وشغّلت docker build مرة على الأقل. مش لازم تكون محترف، بس لو أول مرة تسمع كلمة image يبقى ابدأ بمقال أساسيات Docker الأول.
صورة Docker بتاعتك 1.1 جيجا، والـ deploy بياخد دقايق، وانت شاحن compiler و node_modules كاملة للإنتاج من غير لزوم. الـ multi-stage build بيحل ده في ملف واحد وبينزّل الحجم لـ 95 ميجا.
Docker Multi-stage Builds: ابنِ في مطبخ، اشحن مطعم نضيف
المشكلة باختصار
لما تبني تطبيق Node أو Go أو Java، انت بتحتاج أدوات كتير وقت البناء: الـ compiler، الـ dev dependencies، أدوات الـ build زي webpack. الأدوات دي ضرورية علشان تطلّع الناتج النهائي. لكن وقت التشغيل في الإنتاج، انت مش محتاج منها ولا واحدة.
الطريقة الشائعة بتفشل هنا: بتحط كل حاجة في صورة واحدة، فتطلع صورة 1.1 جيجا فيها 90% منها أدوات مش بتتنفّذ أصلاً. النتيجة: deploy أبطأ، فاتورة تخزين أعلى، وسطح هجوم أكبر لأن كل حزمة زيادة هي ثغرة محتملة.
المفهوم بمثال بسيط، ثم بدقة
تخيّل مطعم. المطبخ فيه أفران، خلاطات، أكياس دقيق، وفوضى طبيعية وقت الطبخ. لكن لما تقدّم الطبق للزبون، انت مبتديلوش الفرن والخلاط؛ بتديله الطبق النهائي بس في صحن نضيف. لو شحنت المطبخ كله مع كل طبق، هتدفع نقل ضِعف الوزن على الفاضي.
الـ multi-stage build بيعمل نفس الفكرة. بالدقة: الـ Dockerfile بيتقسم لأكتر من مرحلة (stage)، كل مرحلة بتبدأ بـ FROM خاص بيها. المرحلة الأولى (مرحلة البناء) فيها كل الأدوات وبتطلّع الـ artifact النهائي. المرحلة الأخيرة بتبدأ من صورة أساس نظيفة وصغيرة، وبتنسخ منها الناتج بس عبر COPY --from. كل حاجة في المراحل المؤقتة بتتلغي ومتبقاش جزء من الصورة النهائية.
Dockerfile قبل وبعد
ده الشكل الشائع اللي بيطلّع صورة منفوخة. تطبيق Node بسيط:
# صورة واحدة — بتطلع حوالي 1.1GB
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
دلوقتي نفس التطبيق بـ multi-stage. لاحظ كلمة AS builder والـ COPY --from=builder:
# مرحلتين — بتطلع حوالي 95MB
# مرحلة البناء
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
RUN npm prune --omit=dev
# مرحلة التشغيل
FROM node:20-slim
WORKDIR /app
ENV NODE_ENV=production
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/package.json ./
USER node
CMD ["node", "dist/server.js"]
ابنِ وقِس الفرق بنفسك بالأوامر دي. استخدم docker images وشوف عمود SIZE:
docker build -t myapp:slim .
docker images myapp:slim
# قبل: 1.11GB
# بعد: 95.3MB
الأرقام والسيناريو الواقعي
الافتراض إن عندك خدمة API بتعمل deploy 15 مرة في اليوم على cluster من 6 nodes. الصورة الكبيرة (1.1GB) بتتسحب على كل node عند أول rollout. السحب من الـ registry بياخد حوالي 22 ثانية للصورة الكبيرة على وصلة 1Gbps، مقابل حوالي 3 ثواني للصورة الصغيرة.
- توفير وقت السحب لكل deploy عبر 6 nodes: من حوالي 132 ثانية لـ 18 ثانية.
- تخزين الـ registry: من 1.1GB لـ 95MB لكل tag. لو بتحتفظ بـ 50 tag، ده فرق حوالي 50GB.
- سطح الهجوم: نزل عدد الحزم المثبّتة في الصورة النهائية بحوالي 70% حسب docker scout.
الـ trade-offs بصراحة
كل مكسب معاه ثمن. خد بالك من الأربعة دول:
- تعقيد الملف. الـ Dockerfile بقى أطول وأصعب في القراءة. المكسب: صورة أنضف. لو فريقك مبتدئ، علّق على كل مرحلة.
- الـ debugging أصعب. الصورة النهائية مفيهاش shell أدوات ولا curl أحياناً. لو محتاج تدبّس، استهدف المرحلة الوسيطة بـ
docker build --target builder. - صورة الأساس الصغيرة لها حدود. node:20-slim أو alpine ممكن تنقص فيها مكتبات نظام زي glibc فيكسر باكدج معين. المكسب في الحجم، الخسارة احتمال توافق.
- الـ build cache. ترتيب الأوامر غلط بيكسر الـ layer caching فيبطّأ البناء. حط
COPY package*.jsonقبلCOPY . .علشان الـ cache يشتغل صح.
متى لا تستخدم هذه الطريقة
لو تطبيقك أصلاً صغير ومفيش فيه خطوة build حقيقية (سكربت Python بسيط من غير compilation مثلاً)، الـ multi-stage مش هيوفّرلك حاجة تُذكر وهيزوّد التعقيد ببلاش. وكمان لو بتشتغل لغة مفسّرة وبتحتاج نفس الـ runtime في كل الحالات، التوفير بيقل. القاعدة: الـ multi-stage بيلمع لما يكون في فرق واضح بين أدوات البناء وأدوات التشغيل (Go، Java، Node مع bundling، C++).
الخطوة التالية
افتح الـ Dockerfile الحالي بتاعك، قسّمه لمرحلتين زي المثال فوق، وشغّل docker build ثم docker images وقارن الحجم قبل وبعد. لو ما نزلش 50% على الأقل، غالباً بتنسخ حاجات زيادة في COPY --from — راجع إنك بتنقل الـ artifact النهائي بس مش المصدر كله.
المصادر
- توثيق Docker الرسمي — Multi-stage builds: docs.docker.com/build/building/multi-stage
- توثيق Docker — Best practices for writing Dockerfiles (layer caching و COPY order).
- توثيق Docker Scout لقياس الحزم وعدد الثغرات في الصورة.
- توثيق npm الرسمي — npm ci و npm prune --omit=dev.
- أرقام الحجم وزمن السحب مقاسة محلياً على node:20 مقابل node:20-slim لتطبيق Express نموذجي.