Logo

المعمل 09: Docker المتقدم - التحسين، الأحجام (Volumes)، Compose، ومتغيرات البيئة

18 دقيقة قراءة
شرائح الدرس
1 / 13

المعمل 09: Docker متقدم - التحسين، والمجلدات التخزينية، وCompose، ومتغيرات البيئة

عمليات بناء أسرع، وHot Reload عبر الربط المباشر (bind mounts)، وCompose متعدد الحاويات، ومتغيرات البيئة

نظرة عامة

يغطي هذا المعمل مفاهيم Docker المتقدمة موضّحة عبر تطبيق Node.js/Express. تشمل المواضيع: تحسين ملف Dockerfile، وإعادة التحميل الفوري (Hot Reload) أثناء التطوير باستخدام ربط الأحجام (Volume Bind Mounts)، وأحجام Docker (Docker Volumes) (ربط المسارات مقابل الأحجام المجهولة)، وDocker Compose لتنسيق الحاويات المتعددة، وإدارة متغيرات البيئة.

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

بنهاية هذا المعمل ستكون قادرًا على:

  • تحسين صور Docker باستخدام .dockerignore وتقنيات تخزين الطبقات المؤقت
  • استخدام ربط المسارات (Bind Mounts) لتفعيل إعادة التحميل الفوري أثناء التطوير
  • التمييز بين ربط المسارات (Bind Mounts) والأحجام المجهولة (Anonymous Volumes) ومعرفة متى تُستخدم كل منها
  • تعريف تطبيقات متعددة الحاويات باستخدام Docker Compose
  • إدارة متغيرات البيئة في حاويات Docker وملفات Compose
  • استخدام docker exec لفتح طرفيات تفاعلية داخل الحاويات قيد التشغيل

المفاهيم الأساسية

تحسين الصورة (Image Optimization)

ملف .dockerignore

تمامًا كما يخبر .gitignore نظام Git بالملفات التي يجب تجاهلها، يخبر .dockerignore نظام Docker بالملفات التي يجب استبعادها من سياق البناء (Build Context). هذا يقلل حجم ما يُرسَل إلى خدمة Docker (Daemon) ويمنع تسرب الملفات الحساسة إلى الصور.

الملفات الشائع استبعادها ولماذا:

الملف / المجلدسبب الاستبعاد
node_modules/يُعاد تثبيته داخل الحاوية بواسطة npm install. نسخه يهدر الوقت وقد يتضمن ملفات ثنائية (Binaries) لمنصة خاطئة.
Dockerfileيُستخدَم بالفعل لإنشاء الحاوية - لا داعي لوجوده بداخلها.
.envيحتوي على أسرار يجب ألا تُخبَز داخل الصورة.
.git/بيانات وصفية لنظام التحكم بالإصدارات - غير مطلوبة وقت التشغيل.
*.logملفات السجل مؤقتة وكبيرة الحجم.

مثال على .dockerignore:

node_modules
Dockerfile
.dockerignore
.env
.git
*.log
dist
build
coverage
.DS_Store

بدون .dockerignore، تنسخ تعليمة COPY . . في ملف Dockerfile الخاص بك مجلد node_modules بالكامل (قد يصل إلى مئات الميجابايت) داخل الصورة - رغم أن npm install سيعيد إنشاءه على أي حال.

تخزين الطبقات المؤقت (Layer Caching)

يبني Docker الصور في طبقات. كل تعليمة في Dockerfile تُنشئ طبقة جديدة. يخزّن Docker الطبقات مؤقتًا (Cache) ويعيد استخدامها إذا لم تتغير التعليمة وكل الطبقات السابقة لها.

الاستراتيجية: انسخ ملفات التبعيات قبل كود المصدر.

# Bad: copying everything first invalidates cache on every code change
FROM node:20-alpine
WORKDIR /app
COPY . .                  # ← any file change busts ALL subsequent layers
RUN npm install
CMD ["node", "server.js"]
# Good: copy package files first so npm install is cached
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./   # ← only changes when deps change
RUN npm install                           # ← cached unless deps change
COPY . .                                  # ← source code changes don't bust npm install
CMD ["node", "server.js"]

