إذا عمل تطبيقك على جهازك ثم فشل عند زميلك أو على مخدم الإنتاج، فغالباً لا تكون المشكلة في النص البرمجي وحده، بل في اختلاف البيئة المحيطة به: إصدار Node مختلف، مكتبة ناقصة، أو إعداد لنظام التشغيل لا يشبه إعداد جهازك.
دوكر منصة ومجموعة أدوات تساعدك على بناء صور التطبيقات وتشغيل الحاويات وإدارتها، بحيث تنتقل البيئة المطلوبة مع التطبيق وتبقى متسقة عبر المنصات المتوافقة. لا يعني ذلك أن كل جهاز يصبح متطابقاً، فالنواة والمعمارية والبيئة المضيفة ما زالت عوامل مهمة.
لماذا يهمك؟
أشهر جملة في عالم البرمجة: ”لكنه يعمل على جهازي!“. يعود هذا الاختلاف غالباً إلى البيئة التي تحيط بالتطبيق، لا إلى التطبيق وحده. عندما تصف هذه البيئة داخل Dockerfile وتبني منها صورة، يصبح إعدادها جزءاً قابلاً للمراجعة والتكرار ويمكن مشاركته مع بقية الفريق عبر Git.
فبدلاً من إرسال دليل تثبيت طويل لزميلك، ترسل له ملفات المشروع وملف البناء، ثم يحصل على بيئة متسقة مع بيئتك ما دامت المنصة المضيفة والمعمارية مدعومتين.
كيف يعمل؟
ثلاثة مفاهيم أساسية تكفي لتكوين مدخل أولي:
- الصورة (Image): قالب جاهز للقراءة فقط يحتوي نظام الملفات والاعتماديات، مثل صورة
node:22-alpine. - الحاوية (Container): نسخة قابلة للتشغيل من صورة، وقد تكون قيد التشغيل أو متوقفة. تستطيع إنشاء حاويات متعددة من الصورة نفسها، وتكون لكل واحدة حالتها ونظام ملفاتها القابل للكتابة.
- ملف Dockerfile: وصفة نصية تشرح خطوة بخطوة كيف تُبنى الصورة.
الحاوية ليست جهازاً افتراضياً. الجهاز الافتراضي يشغل نظام تشغيل كاملاً فوق نظامك، أما الحاوية فتشارك نواة نظام التشغيل المضيف، ولذلك تكون عادة أخف وأسرع في البدء، لكن النتيجة الفعلية تعتمد على التطبيق والصورة والموارد المتاحة.
قابلية النقل وحدودها
يساعدك دوكر على نقل بيئة التطبيق بين المنصات المتوافقة، لكنه لا يلغي القيود المتعلقة بالمعمارية والنواة ونظام التشغيل الأساسي.
- معمارية المعالج: صورة مبنية لمعمارية
linux/arm64لا تعمل أصلياً علىamd64. المحاكاة ليست الحل الوحيد، إذ تستطيع بناء الصورة للمعمارية المطلوبة أو استخدام صورة متعددة المنصات. الصورة متعددة المنصات تجمع نسخاً مخصصة لمعمارية كل منصة، ويختار وقت التشغيل النسخة المناسبة للمضيف. - نواة النظام: تحتاج حاويات لينوكس إلى نواة لينوكس. على ماك تعمل حاويات لينوكس داخل آلة افتراضية تعمل بلينوكس، لأن ماك لا يوفر نواة لينوكس مباشرة. وعلى ويندوز يمكن تشغيل حاويات لينوكس عبر WSL 2 أو Hyper-V، ويمكن أيضاً تشغيل حاويات ويندوز في البيئات المدعومة عندما تستخدم صورة ونواة ويندوز مناسبة.
- نظام التشغيل الأساسي: الحاوية المبنية من صورة لينوكس لا تشغل تطبيقاً يتطلب واجهة ويندوز البرمجية (Windows API)، كما أن حاوية ويندوز تحتاج صورة ونواة ويندوز مناسبة، لأن الحاوية تشارك نواة المضيف ولا توفر نظام تشغيل مختلفاً كاملاً.
بمعنى آخر، يساعدك دوكر على تثبيت المكتبات والإعدادات والأدوات داخل بيئة قابلة لإعادة البناء عبر المنصات المتوافقة، لكنه لا يجعل العتاد وأنظمة التشغيل المختلفة متطابقة.
كيف يتحقق العزل داخل الحاوية؟
تبدو الحاوية وكأنها جهاز مستقل، لكنها في الحقيقة عملية عادية في نظام التشغيل تطبق عليها آليات من النواة لتقييد الرؤية والموارد. يعتمد العزل أساساً على آليتين:
- فضاءات الأسماء (Namespaces): تحدد ما تستطيع العملية رؤيته افتراضياً. ترى كل حاوية قائمة عملياتها ونطاق شبكتها واسم مضيفها، لكن خيارات التشغيل مثل
--pid=hostقد تغير هذا العزل. - مجموعات التحكم (cgroups): تتيح تحديد الموارد التي تستطيع العملية استهلاكها. وجود
cgroupsلا يعني أن حدود المعالج والذاكرة مضبوطة تلقائياً، بل يجب تعيينها فعلياً، مثل--cpusو--memory. وقد تغير خيارات التشغيل والصلاحيات، مثل--privileged، مستوى العزل.
تضاف إلى ذلك طبقات الصورة نفسها: نظام ملفات الحاوية مبني من طبقات للقراءة فقط، والكتابات الافتراضية تذهب إلى طبقة الكتابة الخاصة بالحاوية. أما وحدات التخزين (volumes) وعمليات الربط المباشر (bind mounts) وtmpfs فتذهب إلى وجهات خارج هذه الطبقة، لذلك تناسب البيانات التي تريد الاحتفاظ بها أو إدارتها خارج دورة حياة الحاوية.
العلاقة بالمحاكاة الافتراضية
الحاويات والأجهزة الافتراضية تستخدمان أسلوبين مختلفين للعزل:
- الجهاز الافتراضي: يشغل نظام تشغيل كاملاً بنواة خاصة به عبر مراقب الأجهزة الافتراضية. قد يقدم هذا المراقب أجهزة افتراضية كاملة أو يمرر بعض الموارد مباشرة، بحسب التقنية والإعداد. يوفر ذلك حداً إضافياً بين أنظمة التشغيل، لكنه يحتاج عادة إلى موارد ومساحة وزمن بدء أكبر من حاوية تشترك في نواة المضيف.
graph TD
subgraph VM["الجهاز الافتراضي: نظام تشغيل مستقل"]
APP1["تطبيق أ"] --> OS1["نظام تشغيل كامل<br/>(نواة خاصة)"]
APP2["تطبيق ب"] --> OS2["نظام تشغيل كامل<br/>(نواة خاصة)"]
OS1 --> HV["مراقب الأجهزة الافتراضية<br/>(موارد افتراضية)"]
OS2 --> HV
HV --> HW1["عتاد الجهاز"]
end
- الحاوية: تشغل عملية أو مجموعة عمليات معزولة تشترك في نواة النظام المضيف، من دون نواة خاصة بها. تكون عادة أصغر وأسرع في البدء، لكن مستوى العزل يعتمد على النواة وإعدادات التشغيل، ولذلك لا يصح تحويل هذه المقارنة إلى أرقام ثابتة لكل تطبيق.
graph TD
subgraph CONT["الحاويات: عزل بالنواة المشتركة"]
C1["حاوية 1<br/>(عملية مقيدة)"] --> NS["Namespaces + cgroups"]
C2["حاوية 2<br/>(عملية مقيدة)"] --> NS
NS --> K["نواة لينوكس المشتركة"]
K --> HW2["عتاد الجهاز"]
end
يحمل كل جهاز افتراضي نظام تشغيل كاملاً، بينما تعتمد الحاويات على آليات العزل التي توفرها النواة المشتركة. لذلك قد تكون الحاوية أقل استهلاكاً للموارد، في حين يضيف الجهاز الافتراضي حداً آخر عبر مراقب الأجهزة الافتراضية. لا يعني ذلك أن أحد الخيارين أفضل دائماً، فالاختيار يعتمد على درجة العزل المطلوبة وطبيعة التطبيق.
على ماك وويندوز، قد يكون مراقب الأجهزة الافتراضية جزءاً من الطريقة التي يوفر بها تطبيق دوكر لسطح المكتب (Docker Desktop) نواة لينوكس اللازمة لحاويات لينوكس. في هذه الحالة تعمل الحاوية داخل آلة افتراضية تعمل بلينوكس، حتى لو كانت أوامر Docker تُنفذ من نظام التشغيل المضيف.
محرك دوكر مقابل تطبيق دوكر لسطح المكتب
عندما تقول ”دوكر“ فأنت غالباً تعني أحد مكوّنين، والفرق بينهما مهم:
- محرك دوكر: المحرك مفتوح المصدر المرخص برخصة Apache 2.0، وهو المكوّن الذي يبني الصور ويشغّل الحاويات. يعمل مباشرة في بيئة لينوكس، ويستخدم عادة على مخدم لينوكس أو داخل منتج يوفّر بيئة لينوكس.
- تطبيق دوكر لسطح المكتب: تطبيق يضم محرك دوكر مع واجهة رسومية وتكاملات وأدوات مساعدة. وهو متاح على لينوكس أيضاً، إضافة إلى ويندوز وماك، وتختلف طريقة توفير البيئة المضيفة بحسب النظام والإعداد.
رخصة محرك دوكر لا تعني أن كل مكوّنات تطبيق دوكر لسطح المكتب مفتوحة المصدر أو مجانية لكل أنواع الاستخدام. بحسب الشروط الرسمية لتطبيق دوكر لسطح المكتب، يكون تطبيق دوكر لسطح المكتب مجانياً للاستخدام الشخصي والتعليم والمشروعات المفتوحة المصدر غير التجارية والشركات الصغيرة ضمن الحدود الرسمية، ومن أمثلتها أن يكون لدى الشركة أقل من 250 موظفاً وإيرادات سنوية أقل من 10 ملايين دولار، مع مراعاة بقية الشروط. ويكون تطبيق دوكر لسطح المكتب مدفوعاً للاستخدام المهني في المؤسسات الأكبر وللجهات الحكومية، لذلك لا يكفي تلخيص الأهلية بهذين الرقمين فقط، بل يجب الرجوع إلى التعريفات والشروط الرسمية.
يختلف Podman عن Colima في المستوى الذي يعمل فيه كل منهما. Podman محرك وأدوات لإدارة الحاويات من دون خدمة مركزية دائمة، ويمكن استخدامه بواجهة متوافقة مع أوامر Docker في حالات كثيرة. أما Colima فهو مدير لبيئة افتراضية تعمل بلينوكس محلياً، ويمكنه توفير بيئة تشغيل Docker أو containerd على جهازك، لذلك لا يمثل محرك حاويات من النوع نفسه.
متى تستخدمه ومتى تتجنبه؟
استخدم دوكر عندما:
- يعتمد تطبيقك على خدمات متعددة، مثل قاعدة بيانات وRedis ومخدم بريد، وتريد بيئة تطوير بأمر واحد.
- تريد بيئة نشر قابلة للتكرار على مخدم أو منصة متوافقة.
- تعمل ضمن فريق وتريد توحيد بيئات التطوير وتقليل اختلاف الإعدادات.
تجنبه عندما:
- يكون مشروعك موقعاً ثابتاً أو صفحة بسيطة، ولا يبرر التعقيد الإضافي الفائدة التي تحتاجها.
- تبني تطبيقاً بواجهة رسومية لسطح المكتب، إلا إذا كنت تعرف كيف ستربط الحاوية ببيئة العرض والعتاد المطلوبين.
مثال عملي
سنفترض أن لدينا تطبيق Node.js بسيطاً يستمع إلى المنفذ 3000 ويعيد نصاً قصيراً. سننشئ له الملفات الأساسية داخل المقال حتى تستطيع تشغيل المثال كما هو، مع التنبيه إلى أن التطبيقات الحقيقية قد تحتاج إلى إعدادات واعتماديات إضافية.
في هذا المثال، يجب أن يستمع التطبيق على 0.0.0.0 داخل الحاوية، لا على localhost فقط، حتى تصل إليه الاتصالات القادمة من منفذ Docker المنشور.
نبدأ بملف package.json بسيط، لأن npm ci يحتاج إلى ملف القفل والبيانات التي يصفها:
{
"name": "my-app",
"version": "1.0.0",
"private": true
}
ثم ننشئ package-lock.json متوافقاً معه:
{
"name": "my-app",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "my-app",
"version": "1.0.0"
}
}
}
أما server.js فيستمع على 0.0.0.0 كما يلي:
const http = require("node:http");
const port = Number(process.env.PORT || 3000);
const server = http.createServer((_request, response) => {
response.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" });
response.end("مرحبا من داخل Docker\n");
});
server.listen(port, "0.0.0.0", () => {
console.log(`Listening on ${port}`);
});
إذا لم يكن package-lock.json موجوداً أو لم يكن متوافقاً مع package.json، فسيفشل npm ci. أنشئ ملف القفل من المشروع نفسه، ثم راجعه واحتفظ به مع بقية الملفات.
نبدأ بملف .dockerignore صغير. الهدف هو عدم إرسال الملفات الكبيرة أو الحساسة إلى سياق البناء:
node_modules
.git
.env
dist
build
نكتب Dockerfile تدريجياً، ونشرح سبب كل جزء:
أولاً، نختار صورة Node.js كأساس حتى تتوفر بيئة التشغيل داخل الصورة:
FROM node:22-alpine
ثم نحدد مجلد العمل. يجعل ذلك المسارات التالية واضحة، ويمنع وضع ملفات التطبيق في جذر نظام الملفات:
WORKDIR /app
ننسخ ملفات تعريف الحزم قبل بقية النص البرمجي، لأن Docker يستطيع إعادة استخدام هذه الطبقة عندما لا تتغير الاعتماديات:
COPY package.json package-lock.json ./
بعد ذلك نثبت الاعتماديات من ملف القفل. يستخدم npm ci الحالة المقفلة بدل إعادة حل الإصدارات، وهو سبب حاجته إلى package-lock.json متوافق. نستخدم --omit=dev لأن الصورة هنا مخصصة للتشغيل:
RUN npm ci --omit=dev
الآن ننسخ بقية ملفات التطبيق، ومنها server.js:
COPY . .
يخبر EXPOSE قارئ الملف بأن التطبيق يتوقع المنفذ 3000، لكنه توثيق فقط، ولا ينشر المنفذ ولا ينشئ تحويلاً للشبكة. النشر يحدث لاحقاً عند استخدام -p مع docker run:
EXPOSE 3000
أخيراً، نحدد العملية الافتراضية التي تبدأ عند تشغيل الحاوية:
CMD ["node", "server.js"]
بعد جمع المقاطع، يكون Dockerfile الكامل كما يلي:
FROM node:22-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
نبني الصورة ثم نشغل الحاوية ونربط المنفذ 3000 في المضيف بالمنفذ نفسه داخل الحاوية:
docker build -t my-app:1.0 .
docker run --rm -d --name my-app -p 3000:3000 my-app:1.0
نتحقق من النتيجة باستخدام curl:
curl http://localhost:3000
المخرج المتوقع:
مرحبا من داخل Docker
بعد التحقق، أوقف الحاوية بالأمر docker stop my-app إذا كانت ما تزال تعمل.
مصطلحات ذات صلة
- ما هو API؟: الواجهة التي تتحدث بها خدماتك داخل الحاويات مع العالم.