Docker والحاويات

CMD مقابل ENTRYPOINT والإعداد

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

CMD مقابل ENTRYPOINT والإعداد

كل حاوية تحتاج أن تعرف أي عملية تُشغّل. يمنحك Docker تعليمتين لتعريف ذلك — CMD وENTRYPOINT — وفهم الفرق بينهما هو من أكثر ما يمكنك تعلمه عن Dockerfiles قيمةً عملياً. أخطئ فيه وستتجاهل الحاويات بصمت المعطيات والإشارات أو تتصرف بشكل مختلف تاماً بين بيئة التطوير وCI والإنتاج. أتقنه وستعمل صورك بشكل بديهي على سطر الأوامر وفي Kubernetes وفي الإنتاج.

دلالات الإقلاع: كيف يختار Docker العملية PID 1

حين يُشغّل Docker حاوية، يُنشئ عملية Linux معزولة بنطاق اسمي (namespace). العملية الأولى التي تعمل — PID 1 — هي init الحاوية. حين يخرج PID 1، تتوقف الحاوية. هذا مهم لسببين:

  • توزيع الإشارات: يُرسل النواة SIGTERM إلى PID 1 حين تُشغّل docker stop. إن كان PID 1 غلافاً من shell لا يُعيد توجيه الإشارات، فعمليتك الحقيقية لن تحصل أبداً على إشارة الإغلاق السلس وسيقتلها Docker بـSIGKILL بعد مهلة 10 ثوانٍ — تاركاً اتصالات مفتوحة ومعاملات غير مُودَعة أو ملفات نصف مكتوبة.
  • تبنّي العمليات الزومبي: PID 1 مسؤول عن تبنّي العمليات الزومبي (الأبناء التي خرجت لكن حالة خروجها لم تُجمَع). معظم بيئات تشغيل التطبيقات غير مكتوبة للقيام بذلك. استخدام exec form وinit بسيط مثل tini يعالج كلا المشكلتين.

Shell Form مقابل Exec Form

يقبل كلٌّ من CMD وENTRYPOINT صيغتين. هذا التمييز يقود كل شيء آخر.

# Shell form — يُشغّل Docker: /bin/sh -c "أمرك" CMD python app.py ENTRYPOINT python app.py # Exec form — يُشغّل Docker الثنائي مباشرة كـ PID 1 (بلا غلاف shell) CMD ["python", "app.py"] ENTRYPOINT ["python", "app.py"]

تُغلّف Shell form أمرك في /bin/sh -c. يصبح ذلك الـ shell هو PID 1 لا عمليتك. لا يُعيد توجيه SIGTERM إلى الأبناء افتراضياً، والـ shell يخرج حين ينتهي الأمر — لكن الـ shell نفسه هو PID 1، فالتوقيت غير قابل للتنبؤ. افضّل دائماً exec form في صور الإنتاج.

مصيدة إنتاجية — shell form وضياع الإشارات: واجهة برمجية Node.js تستخدم shell form (CMD node server.js) ستستقبل SIGTERM عند عملية الـ shell لا عند Node. يتجاهلها الـ shell (أو ينهي فوراً)، ويتلقى Node الـSIGKILL بعد مهلة إيقاف Docker. في Kubernetes، هذا يعني أن pods ستصل دائماً لمهلة إنهاء الخدمة (30 ث افتراضياً) وتُقتل قسراً، تاركةً طلبات HTTP جارية غير مكتملة. انتقل إلى exec form: CMD ["node", "server.js"].

ENTRYPOINT مقابل CMD: نموذج التفاعل

القاعدة الجوهرية بسيطة: ENTRYPOINT يُعرّف الملف التنفيذي؛ CMD يُزوّده بمعطيات افتراضية. حين يوجد كلاهما، يُسلسلهما Docker: ENTRYPOINT + CMD. المعطيات المُمرَّرة على سطر الأوامر تستبدل CMD لكن لا تستبدل ENTRYPOINT أبداً (إلا باستخدام --entrypoint).

CMD vs ENTRYPOINT interaction model CMD + ENTRYPOINT Composition Only CMD CMD ["python", "app.py"] PID 1 python app.py docker run img arg arg (CMD fully replaced) Only ENTRYPOINT ENTRYPOINT ["nginx"] PID 1 nginx docker run img -g daemon off; nginx -g daemon off; ENTRYPOINT + CMD ENTRYPOINT ["app"] CMD ["--help"] docker run img --serve (overrides CMD) PID 1 app --serve
كيف يتشكّل CMD وENTRYPOINT معاً. معطيات CLI تستبدل CMD لكن لا تستبدل ENTRYPOINT، مما يجعل ENTRYPOINT الملف التنفيذي الثابت وCMD المعطيات الافتراضية القابلة للتجاوز.