قواعد تخزين الطبقات المؤقت:

  • تُخزَّن الطبقات مؤقتًا من الأعلى إلى الأسفل
  • بمجرد إبطال طبقة واحدة، يُعاد بناء كل الطبقات الموجودة أسفلها
  • ضع الخطوات البطيئة، نادرة التغيير (مثل RUN npm install) بالقرب من الأعلى
  • ضع الخطوات متكررة التغيير (مثل COPY . .) بالقرب من الأسفل
flowchart TD
    A["FROM node:20-alpine\n(نادرًا ما تتغير)"] --> B["WORKDIR /app\n(لا تتغير أبدًا)"]
    B --> C["COPY package.json\n(تتغير عند تغيّر التبعيات)"]
    C --> D["RUN npm install\n(مخزّنة مؤقتًا إذا لم تتغير C)"]
    D --> E["COPY . .\n(تتغير كثيرًا)"]
    E --> F["CMD\n(نادرًا ما تتغير)"]
 
    style D fill:#90EE90,stroke:#333
    style C fill:#FFD700,stroke:#333
    style E fill:#FF7F7F,stroke:#333

أحجام Docker (Volumes) وإعادة التحميل الفوري (Hot Reload)

مشكلة الحاويات أثناء التطوير

عندما تشغّل حاوية من صورة، تحصل الحاوية على لقطة (Snapshot) من كودك في وقت البناء. إذا حرّرت ملفًا على جهازك المضيف، لن تلاحظ الحاوية التغيير - سيتوجب عليك إعادة بناء الصورة في كل مرة. هذا غير عملي أثناء التطوير النشط.

فحص حاوية قيد التشغيل

قبل العمل مع الأحجام (Volumes)، تحتاج معرفة كيفية فحص ما يحدث داخل الحاوية:

# Open an interactive bash terminal inside a running container
docker exec -it <container_name> bash
 
# Inside the container: list files in current directory
ls
 
# Inside the container: print working directory
pwd
 
# Inside the container: display contents of a file
cat index.js
 
# View container logs (useful to verify nodemon is running)
docker logs <container_name>

العلامة -it تعني طرفية تفاعلية (Interactive Terminal) - تفتح جلسة صدفة (Shell) حية داخل الحاوية.

جعل إعادة التحميل الفوري تعمل: nodemon

افتراضيًا، لا يعيد تطبيق Node.js الذي يُشغَّل بـ node index.js تشغيل نفسه عند تغيّر الملفات. استخدم nodemon لمراقبة التغييرات وإعادة تشغيل الخادم تلقائيًا:

// package.json
{
  "scripts": {
    "start": "node index.js",
    "start:dev": "nodemon index.js"
  },
  "devDependencies": {
    "nodemon": "^3.0.0"
  }
}

حدّث تعليمة CMD في Dockerfile لاستخدام سكربت التطوير:

FROM node:18-alpine
WORKDIR /app
COPY package.json .
RUN npm install
COPY . .
CMD ["npm", "run", "start:dev"]

ربط المسارات (Bind Mounts)

ربط المسار (Bind Mount) يربط مجلدًا على جهازك المضيف مباشرة داخل الحاوية. تكون التغييرات على المضيف مرئية فورًا داخل الحاوية - لا حاجة لإعادة بناء الصورة.

# -v flag: bind mount current directory into /app inside the container
# $(pwd) resolves to your absolute path (required — relative paths do NOT work)
docker run -d \
  --name express-node-app-container \
  -p 4000:4000 \
  -v $(pwd):/app \
  my-express-app

على نظام Windows، استخدم %cd% بدلًا من $(pwd).

معمارية إعادة التحميل الفوري:

graph LR
    subgraph Host["المضيف"]
        SRC["كود المصدر\n$(pwd)"]
    end
    subgraph Container["الحاوية"]
        APP["/app\n(مربوط)"]
        NODEMON["nodemon\n(مراقب الملفات)"]
        SERVER["خادم Node.js\n:4000"]
    end
 
    SRC <-->|"مزامنة فورية"| APP
    APP --> NODEMON
    NODEMON -->|"يعيد التشغيل عند التغيير"| SERVER

ربط مسار للقراءة فقط (موصى به للتطوير)

ربط المسار الافتراضي ثنائي الاتجاه: تظهر التغييرات على المضيف في الحاوية، والتغييرات داخل الحاوية تظهر أيضًا على المضيف. هذا يمثّل خطرًا أمنيًا - يمكن لعملية داخل الحاوية أن تحذف ملفاتك.

