دليل عملي لـ Docker و Kubernetes للمطورين المعاصرين
دليل بدون حشو للحاويات، أفضل ممارسات Dockerfile، أساسيات Kubernetes، ومعرفة متى تحتاج فعلاً إلى منظم حاويات.
كل مطور يصطدم بنفس الجدار في النهاية. تكتب كوداً على جهازك، يعمل بشكل مثالي، تدفعه إلى بيئة الاختبار، وينفجر بخطأ غامض حول تبعية نظام مفقودة أو إصدار مكتبة مختلف. مشكلة "يعمل على جهازي" الكلاسيكية تطارد تطوير البرمجيات لعقود، ولم يحلها Docker فحسب — بل جعل الحل بسيطاً جداً لدرجة أنه لا يوجد الآن عذر لعدم استخدامه.
لكن Docker يحل فقط مشكلة التغليف. بمجرد أن تحوي تطبيقك، لا تزال بحاجة إلى تشغيله في الإنتاج — ربما عبر خوادم متعددة، مع موازنة التحميل، ونشر بدون توقف، وفحوصات الصحة، واسترداد تلقائي. هذا هو مكان دور Kubernetes. وهناك أيضاً حيث يضيع معظم المطورين في التعقيد، لأن Kubernetes يقدم مفردات جديدة تماماً من التجريدات التي تستغرق وقتاً لاستيعابها.
هذا الدليل يقطع من خلال الضوضاء. يغطي ما هي الحاويات فعلاً تحت الغطاء، وكيفية كتابة Dockerfiles فعالة وآمنة، ومفاهيم Kubernetes التي تحتاج إلى معرفتها لنشر تطبيقات حقيقية، والمقايضات بين Docker Compose و Kubernetes للتطوير المحلي، والأهم من ذلك — متى يجب عليك استخدام كل أداة ومتى تتركها.
ما هي الحاويات فعلاً
الحاوية ليست آلة افتراضية خفيفة. هذا هو المفهوم الخاطئ الأكثر شيوعاً، ويؤدي إلى نماذج عقلية غير صحيحة. الآلة الافتراضية تشغل نظام تشغيل ضيف كامل فوق مشغل آلات افتراضية، مع نواتها الخاصة، وتخصيص الذاكرة الخاص، وبرامج تشغيل الأجهزة الخاصة بها. الحاوية تشارك نواة المضيف وتعمل كعملية مساحة مستخدم معزولة. العزل يتم توفيره بواسطة ميزات نواة لينكس — مساحات الأسماء لعزل العمليات، مجموعات التحكم لحدود الموارد، وأنظمة الملفات المتراكبة لطبقات الصور الفعالة.
هذا التمييز مهم لأنه يشرح السلوك الذي ستلاحظه. الحاويات تبدأ في أجزاء من الثانية لأنه لا توجد نواة لتشغيلها. تستخدم ذاكرة أقل لأنه لا توجد نواة مكررة ولا عمليات نظام زائدة. لكنها تعني أيضاً أن حاوية تعمل على لينكس لا يمكنها تشغيل إصدار نواة مختلف عن المضيف، وحاوية ويندوز تتطلب مضيف ويندوز. على macOS، يشغل Docker Desktop حاويات لينكس داخل آلة افتراضية خفيفة لهذا السبب تحديداً.
الصورة هي القالب للقراءة فقط — لقطة لنظام ملفات بالإضافة إلى بيانات وصفية. الحاوية هي نسخة عاملة من تلك الصورة، مع طبقة قابلة للكتابة في الأعلى. يمكنك بناء صورة مرة واحدة وتشغيل عشرات الحاويات منها. هذه هي الوحدة الأساسية للتشغيل في عالم Docker، وفهمها بوضوح يجعل كل شيء آخر أسهل.
أفضل ممارسات Dockerfile
Dockerfile هو وصفة لبناء صورة. كل تعليمة تنشئ طبقة جديدة، والطبقات مخزنة مؤقتاً. هذا يعني أن ترتيب التعليمات يؤثر مباشرة على سرعة البناء، وحجم الصورة، والأمان. إليك المبادئ الأكثر أهمية في المشاريع الحقيقية.
ترتيب الطبقات حسب تواتر التغيير
Docker يخزن كل طبقة مؤقتاً بعد بنائها. إذا لم تتغير طبقة منذ آخر بناء، يعيد Docker استخدام النسخة المخزنة. هذا يعني أنه يجب عليك وضع التعليمات التي تتغير نادراً في الأعلى والتعليمات التي تتغير بشكل متكرر في الأسفل. تبعيات النظام (apt-get، apk add) تتغير أبداً تقريباً. تبعيات التطبيق (npm install، pip install) تتغير عندما تحدث ملف القفل الخاص بك. كود مصدر التطبيق يتغير مع كل commit.
# Bad: source code before dependencies
FROM node:20-alpine
WORKDIR /app
COPY . . # busts the cache for everything below
RUN npm ci # runs on every build, even if package.json did not change
EXPOSE 3000
CMD ["node", "dist/index.js"]
# Good: stable-first layer ordering
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci # cached unless package.json changes
COPY . . # only the source changes bust this layer
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/index.js"]الفرق كبير. Dockerfile السيئ يعيد بناء جميع التبعيات مع كل commit. الجيد يعيد بناء التبعيات فقط عندما يتغير ملف القفل، وهو عادةً مرة واحدة لكل طلب سحب بدلاً من كل commit. في مشروع Node.js مع 500 تبعية، هذا يمكن أن يوفر دقيقتين لكل بناء.
البناء متعدد المراحل
البناء متعدد المراحل يتيح لك استخدام Dockerfile واحد لبناء تطبيقك وإنتاج صورة تشغيل صغيرة. مرحلة البناء تحتوي على المترجمات، وتبعيات التطوير، وأدوات البناء. مرحلة التشغيل تنسخ فقط المخرجات المترجمة. هذا يحافظ على صور الإنتاج صغيرة ويقلل من سطح الهجوم.
# Build stage
FROM node:20-alpine AS builder
WORKDIR /build
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
# Runtime stage — starts from a fresh, minimal base
FROM node:20-alpine AS runner
WORKDIR /app
# Only what is needed to run
COPY --from=builder /build/dist ./dist
COPY --from=builder /build/package.json ./
COPY --from=builder /build/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/index.js"]مرحلة التشغيل لا تشمل مترجم TypeScript، أو ملفات المصدر، أو أي تبعيات تطوير. لتطبيق نموذجي، هذا يقلص الصورة من 800 MB إلى أقل من 200 MB. بناء COPY --from=builder هو البصيرة الرئيسية — يسحب ملفات من مرحلة سابقة دون حمل الطبقات من تلك المرحلة.
التشغيل كمستخدم غير جذري
الحاويات تعمل كجذر افتراضياً. هذا خطر أمني: إذا استغل مهاجم تطبيقك، فلديه وصول جذر داخل الحاوية. الإصلاح هو سطر واحد في Dockerfile يتحول إلى مستخدم غير جذري. معظم الصور الأساسية تأتي مع مستخدم node أو nobody لهذا الغرض.
بعيداً عن الأمان، البناء متعدد المراحل والترتيب المناسب للطبقات يحسنان أيضاً سرعة خط CI/CD. كل دقيقة يتم توفيرها في بناء صورة هي دقيقة لا ينتظرها مطوروك للنشر. في فريق من عشرة مطورين ينشرون خمس مرات في اليوم، توفير دقيقتين لكل بناء يستعيد أكثر من ستين ساعة من وقت المطورين سنوياً.
أساسيات Kubernetes
Kubernetes هو منظم حاويات. يأخذ مجموعة من الآلات (العقد)، ويجدول الحاويات عليها، ويبقيها قيد التشغيل، ويتعامل مع الشبكات، ويوفر API تصريحي لوصف الحالة المرغوبة لنظامك. تخبر Kubernetes بما تريد — ثلاث نسخ متماثلة من خادم API الخاص بك، منفذ 8080 مكشوف، استراتيجية تحديث متدرج — ويحقق ذلك.
منحنى التعلم حقيقي لأن Kubernetes يقدم مجموعة متعددة الطبقات من التجريدات. الثلاثة التي ستتفاعل معها أكثر هي Pods وDeployments وServices.
Pods
Pod هي أصغر وحدة قابلة للنشر في Kubernetes. تمثل حاوية أو أكثر تشترك في مساحة اسم الشبكة ووحدات تخزين. عملياً، معظم Pods تشغل حاوية واحدة. أنماط Sidecar (حاوية رئيسية بالإضافة إلى حاوية تسجيل أو بروكسي) تستخدم Pods متعددة الحاويات، لكن لنشر التطبيق اليومي، ستستخدم حاوية واحدة لكل Pod.
نادراً ما تنشئ Pods مباشرة. Pods مؤقتة — يمكن إنهاؤها وإعادة جدولتها في أي وقت. إذا أنشأت Pod يدوياً وفشلت العقدة التي تعمل عليها، يختفي Pod إلى الأبد. هذا هو المكان الذي تأتي فيه Deployments.
Deployments
Deployment يدير مجموعة من Pods المتطابقة (ReplicaSet). يتعامل مع التحديثات المتدرجة، والتوسع، والشفاء الذاتي، والتراجعات. هذا هو المورد الذي ستستخدمه لنشر التطبيقات عديمة الحالة.
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
labels:
app: api-server
spec:
replicas: 3
selector:
matchLabels:
app: api-server
template:
metadata:
labels:
app: api-server
spec:
containers:
- name: api
image: myregistry/api-server:v1.2.3
ports:
- containerPort: 3000
resources:
requests:
cpu: 250m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
livenessProbe:
httpGet:
path: /healthz
port: 3000
readinessProbe:
httpGet:
path: /ready
port: 3000هذا التصريح يعلن ثلاث نسخ متماثلة من خادم API. Kubernetes سيضمن أن ثلاثة Pods تعمل دائماً. إذا تعطل Pod، ينشئ Kubernetes بديلاً. أثناء التحديث المتدرج (تغيير وسم الصورة)، يستبدل Kubernetes Pods واحدة تلو الأخرى، مما يضمن عدم التوقف. فحص البقاء يخبر Kubernetes متى يكون Pod سليماً؛ فحص الاستعداد يخبره متى يكون Pod جاهزاً لاستقبال حركة المرور.
Services
Pods لها عناوين IP ديناميكية. كل مرة يُعاد إنشاء Pod، يحصل على IP جديد. Service توفر نقطة نهاية شبكة ثابتة توازن حركة المرور عبر Pods المطابقة لمحددها. هذه هي الطريقة التي تجد بها أجزاء أخرى من نظامك وتتحدث إلى تطبيقك.
apiVersion: v1
kind: Service
metadata:
name: api-server
spec:
selector:
app: api-server
ports:
- port: 80
targetPort: 3000
type: ClusterIPهذا الخدمة يربط المنفذ 80 على IP داخلي مستقر للكتلة بالمنفذ 3000 على Pods ذات الوسم app: api-server. الخدمات الأخرى داخل الكتلة يمكنها الوصول إليه عبر اسم DNS api-server. لحركة المرور الخارجية، ستستخدم LoadBalancer أو Ingress فوق الخدمة.
Kubernetes ليس منصة تنشر عليها. إنه منصة تصف نشرك لها. الفرق بين الأمر والتصريح هو أهم تحول عقلي يمكنك القيام به.
التطوير المحلي: Docker Compose مقابل Kubernetes
أكبر خطأ ترتكبه الفرق هو افتراض أنها تحتاج Kubernetes للتطوير المحلي لأنها تستخدمه في الإنتاج. Docker Compose و Kubernetes يخدمان أغراضاً مختلفة، واختيار الخطأ للعمل المحلي يخلق احتكاكاً غير ضروري.
Docker Compose مصمم للتطوير المحلي. يعمل على جهاز واحد، يبدأ الحاويات في ثوانٍ، وله تنسيق YAML بسيط يتوافق مباشرة مع ما تحتاجه: خادم ويب، قاعدة بيانات، نسخة Redis، وربما عامل قائمة انتظار. تعرف الخدمات، و docker compose up يجلب كل شيء عبر الإنترنت، مع السجلات المتدفقة إلى طرفيتك، والمنافذ المعينة إلى localhost، وإعادة التحميل التلقائي يعمل خارج الصندوق.
version: "3.8"
services:
api:
build: .
ports:
- "3000:3000"
volumes:
- .:/app
- /app/node_modules
environment:
- DATABASE_URL=postgres://user:pass@db:5432/app
depends_on:
- db
db:
image: postgres:16-alpine
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:ملف Compose هذا يمنحك بيئة تطوير عاملة مع إعادة تحميل تلقائي، وقاعدة بيانات محلية بتخزين دائم، وشبكات مناسبة بين الخدمات. يستغرق حوالي ثلاثين سطراً من YAML ويبدأ في أقل من عشر ثوانٍ.
Minikube وKind وk3s يمكنها تشغيل Kubernetes محلياً، لكنها تضيف عبئاً كبيراً. تتطلب ذاكرة أكثر، وتستغرق وقتاً أطول للبدء، وتقدم تعقيداً (وحدات تحكم الدخول، شبكات الخدمات، فئات التخزين) لا تحتاجه ببساطة عندما تكرر على ميزة واحدة. تشغيل Kubernetes محلياً مفيد لاختبار سلوك Kubernetes المحدد — مثل سياسات إخلاء Pod، والتوسع التلقائي الأفقي للـ Pod، أو تعريفات الموارد المخصصة — لكنه ليس بديلاً عن Compose في التطوير اليومي.
- استخدم Docker Compose للتطوير المحلي. إنه سريع وبسيط ويتوافق مباشرة مع الحاويات التي تشغلها.
- استخدم Kubernetes (عبر Minikube أو Kind) للاختبار التكاملي عندما تستخدم بنيتك التحتية الإنتاجية ميزات Kubernetes مثل ConfigMaps أو Secrets أو وحدات التحكم المخصصة.
- استخدم مجموعة تطوير عن بعد فقط عندما تحتاج إلى وصول GPU، أو أجهزة متخصصة، أو بيئة اختبار مشتركة تعكس الإنتاج تماماً.
- لا تشغل مجموعتي Kubernetes محلياً فقط لأن لديك بيئتين. Compose يتعامل مع ذلك بعلم --profile واحد.
- إذا كان فريقك يقضي وقتاً أطول في تصحيح تكوينات Kubernetes من كتابة كود التطبيق، فقد تجاوزت أضواءك الأمامية. تراجع إلى Compose وأضف التعقيد فقط عندما يتجاوز ألم عدم وجوده ألم الحفاظ عليه.
المزالق الشائعة وكيفية تجنبها
حتى بعد فهم المفاهيم، بعض الأخطاء تتكرر عبر الفرق. إليك تلك التي تستحق الحفظ حتى تتمكن من تخطي جلسات التصحيح التي تستغرق يومين.
فوضى وسم الصورة هي مشكلة الإنتاج الأكثر شيوعاً. استخدام latest كوسم صورة في Kubernetes يعني أنك لا تستطيع معرفة أي إصدار يعمل على أي عقدة معينة. Kubernetes يسحب الصورة فقط إذا لم تكن موجودة على العقدة، لذا latest على عقدة قد تكون إصداراً مختلفاً عن latest على أخرى. استخدم دائماً وسوم الإصدار الدلالي أو SHA للالتزام. الأفضل من ذلك، استخدم ملخص صورة مؤهل بالكامل — هذا هو الشيء الوحيد المضمون أن يكون غير قابل للتغيير.
طلبات الموارد والحدود غالباً ما يتم حذفها أو تعيينها بشكل عشوائي. إذا لم تحدد الطلبات، لا يمكن لـ Kubernetes جدولة Pods بذكاء، وتصبح العقد محملة بشكل زائد. إذا لم تحدد الحدود، تسرب ذاكرة في Pod واحد يمكن أن يعطل Pods أخرى على نفس العقدة. حدد كليهما، واستخدم أدوات مثل Vertical Pod Autoscaler في وضع التوصية لضبطها بناءً على الاستخدام الفعلي.
ConfigMaps وSecrets يتم تركيبها كمتغيرات بيئة أو ملفات. متغيرات البيئة مريحة، لكن أي تغيير يتطلب إعادة تشغيل Pod ليصبح ساري المفعول. التركيبات القائمة على الملفات يمكن تحديثها دون إعادة تشغيل (Pod يقرأ المحتوى الجديد عند الوصول إلى الملف)، لكن العديد من التطبيقات تخزن التكوين مؤقتاً عند بدء التشغيل. اعرف أي نمط يستخدمه تطبيقك، وصمم نهج التكوين وفقاً لذلك.
وحدات التخزين المستمرة في Kubernetes ليست سحرية. PersistentVolumeClaim تطلب تخزيناً، لكن فئة التخزين الأساسية يجب أن تكون مهيأة لمزود السحابة الخاص بك. فئات التخزين الافتراضية قد تستخدم تخزيناً متصلاً بالشبكة له خصائص أداء مختلفة عن أقراص SSD المحلية. إذا كان أداء قاعدة البيانات مهماً، قم بقياس أداء فئة التخزين الخاصة بك قبل الانتقال إلى الإنتاج.
التسجيل والتصحيح في Kubernetes أصعب منه على خادم واحد. Pods مؤقتة، لذا تختفي السجلات عندما يُحذف Pod. استخدم kubectl logs --tail=50 -f pod-name للتتبع المباشر، لكن لتصحيح الإنتاج، تحتاج إلى حل تسجيل مركزي (Loki، Elasticsearch، أو خدمة تسجيل سحابية). بالمثل، استخدم kubectl exec -it pod-name -- sh لتفقد حاوية عاملة، لكن تذكر أن أي تغييرات تجريها داخل حاوية عاملة تُفقد عند إعادة التشغيل.
هل تحتاج حقاً إلى Kubernetes؟
هذا هو السؤال الذي لا أحد يريد طرحه لأن Kubernetes يبدو جيداً في السيرة الذاتية الهندسية ويشير إلى النضج التشغيلي. لكن Kubernetes هو حل لمشكلة محددة: تشغيل خدمات محوية متعددة عبر آلات متعددة مع استرداد تلقائي، وتوسع، ونشر متدرج. إذا كان لديك خدمة أو اثنتين تعملان على خادم واحد، Kubernetes هو إفراط.
إليك إطار قرار مباشر. إذا كنت تنشر تطبيقاً واحداً يخدم أقل من عشرة آلاف طلب في الثانية، خادم واحد مع Docker Compose في الإنتاج (نعم، Compose يعمل بشكل جيد في الإنتاج لأحمال عمل عديدة) سيخدمك جيداً. أضف بروكسياً عكسياً مثل Caddy أو Nginx، وأعد نسخاً احتياطية آلية، وستحصل على نظام إنتاجي يمكن لمطور واحد فهمه وصيانته بالكامل.
انتقل إلى Kubernetes عندما يكون لديك خدمات متعددة تحتاج إلى النشر بشكل مستقل، عندما تحتاج إلى توسع لكل خدمة (API الخاص بك يحتاج عشر نسخ متماثلة لكن العامل الخاص بك يحتاج اثنتين فقط)، عندما تحتاج إلى نشر بدون توقف كعملية روتينية، أو عندما يكون لدى فريقك على الأقل شخص واحد مسؤوليته الأساسية هي البنية التحتية. قبل استيفاء هذه الشروط، التكلفة التشغيلية لـ Kubernetes — سواء في إدارة الكتلة أو في العبء المعرفي للمطور — هي صافية سلبية.
العديد من الفرق تستفيد من حل وسط. استخدم Docker Compose للتطوير المحلي ومنصة حاويات مُدارة مثل AWS App Runner أو Google Cloud Run أو Fly.io للإنتاج. هذه المنصات تمنحك نشر الحاويات، وHTTPS التلقائي، والتوسع دون أن تطلب منك إدارة مستوى تحكم Kubernetes. تحصل على معظم فوائد الحاويات مع لا شيء من منحنى تعلم Kubernetes.
أفضل استراتيجية بنية تحتية هي تلك التي تتيح لفريقك إنتاج الميزات. Docker و Kubernetes أدوات، وليست هويات. استخدمها عندما تساعد، وتجاوزها عندما لا تفعل.
