ليه صورة Docker بتاعتك 1.2 جيجا؟ الحل في الـ Multi-Stage Build
مستوى المقال: مبتدئ — لو بتبني أول Dockerfile ليك، أو صورك بتطلع تقيلة وانت مش عارف ليه، المقال ده مكتوب ليك من الصفر.
تقدر تنزّل حجم صورة Docker من 1.2 جيجابايت لـ 80 ميجابايت من غير ما تغيّر سطر واحد في كود تطبيقك. الطريقة اسمها multi-stage build، وهنعملها خطوة بخطوة دلوقتي.
المشكلة باختصار
لمّا تبني صورة عادية، Docker بيحطّ جوّاها كل حاجة استخدمتها وقت البناء: المترجم، أدوات البناء، مكتبات التطوير، الكود الخام، وملفات الكاش. بس تطبيقك وهو شغّال مش محتاج ولا واحدة من دي. النتيجة صورة تقيلة، بطيئة في الرفع والتنزيل على كل سيرفر، وسطح هجوم أكبر لأن كل أداة زيادة معناها ثغرات محتملة زيادة.
المفهوم بمثال بسيط الأول
تخيّل إنك بتطبخ طبق لضيف. مطبخك فيه بوتاجاز وحلل وسكاكين ومكوّنات خام وزبالة. اللي بتقدّمه للضيف هو الطبق النهائي بس، مش المطبخ كله. لو رفعت المطبخ بحاله وحطيته على السفرة، هتشغّل مكان ضخم بلا أي داعي.
الصورة العادية بتعمل كده بالظبط: بتشحن "المطبخ" كله مع الطبق. الـ multi-stage build بيخلّيك تطبخ في مطبخ منفصل، وبعدين تنقل "الطبق" بس لطبق نضيف تقدّمه للضيف. المطبخ بيتساب ورا، والضيف بياخد الأكل من غير الزبالة.
وبعد كده الشرح الدقيق
صورة Docker مبنية طبقات (layers). كل أمر في الـ Dockerfile بيضيف طبقة فوق اللي قبلها. من غير multi-stage، الطبقات اللي فيها أدوات البناء واعتماديات التطوير بتفضل جوّه الصورة النهائية حتى لو التطبيق مش محتاجها وقت التشغيل. الـ multi-stage build بيسمحلك تكتب أكتر من تعليمة FROM في نفس الملف؛ المرحلة الأخيرة بس هي اللي بتتشحن كصورة نهائية، وانت بتنقل ليها اللي محتاجه فعلًا من المرحلة اللي قبلها عبر COPY --from.
الحل خطوة بخطوة
ده Dockerfile عادي بمرحلة واحدة لتطبيق Node. لاحظ إنه بيسيب كل حاجة جوّه الصورة النهائية:
FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
CMD ["node", "dist/server.js"]
ودي نفس التطبيق بـ multi-stage. مرحلة للبناء، ومرحلة نحيفة للتشغيل بتاخد الناتج بس:
# المرحلة 1: البناء (المطبخ)
FROM node:20 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# المرحلة 2: التشغيل (الطبق النضيف)
FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY --from=builder /app/dist ./dist
USER node
CMD ["node", "dist/server.js"]