أضف :ro (للقراءة فقط) لمنع الحاوية من الكتابة مرة أخرى إلى المضيف:

docker run -d \
  --name express-node-app-container \
  -p 4000:4000 \
  -v $(pwd):/app:ro \
  my-express-app

الآن إذا حاول شيء داخل الحاوية إنشاء أو حذف ملفات:

# Inside the container:
touch newfile.js
# Error: touch: newfile.js: Read-only file system

الأحجام المجهولة: حماية node_modules

مشكلة ربط المسار للقراءة فقط: إذا حذفت node_modules على جهازك المضيف (أو إذا لم تكن موجودة هناك)، تفقدها الحاوية أيضًا - مما يُسقط التطبيق. يحدث هذا لأن ربط المسار يعكس كلا المجلدين.

الحل: الحجم المجهول (Anonymous Volume)

علامة -v ثانية تحمل فقط مسار الحاوية (بدون مسار المضيف) تُنشئ حجمًا مجهولًا. يدير Docker هذا الحجم بشكل مستقل عن ربط المسار. يحمي ذلك المجلد من الاستبدال:

docker run -d \
  --name express-node-app-container \
  -p 4000:4000 \
  -v $(pwd):/app:ro \
  -v /app/node_modules \
  my-express-app

-v /app/node_modules تخبر Docker: "احتفظ بـ /app/node_modules في حجمه الخاص - لا تدع ربط مسار المضيف يمسّه."

ملخص أنواع الأحجام:

النوعالصياغةالوصفحالة الاستخدام
ربط مسار (Bind Mount)-v /host/path:/container/pathمزامنة ثنائية الاتجاه بين المضيف والحاويةإعادة التحميل الفوري أثناء التطوير
ربط مسار للقراءة فقط-v /host/path:/container/path:roباتجاه واحد: تغييرات المضيف تصل للحاوية، وليس العكسمزامنة كود المصدر (موصى به)
حجم مجهول (Anonymous Volume)-v /container/pathيديره Docker، بلا مسار مضيفحماية node_modules، بيانات مؤقتة
حجم مُسمّى (Named Volume)-v myvolume:/container/pathيديره Docker باسم محدداستمرارية بيانات قاعدة البيانات
# List all volumes
docker volume ls
 
# Inspect a volume
docker volume inspect <volume_name>
 
# Remove a volume
docker volume rm <volume_name>
 
# Create a named volume explicitly
docker volume create mydata

ربط مسار انتقائي: مجلد المصدر فقط

نهج أنظف هو ربط مجلد يحتوي على كود مصدر تطبيقك فقط (مثل ./src)، وليس جذر المشروع بأكمله. هذا يُبقي node_modules خارج الربط بشكل طبيعي:

docker run -d \
  --name express-node-app-container \
  -p 4000:4000 \
  -v $(pwd)/src:/app/src:ro \
  my-express-app

هذا هو الحل الأنظف: لا حاجة لحجم مجهول، ولا خطر من استبدال node_modules.


Docker Compose

Docker Compose أداة تأتي مع Docker (لكن لها رقم إصدار خاص بها) تتيح لك تعريف وتشغيل الحاويات باستخدام ملف إعدادات YAML. بدلًا من تشغيل أوامر docker build وdocker run الطويلة في كل مرة، تكتب الإعدادات مرة واحدة وتستخدم أوامر بسيطة.

تحقق من الإصدارات:

docker version
docker compose version

لماذا Compose؟

بدون Compose، يتطلب تشغيل تطبيق موزّع في حاويات:

# Step 1: build
docker build -t my-express-app .
 
# Step 2: run (long command, error-prone to retype every time)
docker run -d \
  --name express-node-app-container \
  -p 4000:4000 \
  -v $(pwd)/src:/app/src:ro \
  -v /app/node_modules \
  my-express-app

مع Compose، يصبح هذا خطوتين - مرة واحدة فقط:

docker compose up -d

يصبح Compose أكثر أهمية عندما يكون لديك حاويات متعددة (تطبيق + قاعدة بيانات + تخزين مؤقت + وكيل عكسي Reverse Proxy) تحتاج للتواصل.

بنية docker-compose.yml

يعيش ملف docker-compose.yml في جذر المشروع بجانب Dockerfile.

version: "3"
 
