الفصل 1: أساسيات Docker

قبل أن تكتب أول Dockerfile، افهم أولًا لماذا يوجد Docker وما المشكلة التي يحلّها. في هذا الفصل نتعرّف على الفرق بين الحاوية والجهاز الافتراضي، وعلى علاقة الـ Image بالحاوية، وكيف يعمل Docker من الداخل.

نشرح كل فكرة بشكل عام أولًا، ثم نطبّقها على مثال حقيقي — تطبيق Spring Boot — كلما احتجنا أن نراها تعمل.

⏱️ زمن القراءة: — 📊 المستوى: مبتدئ 🧪 معمل تطبيقي واحد

أهداف التعلّم

بنهاية هذا الفصل ستعرف كيف:

  • تشرح مشكلة «يعمل على جهازي» ولماذا يحلّها Docker.
  • تفرّق بين الجهاز الافتراضي (VM) والحاوية (Container).
  • تصف العلاقة بين الـ Image والحاوية.
  • تعرف مكوّنات Docker الثلاثة: Client و Daemon و Registry.
  • تتابع مراحل دورة حياة الحاوية.
  • تربط هذه المفاهيم بتطبيق واقعي (Spring Boot).

المتطلبات المسبقة

هذا الفصل نظري بالكامل. لا تحتاج إلى تثبيت Docker بعد؛ سنفعل ذلك في الفصل التالي. المفاهيم عامة وتنطبق على أي تطبيق. بعض الأمثلة تستخدم Java و Spring Boot، فمعرفتهما تفيد لكنها ليست شرطًا.

💡 ملاحظة

الأوامر التي تظهر في قسمَي «مثال» و«عرض عملي» هي معاينة فقط لتكوين تصوّر ذهني — ستنفّذها بنفسك في الفصل 2 بعد تثبيت Docker.

لماذا Docker؟ مشكلة «يعمل على جهازي»

تخيّل أنك أنهيت تطوير تطبيقك على جهازك. يعمل بشكل ممتاز. ترسله إلى زميلك أو إلى الخادم، فينهار فورًا. لماذا؟

لأن أي تطبيق لا يعمل في فراغ، بل يعتمد على بيئة كاملة حوله:

  • إصدار محدّد من بيئة التشغيل (runtime) والمكتبات.
  • متغيّرات بيئة (environment variables) وإعدادات.
  • خدمات خارجية مثل قاعدة بيانات بإصدار معيّن.
  • منافذ (ports) ومكتبات نظام تشغيل.

أي اختلاف بسيط في هذه البيئة بين جهازك والخادم قد يكسر التطبيق. هذه هي مشكلة «works on my machine» الشهيرة.

مثال ملموس: تطبيق Java يعتمد على JRE بإصدار Java 21 على جهازك، بينما الخادم عليه Java 17 فقط — فيفشل التشغيل.

ما الذي يحلّه Docker؟

فكرة Docker بسيطة. بدل نقل التطبيق وحده، نحزمه مع بيئته كاملة في وحدة واحدة اسمها Image.

هذه الوحدة تعمل بنفس الطريقة على أي جهاز فيه Docker: جهازك، جهاز زميلك، أو الخادم. تختفي الاختلافات، وتختفي معها المشكلة.

✅ الفكرة الجوهرية

Docker لا يجعل تطبيقك أسرع، بل يجعله يعمل بنفس الطريقة في كل مكان. القيمة هي التطابق (consistency) بين التطوير والإنتاج.

الأجهزة الافتراضية (Virtual Machines)

قبل الحاويات، كان العزل يتم بالجهاز الافتراضي. الـ VM يحاكي جهازًا كاملًا: لكل تطبيق نظام تشغيل ضيف (Guest OS) مستقل يعمل فوق طبقة اسمها Hypervisor.

هذا يوفّر عزلًا قويًا، لكن بثمن:

  • كل VM يحمل نظام تشغيل كامل بحجم جيجابايتات.
  • الإقلاع يستغرق دقائق.
  • استهلاك عالٍ للذاكرة والمعالج لأن كل نظام ضيف يعمل بالكامل.

الحاويات (Containers)

الحاوية تحلّ نفس المشكلة بطريقة أخف. بدل محاكاة جهاز كامل، تتشارك كل الحاويات نواة نظام التشغيل المضيف (host kernel)، وتعمل كعمليات (processes) معزولة فوقها.

النتيجة:

  • الحجم بـ ميجابايتات لا جيجابايتات.
  • الإقلاع خلال ثوانٍ أو أقل.
  • استهلاك أقل بكثير — يمكن تشغيل عشرات الحاويات على نفس الجهاز.

❗ مهم جدًّا

الحاوية ليست جهازًا افتراضيًا صغيرًا، بل هي عملية معزولة. لا تحتوي على نظام تشغيل كامل، بل على ملفات وبيئة التطبيق فقط، وتستعير النواة من المضيف.

مقارنة سريعة: VM مقابل Container

