Docker والحاويات

كتابة ملفات Dockerfile

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

كتابة ملفات Dockerfile

يُعدّ ملف Dockerfile المصدرَ الوحيد للحقيقة فيما يخص طريقة تجميع صورة الحاوية. كل صورة إنتاجية في الشركات الجادة — سواء كانت Node API أو Python ML worker أو Go microservice — تبدأ من هنا. كتابة Dockerfile بشكل رديء تعني بنيات CI بطيئة، وصور ضخمة، وسلوك غير متوقع في وقت التشغيل، وسطح أمني لا يمكن تفسيره. أما كتابتها بشكل صحيح فتعني اختراقات ذاكرة التخزين المؤقت في 10 ثوان، وصور نهائية بحجم 20 ميغابايت فقط، وبنيات قابلة للتكرار على جهاز كل مطور وعلى كل CI runner.

يستعرض هذا الدرس كل تعليمة مهمة، ويوضح سبب كون ترتيب الطبقات اعتباراً رئيسياً، ويُريك كيف تبدو ملفات Dockerfile الصديقة للتخزين المؤقت في الإنتاج الفعلي.

التعليمات الأساسية

FROM — اختيار الصورة الأساسية

FROM هي دائماً التعليمة الأولى. تحدد الصورة الأساسية التي تُبنى صورتك فوقها. كل تعليمة لاحقة تضيف طبقة جديدة فوق تلك الأساس.

في الشركات الكبرى، ثلاث قواعد تحكم FROM:

  • دائماً ثبّت digest محدد أو على الأقل وسماً محدداً — لا تستخدم FROM node:latest أبداً. الوسوم العائمة تكسر قابلية التكرار بصمت عندما ينشر المصدر صورة جديدة.
  • فضّل الصور الأساسية الرسمية المصغّرة: node:22-alpine، python:3.12-slim، golang:1.22-bookworm. متغيرات Alpine وslim أصغر بكثير من الصور الكاملة المبنية على Debian.
  • للثنائيات المُجمَّعة ثابتياً (Go، Rust)، فضّل FROM scratch — المرحلة النهائية لا تحتوي على أي سطح نظام تشغيل.
# سيء — غير مثبّت، وليس بالحد الأدنى FROM node:latest # أفضل — وسم مثبّت، قاعدة Alpine FROM node:22.3-alpine3.20 # الأفضل (بناء متعدد المراحل) — ثبّت مرحلة البناء، scratch أو distroless للتشغيل FROM node:22.3-alpine3.20 AS builder FROM gcr.io/distroless/nodejs22-debian12 AS runtime

COPY — إحضار الملفات إلى الصورة

COPY <src> <dest> تنسخ الملفات من سياق البناء (نظام ملفاتك المحلي) إلى طبقة الصورة. ADD موجودة أيضاً لكن يجب تجنبها إلا إذا احتجت تحديداً لاستخراج tar التلقائي أو جلب URL البعيد — كلاهما مصدر مشاكل. استخدم COPY افتراضياً.

الأعلام المهمة التي ستستخدمها فعلياً في الإنتاج:

  • --chown=user:group — تحديد الملكية في خطوة واحدة بدلاً من تعليمة RUN chown منفصلة (التي ستنشئ طبقة إضافية).
  • --from=builder — النسخ من مرحلة أخرى في بناء متعدد المراحل (يُغطى في درس لاحق).
احرص دائماً على وجود ملف .dockerignore بجانب Dockerfile. بدونه، COPY . . ترسل مجلد عملك بالكامل إلى سياق البناء — بما يشمل .git/، وnode_modules/، وملفات الاختبار، وملفات .env المحلية. ملف .dockerignore جيد يُقلص نقل سياق البناء من غيغابايتات إلى كيلوبايتات.

RUN — تنفيذ خطوات البناء

RUN تُنفّذ أمر shell أثناء البناء وتحفظ النتيجة كطبقة جديدة. كل تعليمة RUN هي مفتاح تخزين مؤقت. أهم قاعدة في الإنتاج: اجمع الأوامر المترابطة في تعليمة RUN واحدة، خاصة عند تثبيت الحزم وتعديلها وتنظيفها — لأنك لو قسّمتها على عدة تعليمات RUN، ستجمّد الملفات الوسيطة في الطبقات السابقة حتى بعد حذفها.