services:
  node-app:
    container_name: express-node-app-container
    build: .
    ports:
      - "4000:4000"
    volumes:
      - ./src:/app/src:ro
      - /app/node_modules

الحقول الأساسية:

الحقلما يعادله في docker runالوصف
container_name--nameاسم الحاوية
build: .(يشغّل docker build أولًا)مسار Dockerfile؛ . = المجلد الحالي
ports-p host:containerربط المنافذ
volumes-vربط الأحجام (ربط مسار أو مجهول)
environment--envمتغيرات بيئة مباشرة (Inline)
env_file--env-fileتحميل المتغيرات من ملف
depends_on(لا يوجد ما يعادله)تبعية ترتيب البدء

ملاحظة حول الأحجام في Compose: على عكس علامة -v في docker run (التي تتطلب مسارات مطلقة)، يدعم Compose المسارات النسبية. ./src تعمل بشكل جيد في Compose وتُحل تلقائيًا إلى المجلد الحالي.

ملاحظة حول تباعد YAML (Indentation): صياغة YAML حساسة للتباعد. يجب أن يكون كل مفتاح فرعي متسق التباعد (مسافتان هو المعيار). التباعد غير الصحيح يسبب أخطاء تحليل (Parsing Errors).

مثال معماري (حاويات متعددة)

graph TD
    subgraph Docker Compose Application["تطبيق Docker Compose"]
        WEB["خدمة node-app\n(Node.js)\nمنفذ 4000"]
        DB["خدمة db\n(PostgreSQL)\nمنفذ 5432"]
        CACHE["خدمة cache\n(Redis)\nمنفذ 6379"]
    end
 
    subgraph Volumes["الأحجام"]
        PGDATA["pgdata (حجم مُسمّى)"]
    end
 
    WEB -->|"depends_on"| DB
    WEB -->|"depends_on"| CACHE
    DB --- PGDATA

كل الخدمات في نفس ملف docker-compose.yml يمكنها الوصول لبعضها باستخدام اسم الخدمة كاسم مضيف (Hostname). ينشئ Docker Compose شبكة داخلية تلقائيًا.

أوامر Docker Compose الأساسية

# Start all services — builds image if needed, then runs containers
docker compose up
 
# Start in detached mode (background, terminal freed)
docker compose up -d
 
# Start and force rebuild images
docker compose up -d --build
 
# Stop and remove all containers defined in the file
docker compose down
 
# Stop and remove containers AND volumes
docker compose down -v
 
# View logs for all services
docker compose logs
 
# Follow logs for a specific service
docker compose logs -f node-app
 
# List running services
docker compose ps
 
# Open a shell inside a running service
docker compose exec node-app bash
 
# Rebuild a specific service without cache
docker compose build --no-cache node-app
 
# Show available commands
docker compose --help

كيف يسمّي Compose الصورة التي يبنيها:

إذا كان اسم المجلد الذي يحتوي على docker-compose.yml هو my-express-app واسم الخدمة node-app، ستُسمَّى الصورة my-express-app_node-app. يشتق Compose اسم الصورة من مجلد المشروع واسم الخدمة تلقائيًا.

مثال كامل: تطبيق + قاعدة بيانات

version: "3"
 
services:
  node-app:
    container_name: express-node-app-container
    build: .
    ports:
      - "4000:4000"
    volumes:
      - ./src:/app/src:ro
      - /app/node_modules
    env_file:
      - .env
    depends_on:
      - db
 
  db:
    image: postgres:15-alpine
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: devuser
      POSTGRES_PASSWORD: devpassword
      POSTGRES_DB: myappdb
    volumes:
      - pgdata:/var/lib/postgresql/data
 
volumes:
  pgdata:

متغيرات البيئة (Environment Variables)

تتيح لك متغيرات البيئة ضبط سلوك التطبيق دون كتابة القيم بشكل ثابت داخل الصورة. هذا ضروري لـ:

  • التبديل بين بيئات التطوير والتجهيز (Staging) والإنتاج
  • إبقاء بيانات اعتماد قاعدة البيانات والأسرار خارج قاعدة الكود
  • جعل الحاويات قابلة للنقل والتهيئة وقت التشغيل

الطريقة 1: ENV داخل Dockerfile