المعيار Virtual Machine Container
العزلنظام تشغيل ضيف كاملعملية معزولة على نواة المضيف
الحجمجيجابايتات (GB)ميجابايتات (MB)
زمن الإقلاعدقائقثوانٍ
الاستهلاكعالٍمنخفض
الكثافة على الجهازعدد محدودعشرات أو أكثر

الـ Images والحاويات (Images & Containers)

هذان المصطلحان هما جوهر Docker، والخلط بينهما شائع. الفرق بسيط:

  • Image: قالب ثابت للقراءة فقط (read-only) يحتوي على تطبيقك وكل ما يحتاجه ليعمل. لا يعمل بذاته — إنه مجرد «مخطّط».
  • Container: نسخة حيّة قابلة للتشغيل من الـ Image. من الـ Image الواحدة يمكنك تشغيل عدد لا محدود من الحاويات.

✅ بلغة Java

إن كنت تعرف Java فالتشبيه مباشر: الـ Image كالـ Class، والحاوية كالـ Object (نسخة من الكلاس). كما تنشئ عدة كائنات من كلاس واحد، تنشئ عدة حاويات من Image واحدة.

تشبيه من الواقع

سُمّي Docker على اسم عمّال الموانئ (dockers) الذين يشحنون الحاويات، والتشبيه مقصود:

✅ حاوية الشحن

قبل حاويات الشحن القياسية، كان كل نوع بضاعة يُشحن بطريقته الخاصة — فوضى. جاءت الحاوية الموحّدة: صندوق واحد بمقاس ثابت يمكن أن ترفعه أي رافعة وتنقله أي سفينة أو شاحنة، بغضّ النظر عمّا بداخله.

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

مخطّطات توضيحية

البنية: VM مقابل Container

flowchart TB
    subgraph VM["نموذج الأجهزة الافتراضية"]
        direction TB
        H1["Hardware"] --> HV["Hypervisor"]
        HV --> G1["Guest OS + App A"]
        HV --> G2["Guest OS + App B"]
    end
    subgraph CT["نموذج الحاويات"]
        direction TB
        H2["Hardware"] --> OS["Host OS + Kernel"]
        OS --> DE["Docker Engine"]
        DE --> C1["Container A"]
        DE --> C2["Container B"]
        DE --> C3["Container C"]
    end
              
الـ VM يكرّر نظام التشغيل لكل تطبيق، بينما تتشارك الحاويات نواة المضيف عبر Docker Engine.

معمارية Docker

يعمل Docker بنموذج Client / Server. عندما تكتب أمرًا مثل docker run، فأنت تستخدم الـ Client الذي يرسل الطلب إلى الـ Docker Daemon (الخادم)، وهو من يبني الـ Images ويشغّل الحاويات. أما الـ Images الجاهزة فتأتي من Registry مثل Docker Hub.

flowchart LR
    U["أنت"] -->|"docker run / build / pull"| CLI["Docker Client (CLI)"]
    CLI -->|"REST API"| D["Docker Daemon (dockerd)"]
    D -->|"يدير"| IMG["Images"]
    D -->|"يشغّل"| CON["Containers"]
    D <-->|"pull / push"| REG["Registry — Docker Hub"]
              
الـ Client يرسل الأوامر، والـ Daemon ينفّذها، والـ Registry مصدر الـ Images ووجهتها.

دورة حياة الحاوية

تمرّ الحاوية بحالات واضحة يتحكّم بها أمر واحد في كل انتقال:

stateDiagram-v2
    [*] --> Created: docker create
    Created --> Running: docker start
    Running --> Paused: docker pause
    Paused --> Running: docker unpause
    Running --> Stopped: docker stop
    Stopped --> Running: docker start
    Stopped --> Removed: docker rm
    Removed --> [*]
              
الأمر docker run يختصر الطريق: ينشئ الحاوية ويشغّلها في خطوة واحدة.

مثال: المشكلة بشكل ملموس

لنجعل مشكلة «يعمل على جهازي» ملموسة. تطبيق Spring Boot الخاص بك يتطلّب Java 21، لكن الخادم عليه Java 17 فقط. عند التشغيل:

$ java -jar tasks-service.jar
Error: LinkageError occurred while loading main class
  UnsupportedClassVersionError: class file version 65.0,
  this version only recognizes up to 61.0

مع Docker، نثبّت إصدار Java داخل الـ Image نفسها، فلا يهمّ ما المثبّت على الخادم:

# The image carries Java 21 with it, wherever it runs
FROM eclipse-temurin:21-jre
COPY tasks-service.jar app.jar
ENTRYPOINT ["java", "-jar", "app.jar"]

الآن تعمل الحاوية بـ Java 21 على أي خادم، حتى لو كان لا يحوي Java إطلاقًا.

عرض عملي (معاينة)

💡 معاينة فقط

لن تنفّذ هذه الأوامر الآن — Docker غير مثبّت بعد. الهدف أن ترى الشكل النهائي الذي نتّجه إليه. ستشغّلها بنفسك في الفصل 2.