# سيء — يُنشئ 3 طبقات؛ ذاكرة apt مجمّدة في الطبقة 1 حتى بعد الحذف في الطبقة 3 RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # الصحيح — طبقة واحدة، النتيجة النهائية صورة أصغر RUN apt-get update \ && apt-get install -y --no-install-recommends curl ca-certificates \ && rm -rf /var/lib/apt/lists/*

الشكل shell مقابل الشكل exec: RUN apt-get update هو شكل shell (يعمل عبر /bin/sh -c). يمكنك أيضاً استخدام شكل exec: RUN ["apt-get", "update"]. شكل shell أكثر قراءة للأوامر المتسلسلة؛ شكل exec يتجنب تفسير shell ويُفضَّل في CMD وENTRYPOINT.

CMD وENTRYPOINT — تعريف سلوك التشغيل

هاتان التعليمتان مصدر ارتباك دائم. القاعدة بسيطة بعد استيعابها:

  • ENTRYPOINT — الملف التنفيذي الثابت الذي يعمل دائماً. يحدد ما تكونه الحاوية.
  • CMD — الوسائط الافتراضية التي تُمرَّر إلى ENTRYPOINT (أو، إن لم يكن هناك ENTRYPOINT، الأمر الافتراضي للتشغيل). يحدد الإعدادات الافتراضية المعقولة التي يمكن تجاوزها عند docker run.

كلاهما يقبل شكل shell وشكل exec. استخدم دائماً شكل exec (["executable", "arg1"]) لـ CMD وENTRYPOINT. شكل shell يُغلّف العملية في /bin/sh -c، مما يجعلها PID 2 بدلاً من PID 1 — وهذا يعني أن الإشارات (SIGTERM، SIGINT) من Docker أو Kubernetes لا تُسلَّم أبداً لعمليتك، مما يُسبب إيقاف تشغيل غير نظيف ونشرات متداول بطيئة.

# شكل shell — سيء للإنتاج؛ تطبيقك لن يستقبل SIGTERM أبداً ENTRYPOINT node server.js # شكل exec — الصحيح؛ node هو PID 1 ويستقبل الإشارات مباشرة ENTRYPOINT ["node", "server.js"] # النمط الإنتاجي المعتاد: ملف تنفيذي ثابت + وسائط افتراضية قابلة للتجاوز ENTRYPOINT ["python", "-m", "gunicorn"] CMD ["--workers", "4", "--bind", "0.0.0.0:8000", "myapp.wsgi:application"] # التجاوز عند التشغيل: docker run myimage --workers 8 --bind 0.0.0.0:9000
فخ إنتاجي شائع: إذا عرّفت ENTRYPOINT وCMD معاً، يُزوّد CMD وسائط افتراضية لـ ENTRYPOINT. لكن إذا تجاوزت CMD عند docker run، تُستبدل CMD بالكامل — لا تُدمج. صمّم واجهة الوسائط الخاصة بك وفق ذلك.

ترتيب الطبقات وملفات Dockerfile الصديقة للتخزين المؤقت

تخزين Docker المؤقت للبناء مُقيَّد بنص التعليمة ومحتوى أي ملفات مشار إليها. بمجرد إلغاء صلاحية طبقة، يجب إعادة بناء كل طبقة لاحقة. هذا يعني أن ترتيب الطبقات قرار مهم للأداء، وليس قرار جماليات فقط.

القاعدة الذهبية: رتّب التعليمات من الأقل تغيراً إلى الأكثر تغيراً.

Dockerfile layer ordering — cache-friendly vs cache-busting Cache-Busting (Bad) FROM node:22-alpine ✓ always cached COPY . . ✗ invalidated on ANY file change RUN npm install ✗ re-runs every time RUN npm run build ✗ re-runs every time Cache-Friendly (Good) FROM node:22-alpine ✓ always cached COPY package*.json ./ ✓ cached unless deps change RUN npm ci ✓ cached unless deps change COPY . . ✓ only invalidates build step RUN npm run build ✓ only re-runs on code change المبدأ الأساسي انسخ ملفات الاعتمادية قبل كود المصدر.
ترتيب الطبقات: نسخ ملفات بيان الاعتمادية قبل كود المصدر يحافظ على ذاكرة التخزين المؤقت لـ npm/pip عند تغيير الكود فقط.