FROM node:18-alpine
WORKDIR /app
ENV PORT=4000
ENV NODE_ENV=production
COPY package.json .
RUN npm install
COPY . .
CMD ["npm", "start"]

يصل التطبيق إليها عبر process.env.PORT.

الطريقة 2: علامة --env مع docker run

docker run -d \
  --name my-node-app-container \
  -p 4000:4000 \
  --env PORT=4000 \
  --env NODE_ENV=development \
  my-express-app

الصيغة المختصرة: -e PORT=4000

تحقق من متغيرات البيئة داخل حاوية قيد التشغيل:

docker exec -it my-node-app-container bash
 
# List all environment variables
printenv
 
# Get a specific variable
printenv PORT
# Output: 4000
 
printenv NODE_ENV
# Output: development

الطريقة 3: علامة --env-file

لعدة متغيرات، خزّنها في ملف .env:

# .env
PORT=4000
NODE_ENV=development
DB_HOST=localhost
DB_PASSWORD=secret

مرّر الملف بأكمله:

docker run -d \
  --name my-node-app-container \
  -p 4000:4000 \
  --env-file .env \
  my-express-app

الطريقة 4: environment داخل docker-compose.yml (مباشرة - Inline)

services:
  node-app:
    build: .
    ports:
      - "4000:4000"
    environment:
      - PORT=4000
      - NODE_ENV=production

الطريقة 5: env_file داخل docker-compose.yml (موصى بها)

services:
  node-app:
    build: .
    ports:
      - "4000:4000"
    env_file:
      - .env

هذا هو النهج المُوصى به للأسرار - يُحمَّل ملف .env وقت التشغيل لكنه لا يُخبَز داخل الصورة.

ملف .env:

PORT=4000
NODE_ENV=development
DB_HOST=localhost
DB_PASSWORD=secret

أمان .env

أضف .env دائمًا إلى كل من .gitignore و.dockerignore:

# .gitignore and .dockerignore
.env
.env.local
.env.production

وفّر ملف .env.example لزملاء الفريق:

PORT=4000
NODE_ENV=development
DB_HOST=localhost
DB_PASSWORD=your-password-here

استبدال المتغيرات داخل docker-compose.yml

services:
  db:
    image: postgres:15
    environment:
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_USER: ${DB_USER:-defaultuser}    # default value if variable not set

يقرأ Docker Compose ملف .env تلقائيًا في نفس المجلد - لا حاجة لعلامة --env-file.

الوصول لمتغيرات البيئة في Node.js

const port = process.env.PORT || 4000;
const dbUrl = process.env.DATABASE_URL;
const nodeEnv = process.env.NODE_ENV;
 
app.listen(port, () => {
  console.log(`Server running on port ${port} in ${nodeEnv} mode`);
});

أمثلة عملية

إعداد تطوير كامل باستخدام Docker Compose

بنية المشروع:

my-app/
├── .dockerignore
├── .env
├── .env.example
├── .gitignore
├── docker-compose.yml
├── docker-compose.prod.yml
├── Dockerfile
├── Dockerfile.dev
├── package.json
├── package-lock.json
└── src/
    └── server.js

Dockerfile (الإنتاج):

FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
 
FROM node:20-alpine AS runner
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
EXPOSE 3000
CMD ["node", "src/server.js"]

Dockerfile.dev (التطوير):

FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
EXPOSE 3000
CMD ["npx", "nodemon", "src/server.js"]

docker-compose.yml (التطوير):

version: "3.9"
 
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    env_file:
      - .env
    depends_on:
      - db
 
  db:
    image: postgres:15-alpine
    ports:
      - "5432:5432"
    env_file:
      - .env
    volumes:
      - pgdata:/var/lib/postgresql/data
 
volumes:
  pgdata:

.dockerignore:

node_modules
.env
.git
.gitignore
*.log
npm-debug.log*
.DS_Store
coverage
dist

.env:

NODE_ENV=development
PORT=3000
POSTGRES_USER=devuser
POSTGRES_PASSWORD=devpassword
POSTGRES_DB=myappdb
DATABASE_URL=postgres://devuser:devpassword@db:5432/myappdb

ربط الأحجام للتطوير

# Start dev environment with hot reload
docker compose up -d
 
# Edit any file in src/ → nodemon detects change → server restarts
# No rebuild needed
 
# When you add a new package:
docker compose down
# add package to package.json
docker compose up -d --build   # rebuild to run npm install

