Docker للمبتدئين: حوّل مشروع الميرن ستاك لـ Containers

“شغال عندي بس مش عندك”، أشهر جملة هتسمعها في أي فريق برمجة، وأكتر حاجة بتضيع وقت. عندك دلوقتي مشروع ميرن ستاك كامل: باك إند بـ Express متصل بـ MongoDB، فرونت إند بـ React، وحماية بـ JWT. لو حد تاني عايز يشغله على جهازه، لازم يثبت نفس نسخة Node، ونفس المتغيرات، وميحصلش تعارض مع مشروع تاني شغال على نفس البورت. Docker بيحل المشكلة دي تمامًا: بيحط مشروعك جوه صندوق معزول، شغال بنفس الطريقة بالظبط على أي جهاز، بتاعك أو بتاع حد تاني أو حتى على سيرفر حقيقي.

ده أول مقال في سلسلة جديدة اسمها “من الكود للإنتاج”: بعد ما بنينا مشروع الميرن ستاك كامل وحميناه بالـ Authentication، دلوقتي وقت ناخده خطوة أقرب للعالم الحقيقي. Docker هو حجر الأساس، وعليه هنبني بعد كده النشر الفعلي والـ CI/CD.

المحتويات

أولاً: Docker إيه وليه مش مجرد رفاهية

Docker بيخليك تكتب “وصفة” (اسمها Dockerfile) فيها كل حاجة مشروعك محتاجها عشان يشتغل: نسخة Node، المكتبات، الأوامر اللي بتشغله. أي حد عنده Docker يقدر ياخد الوصفة دي ويبني منها “صورة” (image)، وبعدين يشغل منها “حاوية” (container) معزولة تمامًا عن باقي النظام. الفرق عن الطريقة التقليدية (ملف README فيه تعليمات التثبيت) إن الوصفة هنا كود فعلي بيتنفذ تلقائيًا، مش خطوات المفروض حد يتبعها يدويًا وممكن ينسى واحدة منها.

عندنا في مشروع الميرن ستاك بتاعنا جزئين مختلفين تمامًا: باك إند Node.js بيشتغل باستمرار كسيرفر، وفرونت إند React لازم يتحول لملفات ثابتة (build) قبل ما يتقدم لحد. عشان كده هنعمل Dockerfile منفصل لكل واحد فيهم.

سؤال بيتسأل كتير: Docker نفس فكرة الـ Virtual Machine؟ الإجابة لأ. الـ Virtual Machine بتحاكي جهاز كمبيوتر كامل بنظام تشغيل منفصل بالكامل، وده بياخد وقت طويل يبدأ وحجمه بالجيجابايت. الحاوية في Docker بتشارك نواة نظام التشغيل بتاع الجهاز المضيف، وبتحمل بس المكتبات اللي مشروعك محتاجها فعلاً، فبتبدأ في ثواني وحجمها ميجابايتات مش جيجابايتات. ده الفرق اللي بيخلي Docker عملي جدًا لمشاريع زي بتاعتنا، مش بس فكرة نظرية.

ثانياً: تثبيت Docker والتأكد إنه شغال

نزّل Docker Desktop (متاح لـ Windows وmacOS ولينكس) من الموقع الرسمي، وبعد التثبيت افتح Terminal واتأكد إنه شغال.

# لازم يرجعلك رقم نسخة من غير أي خطأ
docker --version

# لازم كمان يكون عندك compose جاهز
docker compose version

# تجربة سريعة إن كل حاجة شغالة تمام
docker run hello-world

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

هينت سريعة: لو سمعت عن Podman وحابب تستخدمه بدل Docker، معظم الأوامر والـ Dockerfiles في المقال ده هتشتغل عنده زي ما هي تقريبًا من غير تعديل، لأنه متوافق مع نفس الصيغة القياسية. أهم فرق إن Podman بيشتغل من غير daemon مركزي دايمًا شغال في الخلفية زي Docker، وده بيخليه اختيار مفضل عند ناس كتير لأسباب أمان. المقال هيكمل بـ Docker لأنه لسه الأشهر والأكتر انتشارًا في تعليمات النشر والدروس، بس الفكرة والكود هيفضلوا نفسهم تقريبًا لو قررت تجرب Podman بعدين.