أشهر أمر تشغيل أول حاوية للتأكّد من أن Docker يعمل هو hello-world:

docker run hello-world

ماذا يحدث خلف الكواليس عند تنفيذ هذا الأمر؟

  1. الـ Client يرسل الطلب إلى الـ Daemon.
  2. الـ Daemon لا يجد الـ Image محليًّا، فيسحبها (pull) من Docker Hub.
  3. ينشئ حاوية من الـ Image ويشغّلها.
  4. تطبع الحاوية رسالة ترحيب ثم تتوقّف.
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
Hello from Docker!
This message shows that your installation appears to be working correctly.

المعمل التطبيقي

كلما احتجنا إلى تطبيق حقيقي عبر الدليل، سنستعين بمثال متكرّر واحد: خدمة المهام (Tasks Service) — واجهة REST بسيطة بـ Spring Boot تحفظ المهام في قاعدة PostgreSQL. تبقى مفاهيم Docker عامة، ونستخدم هذا المثال فقط لرؤيتها مطبَّقة على شيء ملموس.

🧪 معمل 1: جرد بيئة التشغيل

لن نكتب كودًا بعد. مهمتك أن تحدّد ما الذي يجعل تطبيقك يعمل، لأن هذا بالضبط ما سنحزمه لاحقًا داخل الـ Image.

لأي تطبيق لديك (أو خدمة المهام كمثال)، اكتب في ورقة:

  1. إصدار Java المطلوب (JRE).
  2. المنفذ الذي يستمع عليه التطبيق (غالبًا 8080).
  3. متغيّرات البيئة والإعدادات (رابط قاعدة البيانات، كلمة المرور، الـ profile).
  4. الخدمات الخارجية والـ dependencies (PostgreSQL وإصداره).

المخرجات: قائمة الجرد هذه هي «مواصفات» الـ Image المستقبلية الخاصة بك — ستتحوّل مباشرةً إلى تعليمات Dockerfile في الفصل 5، وإلى خدمات في compose.yaml في الفصل 8.

أخطاء شائعة

⚠️ الحاوية جهاز افتراضي صغير

خطأ شائع. الحاوية عملية معزولة تتشارك نواة المضيف، وليست نظام تشغيل كامل. لذلك هي أخف وأسرع، ولذلك أيضًا لا تُشغّل نظامًا مختلفًا كليًّا عن نواة المضيف.

⚠️ الخلط بين Image و Container

الـ Image قالب ثابت، والحاوية تشغيل حيّ له. تعديل حاوية لا يغيّر الـ Image التي بُنيت منها.

⚠️ ظنّ أن Docker يُلغي بيئة التشغيل (runtime)

Docker لا يستبدل الـ runtime الخاص بتطبيقك، بل يحزمه معه. مثلًا تطبيق Java ما زال يحتاج JRE — لكن الآن الـ JRE موجود داخل الـ Image بإصدار نعرفه ونتحكّم به.

أفضل الممارسات

📦 عملية واحدة لكل حاوية

اجعل كل حاوية مسؤولة عن شيء واحد: خدمة Spring Boot في حاوية، وقاعدة البيانات في أخرى.

🧊 الـ Image قطعة أثرية ثابتة

عامل الـ Image كـ artifact غير قابل للتغيير (immutable): تُبنى مرة، وتُشغَّل في كل البيئات كما هي.

♻️ الحاويات قابلة للاستبدال

اعتبر الحاوية سريعة الزوال (ephemeral): يمكن حذفها وإعادة إنشائها في أي لحظة دون خوف.

ورقة المصطلحات (Cheat Sheet)

المصطلح المعنى المختصر
Imageقالب ثابت للقراءة فقط يحوي التطبيق وبيئته
Containerنسخة حيّة قابلة للتشغيل من Image
Docker Clientواجهة الأوامر التي تكتبها (docker ...)
Docker Daemonالخادم (dockerd) الذي يبني ويشغّل
Registryمخزن الـ Images (Docker Hub مثلًا)
Dockerfileوصفة نصية لبناء Image
Docker EngineClient + Daemon معًا على الجهاز

الخلاصة

أهم الأفكار

  • Docker يحلّ مشكلة «يعمل على جهازي» بتحزيم التطبيق مع بيئته.
  • الحاوية عملية معزولة تتشارك نواة المضيف — أخف وأسرع من الـ VM.
  • الـ Image قالب ثابت، والحاوية تشغيل حيّ له (Class مقابل Object).
  • معمارية Docker = Client + Daemon + Registry.

مصطلحات تذكّرها

  • Image / Container / Registry
  • Docker Client / Docker Daemon

أخطاء تجنّبها

  • اعتبار الحاوية جهازًا افتراضيًّا صغيرًا.
  • الخلط بين الـ Image والحاوية.
  • ظنّ أن Docker يُلغي الـ JVM.