استخدام .env مع Docker

# Docker Compose automatically reads .env in the same directory
docker compose up -d
 
# Override with a different env file
docker compose --env-file .env.staging up -d
 
# Pass a single variable override
DATABASE_URL=postgres://prod@prodhost/db docker compose up -d

مهام المعمل

المهمة 1: حسّن ملف Dockerfile باستخدام .dockerignore وتخزين الطبقات المؤقت

بالنظر إلى ملف Dockerfile غير الفعّال هذا:

FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "index.js"]

الخطوات:

  1. أعد كتابة ملف Dockerfile بحيث ينسخ package.json أولًا، ثم يشغّل npm install، ثم ينسخ الباقي:

    FROM node:18-alpine
    WORKDIR /app
    COPY package.json .
    RUN npm install
    COPY . .
    CMD ["node", "index.js"]
  2. أنشئ .dockerignore:

    node_modules
    Dockerfile
    .dockerignore
    .env
  3. ابنِ الصورة:

    docker build -t my-express-app .
  4. أعد البناء مرة أخرى دون تغيير أي شيء - لاحظ أن كل الخطوات تُظهر CACHED.

  5. غيّر سطرًا في index.js، وأعد البناء. تأكد أن npm install لا تزال CACHED (فقط خطوة COPY . . تُعاد).

  6. غيّر package.json (أضف مسافة)، وأعد البناء. تأكد أن npm install تُعاد الآن (تم إبطال التخزين المؤقت).

  7. شغّل الحاوية وتحقق أنها تعمل:

    docker run -d --name express-node-app-container -p 4000:4000 my-express-app
    curl http://localhost:4000
  8. افتح صدفة (Shell) بداخلها وتحقق أن Dockerfile غير موجود:

    docker exec -it express-node-app-container bash
    ls
    # Dockerfile should NOT appear

المهمة 2: إعادة التحميل الفوري باستخدام ربط الأحجام

الهدف: حرّر ملفات المصدر وشاهد التغييرات دون إعادة بناء الصورة.

الخطوات:

  1. أضف nodemon إلى مشروعك وحدّث package.json:

    {
      "scripts": {
        "start": "node index.js",
        "start:dev": "nodemon index.js"
      },
      "devDependencies": {
        "nodemon": "^3.0.0"
      }
    }
  2. حدّث تعليمة CMD في Dockerfile:

    CMD ["npm", "run", "start:dev"]
  3. أعد بناء الصورة:

    docker build -t my-express-app .
  4. أوقف واحذف الحاوية الموجودة:

    docker stop express-node-app-container
    docker rm express-node-app-container
  5. شغّل مع ربط مسار (مصدر فقط) وحجم مجهول لـ node_modules:

    docker run -d \
      --name express-node-app-container \
      -p 4000:4000 \
      -v $(pwd):/app:ro \
      -v /app/node_modules \
      my-express-app
  6. تحقق أن nodemon يعمل:

    docker logs express-node-app-container
    # Expected: [nodemon] watching path(s): *.*
  7. افتح index.js وغيّر نص الاستجابة. حدّث http://localhost:4000 - يجب أن يظهر التغيير فورًا دون إعادة بناء.

  8. حاول الكتابة داخل الحاوية للتحقق من حماية القراءة فقط:

    docker exec -it express-node-app-container bash
    touch /app/newfile.js
    # Expected: Read-only file system error
  9. احذف node_modules من جهازك المضيف. تحقق أن الحاوية لا تزال تعمل (الحجم المجهول يحميها):

    rm -rf node_modules
    curl http://localhost:4000
    # App should still respond

المهمة 3: استخدم Docker Compose

الهدف: استبدل docker build + docker run بملف docker-compose.yml.

الخطوات:

  1. أوقف واحذف الحاويات:

    docker stop express-node-app-container
    docker rm express-node-app-container
  2. أنشئ docker-compose.yml في جذر المشروع:

    version: "3"
     
    services:
      node-app:
        container_name: express-node-app-container
        build: .
        ports:
          - "4000:4000"
        volumes:
          - ./:/app:ro
          - /app/node_modules
  3. ابدأ باستخدام Compose (يبني الصورة تلقائيًا):

    docker compose up -d
  4. تحقق أن الحاوية تعمل:

    docker ps
    # Expected: express-node-app-container listed
  5. تحقق أن التطبيق يستجيب:

    curl http://localhost:4000
  6. أوقف الحاوية باستخدام Compose:

    docker compose down
  7. تأكد أن الحاوية اختفت:

    docker ps
    # express-node-app-container should NOT be listed

