مستوى المقال: متوسط. محتاج تكون عامل 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"]