هذا التفاعل هو ما يجعل الصور ذات أسلوب CLI ممكنة — صور لأدوات مثل curl أو aws-cli أو منفذي الهجرات المخصصين حيث نقطة الدخول هي الأداة نفسها والمستخدمون يُمرّرون الأوامر الفرعية كمعطيات.

# صورة منفذ الهجرات — المستخدمون يُمرّرون الأوامر الفرعية كمعطيات FROM python:3.13-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # الملف التنفيذي ثابت؛ CMD يُعطي افتراضاً آمناً للتطوير المحلي ENTRYPOINT ["python", "-m", "alembic"] CMD ["--help"] # الاستخدام: # docker run myapp:latest -> python -m alembic --help # docker run myapp:latest upgrade head -> python -m alembic upgrade head # docker run myapp:latest history -> python -m alembic history
القاعدة الأساسية: استخدم ENTRYPOINT حين يكون للصورة غرض واحد واضح (أداة CLI أو خادم أو عامل). استخدم CMD وحده للصور الأساسية حيث يُتوقع من المستدعي تقديم أمر مختلف تاماً. استخدمهما معاً حين تريد ملفاً تنفيذياً ثابتاً بمعطيات افتراضية قابلة للضبط.

متغيرات البيئة: الإعداد وقت التشغيل

تقول منهجية تطبيق الاثني عشر عاملاً (العامل III) أن الإعداد الذي يتغير بين بيئات النشر يجب أن يأتي من متغيرات البيئة لا أن يُخبَّأ في الصورة. يمنحك Docker آليتين:

  • ENV KEY=value — يضبط متغيراً وقت البناء يستمر في الحاوية الجارية كقيمة افتراضية. يظهر في docker inspect.
  • docker run -e KEY=value أو Kubernetes env: — تجاوز وقت التشغيل. هذا هو نمط الإنتاج.
# Dockerfile لخدمة ويب جاهزة للإنتاج FROM python:3.13-slim AS base WORKDIR /app # قيم افتراضية معقولة — كلها قابلة للتجاوز وقت التشغيل ENV APP_ENV=production \ PORT=8000 \ LOG_LEVEL=info \ WORKERS=4 COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . # Exec form — بلا غلاف shell، الإشارات تصل إلى gunicorn مباشرة ENTRYPOINT ["gunicorn"] CMD ["--bind", "0.0.0.0:8000", "--workers", "4", "app:create_app()"] # --- وقت التشغيل --- # docker run -e PORT=9000 -e APP_ENV=staging myapp:latest # في Kubernetes: # env: # - name: DATABASE_URL # valueFrom: # secretKeyRef: # name: db-creds # key: url
ممارسة احترافية — لا تضع الأسرار في تعليمات ENV: ENV SECRET_KEY=abc123 يُخبّئ السر في كل طبقة صورة بشكل دائم. يظهر في docker history وdocker inspect وأي سجل صور تدفع إليه. يجب أن تصل الأسرار وقت التشغيل عبر ملفات مُحمَّلة (Kubernetes Secrets، Docker secrets) أو متغيرات بيئة تُحقنها جهة التنسيق أو SDK لإدارة الأسرار. يجب ألا تحمل الصورة أبداً بيانات اعتماد.

معطيات البناء: التحجيم وقت الترجمة

ARG هو المكافئ وقت البناء لـCMD. يُعرّف متغيراً متاحاً فقط خلال مرحلة docker build — لا يستمر في الحاوية الجارية. الاستخدامات الشائعة: تثبيت إصدارات التبعيات، تبديل أعلام التنقيح، تمرير Git SHA لتتبع البناء.