المهمة 4: متغيرات البيئة من .env عبر Compose

الهدف: استخرج كل الإعدادات إلى ملف .env خارجي.

الخطوات:

  1. أنشئ .env في جذر المشروع:

    PORT=4000
    NODE_ENV=development
    DB_HOST=localhost
    DB_PASSWORD=secret123
  2. أضف .env إلى .dockerignore و.gitignore.

  3. حدّث docker-compose.yml لاستخدام env_file:

    version: "3"
     
    services:
      node-app:
        container_name: express-node-app-container
        build: .
        ports:
          - "4000:4000"
        volumes:
          - ./:/app:ro
          - /app/node_modules
        env_file:
          - .env
  4. ابدأ الخدمات:

    docker compose up -d
  5. تحقق من متغيرات البيئة داخل الحاوية:

    docker exec -it express-node-app-container bash
    printenv PORT
    # Expected: 4000
    printenv NODE_ENV
    # Expected: development
    printenv DB_PASSWORD
    # Expected: secret123
  6. أوقف كل شيء:

    docker compose down

الملخص

المفهومالفكرة الأساسية
.dockerignoreاستبعد node_modules وDockerfile و.env لتسريع البناء وإبقاء الأسرار خارج الصورة
تخزين الطبقات المؤقتانسخ package.json قبل ملفات المصدر حتى تُخزَّن npm install مؤقتًا بين تغييرات الكود
docker exec -itتفتح طرفية تفاعلية داخل حاوية قيد التشغيل
docker logsتعرض الناتج (stdout) من حاوية قيد التشغيل
ربط المسار (:)مزامنة ثنائية الاتجاه بين مسار المضيف ومسار الحاوية
المُعدِّل :roيجعل ربط المسار للقراءة فقط - لا يمكن للحاوية الكتابة مرة أخرى إلى المضيف
الحجم المجهولعلامة -v /container/path ثانية (بلا مسار مضيف) تحمي مجلدًا من ربط المسار
docker volume lsتسرد كل الأحجام التي يديرها Docker
docker compose up -dتبني الصورة وتبدأ كل الخدمات من docker-compose.yml
docker compose downتوقف وتحذف كل الخدمات
المسارات النسبية في Composeعلى عكس docker run، يقبل Compose ./src بدلًا من المسارات المطلقة
environment: في Composeعرّف متغيرات البيئة مباشرة داخل docker-compose.yml
env_file: في Composeحمّل متغيرات البيئة من ملف .env (موصى به للأسرار)
printenv (داخل الحاوية)تسرد كل متغيرات البيئة المضبوطة في الحاوية

أوامر مرجعية سريعة

# --- Containers ---
docker build -t myapp .                          # Build image from Dockerfile in current directory
docker run -d --name myapp -p 4000:4000 myapp   # Run container in background
docker ps                                        # List running containers
docker stop myapp                               # Stop a container
docker rm myapp                                 # Remove a stopped container
docker rm -f myapp                              # Force stop and remove
 
# --- Inspect containers ---
docker exec -it myapp bash                      # Open interactive shell in container
docker logs myapp                               # View container logs
docker logs -f myapp                            # Follow (tail) container logs
 
# --- Volumes ---
docker volume ls                                # List all volumes
docker volume inspect <name>                    # Show volume details
docker volume rm <name>                         # Remove a volume
 
# --- Bind mounts (development) ---
docker run -v $(pwd):/app myapp                 # Two-way bind mount
docker run -v $(pwd):/app:ro myapp              # Read-only bind mount
docker run -v $(pwd):/app:ro -v /app/node_modules myapp  # With anonymous volume
 
# --- Docker Compose ---
docker compose up -d                            # Start all services (detached)
docker compose up -d --build                    # Force rebuild before starting
docker compose down                             # Stop and remove all services
docker compose down -v                          # Also remove volumes
docker compose logs -f node-app                 # Follow logs for a service
docker compose exec node-app bash              # Shell into a running service
docker compose ps                               # List running services
docker compose --help                           # Show available commands