ثالثاً: Dockerfile للباك إند (Express)

جوه مجلد الباك إند اللي بنيناه في مقال Node.js وExpress.js، اعمل ملف اسمه Dockerfile (من غير امتداد).

FROM node:24-alpine

WORKDIR /app

COPY package*.json ./
RUN npm ci --omit=dev

COPY . .

ENV NODE_ENV=production
EXPOSE 3000

# تشغيل بمستخدم عادي مش root، خطوة أمان مهمة
USER node

CMD ["node", "server.js"]

كل سطر هنا خطوة بتتنفذ بالترتيب. FROM بيحدد الصورة الأساسية، وهنا اخترنا Node نسخة 24 لأنها الـ Active LTS الحالية (نسخة 20 وصلت لنهاية دعمها في أبريل 2026 وبقت من غير تحديثات أمنية، فمتستخدمهاش في مشروع جديد). وalpine ده نظام لينكس خفيف جدًا بيقلل حجم الصورة لأقصى درجة.

WORKDIR بيحدد مجلد العمل جوه الحاوية. COPY package*.json بينسخ ملفات المكتبات بس الأول (مش كل الكود) عشان خطوة تثبيت المكتبات تتخزن في طبقة منفصلة ومتتعادش كل مرة لو غيرت في الكود من غير ما تغير المكتبات، وده بيسرع البناء بشكل كبير. استخدمنا npm ci مش npm install لأنه بيثبت النسخ المحددة بالظبط في package-lock.json من غير أي اجتهاد، وده اللي إحنا عايزينه في الحاوية. و–omit=dev بيستبعد مكتبات التطوير اللي مش محتاجينها وقت التشغيل.

نقطة مهمة جدًا وبتوقع ناس كتير: اتأكد إن السيرفر بتاعك بيسمع على 0.0.0.0 مش 127.0.0.1، يعني app.listen(3000) عادي تمام، بس لو كاتب app.listen(3000, ‘127.0.0.1’) الحاوية هتشتغل ومش هتلاقي أي خطأ، ومع ذلك مش هتقدر توصله من برة خالص.

ضيف كمان ملف .dockerignore بجانبه عشان متنسخش حاجات مش محتاجها جوه الحاوية.

node_modules
npm-debug.log
.env
.env.*
.git
.gitignore
Dockerfile
.dockerignore
build
dist
coverage

لاحظ إننا حطينا .env جوه .dockerignore، لأن متغيرات الاتصال بـ MongoDB Atlas والـ JWT_SECRET اللي اتكلمنا عنهم في مقال الـ Authentication مش المفروض يتخزنوا جوه الصورة نفسها، هنمررهم وقت التشغيل بدل كده. ونفس الملف ده تقريبًا هتحطه في مجلد الفرونت إند كمان.

رابعاً: Dockerfile للفرونت إند (React)

الفرونت إند مختلف: مش محتاج يفضل Node شغال طول الوقت، محتاج بس يتحول لملفات HTML/CSS/JS ثابتة وبعدين يتقدم عن طريق سيرفر ويب بسيط. هنستخدم أسلوب اسمه Multi-stage Build: مرحلة أولى بتبني المشروع، ومرحلة تانية بس بتاخد الناتج النهائي.

# المرحلة الأولى: بناء المشروع
FROM node:24-alpine AS build

WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# المرحلة التانية: تقديم الملفات الثابتة بس
FROM nginx:alpine

# لو المشروع بـ Vite غيّر dist لـ build حسب مشروعك
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

خد بالك من سطر الـ COPY –from=build ده تحديدًا، لأنه أكتر سطر بيفشل عند الناس. مجلد الناتج بيختلف حسب أداة البناء: Vite بيطلع الملفات في dist، وCreate React App القديم بيطلعها في build. افتح مجلد مشروعك بعد ما تشغل أمر البناء محليًا وشوف الاسم الصح عندك قبل ما تكمل.