ملف Dockerfile إنتاجي متكامل (Node.js API)

فيما يلي ملف Dockerfile كامل وحقيقي يدمج كل المبادئ السابقة. ادرس الترتيب والتعليقات:

# syntax=docker/dockerfile:1.7 FROM node:22.3-alpine3.20 AS builder # تحديد مجلد العمل WORKDIR /app # 1. نسخ ملفات بيان الاعتمادية أولاً فقط — يزيد نسبة اختراق الذاكرة المؤقتة COPY package.json package-lock.json ./ # 2. تثبيت الاعتمادية؛ npm ci قابل للتكرار (يحترم ملف القفل تماماً) # --omit=dev يستبعد devDependencies وقت التثبيت لطبقة البناء RUN npm ci # 3. نسخ المصدر بعد تثبيت الاعتمادية COPY . . # 4. البناء (transpile, bundle, الخ) RUN npm run build # ---- مرحلة التشغيل ---- FROM node:22.3-alpine3.20 ENV NODE_ENV=production WORKDIR /app # التشغيل كمستخدم غير root — مبدأ أقل الامتيازات RUN addgroup -S appgroup && adduser -S appuser -G appgroup # نسخ اعتمادية الإنتاج فقط + الأداة المبنية — لا شيء من devDependencies COPY --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules COPY --from=builder --chown=appuser:appgroup /app/dist ./dist USER appuser EXPOSE 3000 # شكل exec حتى يكون node هو PID 1 ويستقبل SIGTERM بشكل نظيف ENTRYPOINT ["node"] CMD ["dist/server.js"]
شغّل الحاويات دائماً كمستخدم غير root. إذا تعرضت حاويتك للاختراق، فإن المهاجم الذي يعمل كـ root داخل الحاوية سيملك فرصاً أكثر بكثير للهروب إلى المضيف أو تسريب الأسرار من الوحدات المُثبّتة. RUN adduser + USER ربح أمني في سطرين فقط.

تعليمات مفيدة أخرى

  • WORKDIR /app — تحدد مجلد العمل للتعليمات اللاحقة. فضّلها على RUN cd /app الذي لا يستمر.
  • ENV KEY=value — تضع متغيرات بيئة متاحة في وقت البناء والتشغيل معاً. استخدمها لـ NODE_ENV=production، PYTHONUNBUFFERED=1، إلخ. لا تستخدم ENV للأسرار — فهي مدمجة في الصورة ومرئية عبر docker history.
  • ARG — متغير وقت البناء فقط، لا يستمر في الصورة. آمن للأشياء كأرقام الإصدار: ARG APP_VERSION=1.0.0.
  • EXPOSE 3000 — يوثّق المنفذ الذي تستمع إليه الحاوية. لا ينشر المنفذ فعلياً؛ ذلك يحدث عند docker run -p أو في Docker Compose. اعتبره بيانات وصفية للمشغّلين.
  • LABEL — إرفاق بيانات وصفية (المطوّر، الإصدار، git SHA). مفيد لـ docker inspect وأنظمة الجرد الآلية.
ذاكرة البناء المؤقتة خاصة بالجهاز والسجل. في pipelines CI بدون backend تخزين مؤقت مشترك، كل بناء يبدأ بارداً. استخدم أعلام BuildKit --cache-from / --cache-to أو cache-to=type=gha في GitHub Actions للحفاظ على ذاكرة الطبقات المؤقتة بين تشغيلات pipeline — وهذا غالباً أكبر تسريع متاح لـ CI.

ملخص

ملف Dockerfile الاحترافي يُحدَّد بأربع عادات: ثبّت صورتك الأساسية، اجمع أوامر RUN لتطوي الطبقات، رتّب التعليمات من الأقل تغيراً إلى الأكثر، واستخدم دائماً شكل exec لـ ENTRYPOINT وCMD. كل انحراف له تكلفة حقيقية — سواء في حجم الصورة، أو وقت البناء، أو موثوقية التشغيل. اجعل هذه المبادئ هي الافتراضية، لا الاستثناء.