# استخدام ARG للتحجيم وقت البناء FROM node:22-alpine AS builder # ARG متاح فقط أثناء البناء؛ لا يتسرب إلى صورة وقت التشغيل ARG NODE_ENV=production ARG APP_VERSION=unknown ARG GIT_SHA=unknown WORKDIR /app COPY package*.json ./ RUN npm ci --omit=dev COPY . . # أودِع البيانات الوصفية غير السرية في تسمية الصورة (لا ENV) لإمكانية التتبع LABEL org.opencontainers.image.version="${APP_VERSION}" \ org.opencontainers.image.revision="${GIT_SHA}" RUN npm run build # --- متعدد المراحل: فقط المنتج المبني يدخل الصورة النهائية --- FROM node:22-alpine AS runtime WORKDIR /app COPY --from=builder /app/dist ./dist COPY --from=builder /app/node_modules ./node_modules ENV NODE_ENV=production \ PORT=3000 ENTRYPOINT ["node"] CMD ["dist/server.js"] # استدعاء البناء من CI: # docker build \ # --build-arg APP_VERSION=$(git describe --tags) \ # --build-arg GIT_SHA=$(git rev-parse --short HEAD) \ # --build-arg NODE_ENV=production \ # -t myapp:$(git rev-parse --short HEAD) .
مصيدة إنتاجية — ARG قبل FROM يُبطل الذاكرة المؤقتة: كل ARG يتغير (كـ Git SHA) يُبطل ذاكرة البناء المؤقتة في تلك الطبقة وكل ما يليها. ضع قيم ARG كثيرة التغيير في أواخر Dockerfile قدر الإمكان — بعد تثبيت التبعيات — حتى تظل طبقات RUN npm ci / RUN pip install مخزنة مؤقتاً. وضع ARG GIT_SHA في السطر الثاني يعني إعادة تثبيت كاملة مع كل commit.

سكريبتات Shell كنقاط دخول: نمط init

تحتاج الخدمات المعقدة كثيراً إلى القيام بعمل قبل بدء العملية الرئيسية: انتظار قاعدة البيانات، إنشاء ملف إعداد من متغيرات البيئة، تشغيل هجرات قاعدة البيانات. النمط القياسي هو سكريبت shell كنقطة دخول يستخدم exec للتسليم إلى العملية الرئيسية — مع الحفاظ على ملكية PID 1 وإعادة توجيه الإشارات.

#!/bin/sh # docker-entrypoint.sh — يُستخدم كـ ENTRYPOINT في صور API الإنتاجية set -e # اخرج عند أي خطأ # 1. انتظر قاعدة البيانات (مثال: Postgres) echo "Waiting for database at ${DB_HOST}:${DB_PORT}..." until nc -z "${DB_HOST}" "${DB_PORT}"; do sleep 1 done echo "Database ready." # 2. شغّل الهجرات (فقط على أول نسخة — استخدم قفلاً موزعاً في البنية التحتية الحقيقية) python manage.py migrate --noinput # 3. استبدل هذا الـ shell بالعملية الرئيسية (exec ضرورية — تجعلها PID 1) exec "$@"
# Dockerfile يرجع إلى السكريبت FROM python:3.13-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh RUN chmod +x /usr/local/bin/docker-entrypoint.sh # السكريبت كـ ENTRYPOINT؛ CMD يُمرَّر كـ "$@" إلى exec ENTRYPOINT ["docker-entrypoint.sh"] CMD ["gunicorn", "--bind", "0.0.0.0:8000", "app:create_app()"]

exec "$@" في نهاية سكريبت الـ shell هو السر كله. بدونه يظل الـ shell هو PID 1 وعملية gunicorn ابنٌ له. بها، يستبدل الـ shell نفسه بـgunicorn الذي يصبح PID 1 ويستقبل الإشارات مباشرة. هذا النمط يُستخدم حرفياً في صور Docker الرسمية لـPostgreSQL وRedis وNginx.

ممارسة احترافية — استخدم tini للحاويات متعددة العمليات: إن احتاجت حاويتك تشغيل عمليات متعددة (مثلاً مُصدِّر جانبي بجانب التطبيق الرئيسي)، استخدم tini كنظام init بسيط. أضف RUN apt-get install -y tini واضبط ENTRYPOINT ["/usr/bin/tini", "--", "تطبيقك"]. يتبنى Tini الزومبي ويُعيد توجيه الإشارات بشكل صحيح، وهو ما لا تفعله الـ shells العارية ومعظم بيئات تشغيل التطبيقات. مستخدمو Kubernetes يمكنهم أيضاً ضبط shareProcessNamespace: true والسماح لـkubelet بمعالجة ذلك.

ملخص: دليل اتخاذ القرار

  • استخدم exec form لكلٍّ من CMD وENTRYPOINT — دائماً.
  • استخدم ENTRYPOINT لتعريف ما هي الحاوية؛ استخدم CMD للمعطيات الافتراضية التي قد يتجاوزها المستخدم.
  • تجنب ENV للأسرار — احقنها وقت التشغيل من جهة التنسيق أو مدير الأسرار.
  • ضع تعليمات ARG في أواخر Dockerfile قدر الإمكان للحفاظ على ذاكرة الطبقات المؤقتة.
  • يجب أن تنتهي سكريبتات shell كنقاط دخول بـexec "$@" حتى ترث العملية الرئيسية PID 1.