الفايدة الكبيرة من الأسلوب ده: الصورة النهائية معندهاش Node ولا node_modules خالص، بس nginx (سيرفر ويب خفيف جدًا) والملفات الجاهزة. الحجم بيفرق بشكل كبير، وده بيسرع النشر بعدين.

خامساً: إعداد nginx، الخطوة اللي بينساها الكل

لو وقفت عند الخطوة اللي فاتت وشغلت المشروع، هتلاقي حاجتين مكسورين. الأولى: أي صفحة غير الرئيسية هترجعلك 404 لما تعمل refresh، لأن React Router بيتعامل مع المسارات في المتصفح، لكن nginx بيدور على ملف حقيقي بالاسم ده ومش لاقيه. التانية: الفرونت إند مش عارف يوصل للباك إند. الملف ده بيحل الاتنين.

اعمل ملف اسمه nginx.conf جوه مجلد الفرونت إند، جنب الـ Dockerfile بالظبط.

server {
    listen 80;

    # أي طلب على api/ بيتحول للباك إند جوه شبكة Docker
    location /api/ {
        proxy_pass http://backend:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }

    # أي مسار تاني يرجع index.html عشان React Router يشتغل
    location / {
        root /usr/share/nginx/html;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
}

سطر try_files ده هو اللي بيمنع مشكلة الـ 404 عند الـ refresh. أما proxy_pass فبيحل مشكلة أكبر من كده بكتير: بدل ما تحط عنوان الباك إند في كود الفرونت إند، خليه ينادي على /api/… بس، وnginx هو اللي يوصّل الطلب لسيرفس backend باسمه جوه شبكة Docker الداخلية.

الإعداد ده كفاية عشان تشغل المشروع دلوقتي، بس nginx نفسه عنده إمكانيات أكبر من كده بكتير: إعدادات الـ caching، وضبط الـ headers الأمنية، وعمل HTTPS. مش هندخل في التفاصيل دي دلوقتي، لأن ده بالظبط موضوع المقال الجاي في السلسلة لما نتكلم عن النشر الفعلي على سيرفر حقيقي.

ليه ده مهم؟ لأن متغيرات البيئة في React بتتحرق جوه الملفات وقت البناء مش وقت التشغيل. يعني لو حطيت API_URL في docker compose تحت environment وفكرت إنه هيتغير، هتكتشف إنه مش بيتغير خالص لأن الملفات اتبنت خلاص. الطريقة اللي فوق بتخليك مش محتاج المتغير ده من أصله.

عشان الإعداد ده يشتغل، لازم ترجع لكود الفرونت إند اللي بنيناه في مقال React وتشيل أي مكان مكتوب فيه http://localhost:3000 صريح جوه طلبات الـ fetch أو axios، وتستبدله بمسار نسبي بس زي /api/auth/login. من غير التعديل ده، الفرونت إند هيفضل بيحاول يكلم localhost:3000 بتاع جهازك مش سيرفس backend جوه الحاوية، وهتفضل تشوف أخطاء اتصال من غير ما تعرف السبب.

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

سادساً: تجميع كل حاجة بـ Docker Compose

دلوقتي عندنا صورتين منفصلتين، محتاجين نشغلهم مع بعض بأمر واحد. هنستخدم Docker Compose، بنعمل ملف docker-compose.yml في جذر المشروع (فوق مجلدي الباك إند والفرونت إند).

services:
  backend:
    build: ./backend
    # البورت ده للتجربة المباشرة بس، تقدر تشيله وقت النشر
    ports:
      - "3000:3000"
    env_file:
      - .env
    restart: unless-stopped

  frontend:
    build: ./frontend
    ports:
      - "80:80"
    depends_on:
      - backend
    restart: unless-stopped

استخدمنا env_file بدل ما نكتب كل متغير بإيده، وده بيقرا ملف .env الموجود جنب docker-compose.yml ويمرر اللي جواه كله للحاوية. المتغيرات دي هي نفسها اللي جهزناها في مقالي MongoDB Atlas وJWT Authentication، يعني MONGODB_URI وJWT_SECRET. ومتنساش تضيف الملف ده لـ .gitignore عشان ميترفعش على GitHub.

دلوقتي تقدر تشغل المشروع كامل بأمر واحد بس.

docker compose up --build

سطر واحد، وهيبني الصورتين ويشغلهم مع بعض ويربط بينهم على شبكة واحدة. جرب دلوقتي تفتح http://localhost، هتلاقي الواجهة شغالة ومتصلة بالباك إند، بالظبط زي لما كانوا شغالين منفصلين قبل كده.

كمان محتاج تعرف كذا أمر هتستخدمهم يوميًا وانت بتشتغل بـ Docker.

# شوف كل الحاويات الشغالة دلوقتي
docker compose ps

# تابع الـ logs بتاعة سيرفس معين لحظة بلحظة
docker compose logs -f backend

# ادخل جوه حاوية شغالة عشان تفحصها (مفيد جدًا وقت التصحيح)
docker compose exec backend sh

# وقف كل حاجة
docker compose down

خلي بالك إننا بنستخدم docker compose exec مش docker exec، لأن compose بيسمي الحاوية باسم مركب زي myproject-backend-1، فلو كتبت docker exec -it backend sh هيقولك إن الحاوية دي مش موجودة. مع compose exec بتكتب اسم السيرفس زي ما هو في الملف وخلاص.

وdocker compose logs هيبقى صديقك الأول لما حاجة متشتغلش زي ما توقعت، بيوريك بالظبط إيه اللي حصل جوه الحاوية من غير ما تحتاج تدخلها.

سابعاً: أخطاء شائعة هتقابلك

  • نسيان .env جوه .dockerignore، وده بيخلي بيانات حساسة زي كلمة السر تتخزن جوه الصورة نفسها لأي حد يقدر يفتحها.
  • نسخ مجلد الناتج بالاسم الغلط في COPY –from=build. لو نسخت build والمشروع بـ Vite (أو العكس)، البناء هيفشل برسالة إن المسار مش موجود.
  • استخدام COPY . . قبل تثبيت المكتبات بدل بعده، وده بيبطل فايدة الطبقات (layer caching) وبيخلي كل بناء ياخد وقت طويل من غير داعي.
  • توقع إن متغيرات البيئة بتاعة الفرونت إند تتغير وقت التشغيل. زي ما قلنا، هي بتتحرق وقت البناء، فلو غيرتها لازم تعيد البناء.
  • سيرفر الباك إند بيسمع على 127.0.0.1 جوه الحاوية، فالحاوية تشتغل عادي والبورت متوصل، ومع ذلك مفيش أي رد.
  • محاولة الاتصال بقاعدة بيانات محلية (localhost) من جوه الحاوية. الحاوية معزولة عن جهازك، فلو بتستخدم MongoDB Atlas السحابية (زي ما عملنا) المشكلة دي مش هتقابلك أصلاً.
  • استخدام نسخة Node وصلت لنهاية دعمها، أو استخدام node:24 العادية بدل node:24-alpine، والفرق في الحجم كبير جدًا (مئات الميجابايت زيادة) من غير أي فايدة حقيقية في معظم المشاريع.

ثامناً: خطوة قبل النشر الحقيقي

دلوقتي عندك مشروع كامل معزول في صورتين، جاهز ينتقل لأي مكان من غير ما تقلق على “هيشتغل ولا لأ”. الصورتين دول نفسهم اللي هتستخدمهم في الخطوة الجاية: نشرهم فعليًا على سيرفرات حقيقية عشان أي حد على الإنترنت يقدر يدخل على التطبيق، مش بس إنت وجهازك.

لحد ما نوصل لموضوع النشر، جرب تشغل المشروع بالطريقة دي كذا مرة، واتأكد إنك فاهم كل سطر في الـ Dockerfiles بدل ما تنسخهم وخلاص. أسهل اختبار تعمله لنفسك: احذف مجلد node_modules من الجهاز خالص وشغل docker compose up –build تاني، لو المشروع اشتغل عادي يبقى الحاويات فعلاً مستقلة. لو عندك سؤال أو مشكلة قابلتك، قولّي في التعليقات.


اكتشاف المزيد من كود التطور

اشترك للحصول على أحدث التدوينات المرسلة إلى بريدك الإلكتروني.

اترك رد

Scroll to Top