انتقال للمقال
وقت القراءة: ≈ 20 دقيقة (بمعدل فنجان واحد من القهوة 😊)

مقدمة عن Docker Compose

السلام عليكم ورحمة الله وبركاته


المقدمة

في هذه المرحلة أتوقع أن لديك فكرة جيدة عن كيفية تشغيل تطبيق بسيط باستخدام Docker، وتعرفت عن الـ Volumes وكيفية ربطها، وعن الـ Networks وكيفية التواصل بين الـ Containers
لكن ألم تلحظ أننا كنا نقوم بكتابة أوامر كثيرة ووطويلة بمجرد تشغيل مشروع Node.js مع قاعدة بيانات MySQL

الهدف كان إنشاء Container للـ Node.js و Container لقاعدة البيانات MySQL وربطهما معًا في Network واحدة، وربط الـ Volume الخاص مع الـ MySQL، وتمرير environment variables مع كل Container، وعمل Port Mapping للـ Node.js لكي نتمكن من الوصول له من المتصفح
وكل مرة نريد تشغيل المشروع علينا تكرار هذه الأوامر، مما يجعل الأمر مرهقًا جدًا، خصوصًا إذا كان المشروع يحتوي على أكثر من خدمة مثل و Backend و Database و Redis و Elasticsearch و RabbitMQ وغيرها

دعنا نتذكر سويًا الأوامر التي اضطررنا لكتابتها في المرة السابقة لتشغيل المشروع
أولًا، كان علينا إنشاء الـ Network:

> docker network create tabarani-network

ثم قمنا بإنشاء الـ Volume التي سنستخدمها لقاعدة البيانات:

> docker volume create mysql-tabarani-db

ثم قمنا بتحميل الـ Image الخاصة بالـ MySQL وإنشاء الـ Container الخاص بها وربطه بالـ Network و الـ Volume وتمرير بعض الـ Environment Variables والـ Flags الأخرى:

> docker container run -d --rm --name mysql-container --network tabarani-network -e MYSQL_ROOT_PASSWORD=tabarani-very-secret -e MYSQL_DATABASE=tabarani-app -v mysql-tabarani-db:/var/lib/mysql mysql:9.6.0

ثم فمنا ببناء الـ Image الخاصة بالتطبيق من الـ Dockerfile

> docker image build -t nodejs-mysql-app:v1.0 .

وأخيرًا قمنا بإنشاء الـ Container الخاص بالتطبيق وربطه بالـ Network وتمرير الـ Environment Variables وعمل Port Mapping:

> docker container run -d --rm --name nodejs-container --network tabarani-network --env-file .env -p 3000:3000 nodejs-mysql-app:v1.0

كل هذه الأوامر كانت ضرورية لتشغيل المشروع، ولكنها كانت طويلة وقد تكثر هذه الأوامر مع زيادة عدد الخدمات في المشروع، مما يجعل الأمر معقدًا جدًا ويعرضك لخطأ بشري عند كتابة الأوامر أو نسيان أحدها
لذا كان لا بد من وجود شيء ما يساعدنا في إدارة وإنشاء الـ Containers المتعددة والمرتبطة مع بعضها البعض بطريقة أسهل وأكثر تنظيمًا
وهذه هي نقطة ظهور أداة Docker Compose، التي ستجعل حياتنا أسهل بكثير في إدارة وإنشاء التطبيقات والـ Containers الخاصة بها

ما هو Docker Compose ؟

الـ Docker Compose هو أداة تأتي مع الـ Docker تتيح لنا تعريف وإنشاء عدة Containers معًا من خلال ملف واحد فقط
بدلاً من كتابة عدة أوامر طويلة في الـ Terminal لكل Container على حدة
نقوم بكتابة جميع الإعدادات والأوامر في في ملف واحد يسمى docker-compose.yml
ثم نقوم بإنشاء جميع الـ Containers بأمر واحد فقط

هذا الملف يسهل عليك إدارة كل الـ Containers ويحدد كل شيء في مكان واحد، مما يجعل الأمر أسهل وأسرع وأكثر تنظيمًا ويساعدك في تجنب الأخطاء البشرية عند كتابة الأوامر أو نسيان أحدها
لأنك ستكتبها مرة واحدة فقط في ملف الـ docker-compose.yml ثم يمكنك إنشاءها بأمر واحد فقط

ملف docker-compose.yml

ملف الـ docker-compose.yml هو ملف بصيغة YAML يحتوي على جميع الإعدادات الخاصة بالـ Containers التي نريد إنشاءها
يجب أن يكون اسم الملف docker-compose.yml أو docker-compose.yaml أو compose.yml أو compose.yaml

قبل أن نبدأ بكتابة ملف الـ docker-compose.yml دعنا نفهم بنية هذا الملف بشكل عام
الملف يتكون من عدة أقسام رئيسية:

services:
  # Define your services (containers) here

volumes:
  # Create named volumes here

networks:
  # Create custom networks here

القسم الأهم هو services وهو الذي نعرف فيه الـ Containers التي نريد إنشاءها
كل Container نسميه Service في عالم الـ Docker Compose
والقسمان volumes و networks اختياريان ونستخدمهما حسب الحاجة
بالطبع في حالة مشروعنا سنحتاج إلى قسم volumes لتعريف الـ Volume الخاص بالـ MySQL
وأما بالنسبة للـ networks فلن نحتاج لتعريف شبكة مخصصة لأن الـ Docker Compose ينشئ شبكة تلقائيًا لجميع الـ Services التي قمنا بتعريفها في الملف ويربطهم ببعض بشكل تلقائي

مثال بسيط لملف docker-compose.yml

قبل أن نخوض في مثال الـ Node.js و MySQL دعنا نرى مثال بسيط جدًا لملف docker-compose.yml يحتوي على Service واحد فقط:

عندما كنا نريد إنشاء Container بسيط من الـ Nginx كنا نكتب:

> docker container run -d -p 8080:80 --rm --name nginx-container nginx:alpine

هكذا قمنا بإنشاء Container بسيط من الـ Nginx يعمل في الخلفية بسبب -d ويقوم بعمل Port Mapping من 8080 في الجهاز إلى 80 داخل الـ Container، ومررنا --rm ليتم حذف الـ Container تلقائيًا عند إيقافه، ثم أعطيناه اسم nginx-container وأخيرًا حددنا الـ Image التي نريد استخدامها وهي nginx:alpine
كل هذا في أمر واحد طويل نحتاج كتابته كل مرة نريد تشغيل الـ Nginx

لنقم الآن بتحويل هذا الأمر إلى ملف docker-compose.yml لكن لنقم بإيقاف الـ container الحالي أولًا:

> docker container stop nginx-container
nginx-container

الآن دعنا نكتب نفس أمر إنشاء الـ nginx-container في ملف docker-compose.yml:

services:
  nginx-service:
    image: nginx:alpine
    ports:
      - "8080:80"

هذا الملف البسيط يحتوي على Service واحد فقط اسمه nginx-container يستخدم الـ Image الخاصة بـ nginx:alpine ويقوم بعمل Port Mapping من 8080 إلى 80
لاحظ كيف أصبح الأمر أبسط وأوضح ويمكنك قراءته بسهولة

لاحظ أن ملفات الـ docker-compose.yml تستخدم صيغة YAML وهي صيغة تعتمد على المسافات البادئة لتنظيم البيانات، لذلك يجب أن تكون حذرًا في كتابة المسافات بشكل صحيح، وعادةً ما يتم استخدام مسافتين أو أربع مسافات لكل مستوى من المستويات في الملف
وأيضًا القيم تكون بصيغة key: value أو في بعض الحالات تكون على شكل قائمة باستخدام - كما في حالة الـ ports في حالة أنها تستقبل أكثر من قيمة
وكلمة services و image و ports هي كلمات محجوزة في صيغة الـ docker-compose.yml ويجب كتابتها بشكل صحيح حتى يتم التعرف عليها من قبل الـ Docker Compose
أما nginx-service فهو اسم الـ Service الذي اخترناه ويمكنك تسميته بأي اسم تريده

تشغيل الـ Container باستخدام Docker Compose

الآن لإنشاء هذا الـ Container من ملف الـ docker-compose.yml نستخدم الأمر docker compose up:

> docker compose up -d
[+] Running 2/2
 ✔ Network nodejs-mysql-app_default            Created                                                                                                                                                                                            0.1s
 ✔ Container nodejs-mysql-app-nginx-service-1  Started                                                                                                                                                                                            0.6s

أول شيء لاحظ أننا وضعنا -d لتشغيل الـ Container في الخلفية

حاليًا أمر الـ docker compose up قام بإنشاء الـ Container الخاص بالـ Nginx وهو الآن يعمل في localhost:8080
ولاحظ أنه تم إنشاء Network تلقائيًا باسم nodejs-mysql-app_default وربط الـ Container بها بشكل تلقائي
لاحظ أن اسم الـ Container الذي تم إنشاؤه هو nodejs-mysql-app-nginx-service-1 وهو يتكون من اسم المجلد الذي يحتوي على ملف الـ docker-compose.yml ثم اسم الـ Service ثم رقم تسلسلي في حالة وجود أكثر من Container لنفس الـ Service وهذا هو الشكل الافتراضي لأسماء الـ Containers التي ينشئها الـ Docker Compose
يمكنك تغيير هذا الاسم إذا أردت من خلال كتابة container_name داخل الـ Service:

services:
  nginx-service:
    container_name: nginx-container
    image: nginx:alpine
    ports:
      - "8080:80"

هكذا استخدمنا container_name لتحديد اسم الـ Container الذي نريده بدلاً من الاسم الافتراضي الذي ينشئه الـ Docker Compose
وبالطبع سيتم تغير اسم الـ Container إلى nginx-container بدلاً من nodejs-mysql-app-nginx-service-1 عندما نقوم بتشغيل الأمر docker compose up مرة أخرى

لنرى الـ Container الذي تم إنشاؤه الآن:

> docker container list
CONTAINER ID   IMAGE          COMMAND                  CREATED          STATUS          PORTS                  NAMES
57321c05d42c   nginx:alpine   "/docker-entrypoint.…"   13 seconds ago   Up 13 seconds   0.0.0.0:8080->80/tcp   nodejs-mysql-app-nginx-service-1

الآن الـ Container يعمل ويمكنك الوصول له من المتصفح عبر localhost:8080 وسترى صفحة الترحيب الخاصة بالـ Nginx

إيقاف الـ Container عن طريق Docker Compose

الآن لنقم أولًا بإيقاف الـ Container الحالي عن طريق الأمر docker compose down:

>  docker compose down
[+] Running 2/2
 ✔ Container nodejs-mysql-app-nginx-service-1  Removed                                                                                                                                                                                     0.7s
 ✔ Network nodejs-mysql-app_default            Removed                                                                                                                                                                                     0.6s

الآن الـ Container تم إيقافه وحذفه
ولاحظ أنه قام بحذف الـ Network الذي أنشأه الـ Docker Compose
بمعنى أن الـ Docker Compose قام بإنشاء الـ Network تلقائيًا عندما قمنا بتشغيل الـ Container
ثم بعد ما قمنا بإيقاف الـ Container قام بحذف الـ Network تلقائيًا أيضًا لأنه لم يعد هناك أي Container يستخدم هذه الـ Network

تذكر مشروع Node.js و MySQL لتحويلهم إلى Docker Compose

لنبدأ بتحويل مشروعنا الذي يحتوي على Node.js و MySQL إلى استخدام Docker Compose
نفس المشروع والمثال الخاص بالمقالة السابقة مقدمة عن الـ Docker Networks ولكن هذه المرة سنستخدم Docker Compose بدلاً من كتابة الأوامر يدويًا
وإن كنت لا تتذكر فنحن قمنا بإنشاء Dockerfile لتطبيق الـ Node.js الخاص بنا، ثم قمنا بإنشاء Container منه ثم قمنا بإنشاء Container آخر لقاعدة البيانات MySQL وربطهما معًا في Network واحدة، وربط الـ Volume الخاص مع الـ MySQL، وتمرير environment variables مع كل Container، وعمل Port Mapping للـ Node.js لكي نتمكن من الوصول له من المتصفح

ملف الـ Dockerfile:

FROM node:25-alpine

WORKDIR /app

COPY package*.json .

RUN npm install

COPY . .

ENV PORT=3000
EXPOSE 3000

ENTRYPOINT ["npm", "start"]

ولدينا ملف الـ .env:

# APP Port
PORT=3000

# Database configuration
DB_HOST=mysql-container
DB_PORT=3306
DB_USER=root
DB_PASSWORD=tabarani-very-secret
DB_NAME=tabarani-app

وملف الـ .env سنقوم بتمريره للـ Container الخاص بالـ Node.js عند إنشائه بالاستخدام --env-file كما تعلمنا في المقالات السابقة

الآن ملف الـ app.js:

const express = require("express");
const mysql = require("mysql2/promise");

const app = express();
const port = process.env.PORT || 3000;

const dbConfig = {
  host: process.env.DB_HOST,
  port: process.env.DB_PORT,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  database: process.env.DB_NAME,
};

let connection;

async function connectDB() {
  connection = await mysql.createConnection(dbConfig);
  await connection.execute(`
    CREATE TABLE IF NOT EXISTS products (
      id INT AUTO_INCREMENT PRIMARY KEY,
      name VARCHAR(255) NOT NULL,
      price DECIMAL(10, 2) NOT NULL
    )
  `);
  console.log("Connected to MySQL and products table is ready");
}

app.use(express.json());

app.get("/products", async (req, res) => {
  const [rows] = await connection.execute("SELECT * FROM products");
  res.json(rows);
});

app.post("/products", async (req, res) => {
  const { name, price } = req.body;
  const [result] = await connection.execute(
    "INSERT INTO products (name, price) VALUES (?, ?)",
    [name, price],
  );
  res.status(201).json({ id: result.insertId, name, price });
});

connectDB().then(() => {
  app.listen(port, () => {
    console.log(`Server is running on port ${port}`);
  });
});

كما قلنا لا تحتاج لمعرفة أو فهم الكود الخاص بالـ Node.js
وبالطبع مع ملفات أخرى مثل .dockerignore و .gitignore وغيرها

إنشاء MySQL Service في Docker Compose

دعنا نبدأ بتحويل أمر إنشاء الـ mysql-container إلى Service داخل ملف الـ docker-compose.yml
تذكر أن الأمر الذي كنا نكتبه لإنشاء الـ mysql-container كان:

> docker container run -d --rm --name mysql-container --network tabarani-network -e MYSQL_ROOT_PASSWORD=tabarani-very-secret -e MYSQL_DATABASE=tabarani-app -v mysql-tabarani-db:/var/lib/mysql mysql:9.6.0

الآن دعنا نكتب كل هذا في ملف docker-compose.yml:

services:
  mysql-service:
    image: mysql:9.6.0
    container_name: mysql-container
    environment:
      - MYSQL_ROOT_PASSWORD=tabarani-very-secret
      - MYSQL_DATABASE=tabarani-app
    volumes:
      - mysql-tabarani-db:/var/lib/mysql

volumes:
  mysql-tabarani-db:

لاحظ كيف أصبح الأمر أبسط وأوضح وكل الإعدادات الخاصة بالـ mysql-container موجودة في مكان واحد داخل ملف الـ docker-compose.yml

لنشرح كل سطر:

  • services : القسم الرئيسي الذي نعرف فيه الـ Services التي نريد إنشاءها وكل Service يمثل Container في عالم الـ Docker Compose
  • mysql-service : اسم الـ Service وهو ما يمثل الـ Container الخاص بالـ MySQL
  • image : الـ Image التي سيتم استخدامها لإنشاء الـ Container وهي mysql:9.6.0 في حالتنا
  • container_name : اسم الـ Container الذي نريده، هذا اختياري ويمكنك تركه ليتم إنشاؤه بشكل افتراضي من قبل الـ Docker Compose
  • environment: : الـ Environment Variables التي نريد تمريرها للـ Container مثل ما كنا نمررها باستخدام -e

لاحظ أننا لدينا volumes داخل الـ Service وهو يستخدم لربط الـ Volume دااخل الـ Container، مثل ما كنا نفعل من خلال -v
ولدينا قسم مستقل يدعى volumes في نفس مستوى services نستخدمه لتعريف الـ Volume الذي نريد استخدامه في الـ Service، مثل ما كنا نفعل من خلال docker volume create

لاحظ أننا لم نحتاج لكتابة --network لأن الـ Docker Compose ينشئ Network تلقائيًا ويربط جميع الـ Services بها


يمكنك كتابة الـ Environment Variables بطريقتين مختلفتين:

الطريقة الأولى باستخدام -:

environment:
  - MYSQL_ROOT_PASSWORD=tabarani-very-secret
  - MYSQL_DATABASE=tabarani-app

والطريقة الثانية باستخدام key: value:

environment:
  MYSQL_ROOT_PASSWORD: tabarani-very-secret
  MYSQL_DATABASE: tabarani-app

كلا الطريقتين صحيحة وتعمل بنفس الكيفية


يمكننا الآن تشغيل هذا الـ Service باستخدام الأمر docker compose up:

docker compose up -d
[+] Running 3/3
 ✔ Network nodejs-mysql-app_default             Created                                                                                                                                                                                           0.0s
 ✔ Volume "nodejs-mysql-app_mysql-tabarani-db"  Created                                                                                                                                                                                           0.0s
 ✔ Container mysql-container                    Started                                                                                                                                                                                           0.4s

لاحظ أنه قام بإنشاء الـ Network تلقائيًا باسم nodejs-mysql-app_default وربط الـ mysql-container بها
وأنشأ الـ Volume الخاص بنا باسم nodejs-mysql-app_mysql-tabarani-db وربطه بالـ mysql-container أيضًا
وأنشأ الـ Container الخاص بالـ MySQL بنجاح باستخدام الإعدادات التي كتبناها في ملف الـ docker-compose.yml

كل هذا في أمر واحد docker compose up
الآن لنقف الـ Container الخاص بالـ MySQL باستخدام الأمر docker compose down:

docker compose down
[+] Running 2/2
 ✔ Container mysql-container         Removed                                                                                                                                                                                                      1.5s
 ✔ Network nodejs-mysql-app_default  Removed                                                                                                                                                                                                      0.7s

لاحظ أنه قام بحذف الـ Container الخاص بالـ MySQL وحذف الـ Network الذي أنشأه تلقائيًا
لكنه لم يحذف الـ Volume لأنها ليست Anonymous Volume بل Named Volume وبالتالي لا يتم حذفه تلقائيًا عند إيقاف الـ Container
وهذه من مميزات الـ Named Volumes التي ذكرناها في مقالة حفظ البيانات في Docker باستخدام Volumes

ولو أردنا حذف الـ Volume أيضًا عند إيقاف الـ Container فيمكننا إضافة --volumes مع أمر docker compose down
لكن لا أنصح بهذا لأنك هكذا ستحذف الـ Volume وجميع البيانات الموجودة فيه، وهذا قد يكون كارثيًا إذا كان لديك بيانات مهمة في الـ Volume

إنشاء Node.js Service في Docker Compose

الآن دعنا نضيف الـ Service الخاص بالـ Node.js في نفس ملف الـ docker-compose.yml
تذكر أن الأمر الذي كنا نكتبه كان:

> docker image build -t nodejs-mysql-app:v1.0 .
> docker container run -d --rm --name nodejs-container --network tabarani-network --env-file .env -p 3000:3000 nodejs-mysql-app:v1.0

كنا نحتاج أولًا لبناء الـ Image من الـ Dockerfile ثم ننشيء الـ Container من هذه الـ Image

لنحول هذا إلى Service في ملف الـ docker-compose.yml:

services:
  mysql-service:
    image: mysql:9.6.0
    container_name: mysql-container
    environment:
      - MYSQL_ROOT_PASSWORD=tabarani-very-secret
      - MYSQL_DATABASE=tabarani-app
    volumes:
      - mysql-tabarani-db:/var/lib/mysql

  nodejs-service:
    build: .
    container_name: nodejs-container
    ports:
      - "3000:3000"
    env_file:
      - .env

volumes:
  mysql-tabarani-db:

الآن لدينا Service جديد اسمه nodejs-service في داخله كتبنا build: . وهذا يعني أن الـ Docker Compose سيقوم ببناء الـ Image الخاصة بالـ Node.js من الـ Dockerfile الموجود في نفس المجلد
ثم بالطبع قمنا بعمل Port Mapping من 3000 إلى 3000 لتمكين الوصول للتطبيق من المتصفح
واسمينا الـ Container الخاص بالـ Node.js باسم nodejs-container
وأخيرًا استخدمنا env_file لتمرير ملف .env كامل بدلاً من كتابة كل Environment Variable بشكل منفصل

الآن تذكر أن ملف .env يحتوي على DB_HOST=mysql-container وهذا يعني أن الـ Node.js سيحاول الاتصال بالـ MySQL من خلال اسم الـ Container الخاص به، وهذا ممكن لأن الـ Docker Compose ينشئ Network ويربط جميع الـ Services بها، مما يسمح لهم بالتواصل مع بعضهم البعض باستخدام أسماء الـ Services أو أسماء الـ Containers
بمعنى أننا لو كتبنا DB_HOST=mysql-service أو DB_HOST=mysql-container في ملف .env سيعمل المشروع بشكل طبيعي لأن كلا الاسمين يشير إلى نفس الـ Container الخاص بالـ MySQL في الـ Network التي أنشأها الـ Docker Compose


الآن بعد أن أضفنا الـ Service الخاص بالـ Node.js في ملف الـ docker-compose.yml
لنقم بتشغيل المشروع باستخدام الأمر docker compose up:

> docker compose up -d
[+] Building 2.9s (12/12) FINISHED                                                                                                                                                                                                docker:desktolinuxp-
...
 => [nodejs-service 1/5] FROM docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997                                                                                                            2.5s
 => => resolve docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997                                                                                                                           2.5s
 => [nodejs-service internal] load build context                                                                                                                                                                                                  0.0s
 => => transferring context: 629B                                                                                                                                                                                                                 0.0s
 => [nodejs-service auth] library/node:pull token for registry-1.docker.io                                                                                                                                                                        0.0s
 => CACHED [nodejs-service 2/5] WORKDIR /app                                                                                                                                                                                                      0.0s
 => CACHED [nodejs-service 3/5] COPY package*.json .                                                                                                                                                                                              0.0s
 => CACHED [nodejs-service 4/5] RUN npm install                                                                                                                                                                                                   0.0s
 => [nodejs-service 5/5] COPY . .                                                                                                                                                                                                                 0.0s
...
[+] Running 4/4
 ✔ nodejs-service                    Built                                                                                                                                                                                                        0.0s
 ✔ Network nodejs-mysql-app_default  Created                                                                                                                                                                                                      0.0s
 ✔ Container nodejs-container        Started                                                                                                                                                                                                      0.6s
 ✔ Container mysql-container         Started                                                                                                                                                                                                      0.5s

لاحظ أن أول شيء قام به الـ Docker Compose هو بناء الـ Image الخاصة بالـ Node.js من الـ Dockerfile لأننا استخدمنا build: . في ملف الـ docker-compose.yml
ثم قام بإنشاء الـ Network تلقائيًا وربط كلا الـ Containers بها وأخيرًا قام بإنشاء وتشغيل كلا الـ Containers الخاصين بالـ Node.js و MySQL بنجاح

لاحظ أنه لم يقم بإنشاء الـ Volume الخاص بالـ MySQL لأنه تم إنشاؤه مسبقًا في المرة السابقة، والـ Docker Compose لا يعيد إنشاء الـ Volume إذا كان موجودًا بالفعل، بل يستخدمه مباشرةً

لكن لاحظ أن الـ nodejs-container بدأ قبل الـ mysql-container وهذا قد يسبب مشكلة في الاتصال بينهما لأن الـ Node.js سيحاول الاتصال بالـ MySQL عند بدء التشغيل، بالتالي إذا بدأ الـ Node.js قبل أن يكون الـ MySQL جاهزًا فقد يفشل الاتصال ويحدث خطأ في التطبيق

لنتأكد أن كلا الـ Containers يعملان بشكل صحيح أم لا
سنقوم فقط بعمل POST /products لإضافة منتج جديد ثم نعمل GET /products لعرض المنتجات الموجودة في قاعدة البيانات للتأكد أن الاتصال بين الـ Node.js والـ MySQL يعمل بشكل صحيح

> curl -X POST -H "Content-Type: application/json" -d '{"name": "Product 1", "price": 9.99}' http://localhost:3000/products
curl: (7) Failed to connect to localhost port 3000 after 2229 ms: Could not connect to server

لقد حدث خطأ في الاتصال، وهذا ما توقعناه بسبب أن الـ nodejs-container قبل الـ mysql-container
بالتالي عندما حاول nodejs-container الاتصال بالـ mysql-container لم يجده لأنه لم يبدأ بعد، مما أدى إلى فشل الاتصال وحدوث الخطأ في التطبيق

الآن لنقم بإيقاف المشروع باستخدام docker compose down أولًا ثم نفكر في حل هذه المشكلة:

> docker compose down
[+] Running 3/3
 ✔ Container nodejs-container        Removed                                                                                                                                                                                                      0.0s
 ✔ Container mysql-container         Removed                                                                                                                                                                                                      1.3s
 ✔ Network nodejs-mysql-app_default  Removed                                                                                                                                                                                                      0.6s

الـ depends_on

عندما نشغل عدة Services معًا، قد يحتاج بعضها أن يبدأ قبل الآخر مثل ما حدث معنا في مشروعنا، الـ Node.js يحتاج أن يتصل بالـ MySQL عند بدء الإنشاء
بالتالي يجب أن يكون الـ mysql-container يعمل قبل أن يبدأ الـ nodejs-container
وإلا فسيفشل الاتصال بالـ MySQL وسيتوقف التطبيق كما رأينا

لحل هذه المشكلة يوفر لنا الـ Docker Compose خاصية depends_on
هذه الخاصية تخبر الـ Docker Compose أن هذا الـ Service يعتمد على Service آخر ويجب أن يبدأ بعده

دعنا نضيفها لملف الـ docker-compose.yml:

services:
  mysql-service:
    image: mysql:9.6.0
    container_name: mysql-container
    environment:
      - MYSQL_ROOT_PASSWORD=tabarani-very-secret
      - MYSQL_DATABASE=tabarani-app
    volumes:
      - mysql-tabarani-db:/var/lib/mysql

  nodejs-service:
    build: .
    container_name: nodejs-container
    ports:
      - "3000:3000"
    env_file:
      - .env
    depends_on:
      - mysql-service

volumes:
  mysql-tabarani-db:

أضفنا depends_on: للـ nodejs-service وحددنا أنه يعتمد على mysql-service
هذا يعني أن الـ Docker Compose سيبدأ بإنشاء الـ mysql-service أولاً ثم سببدأ بإنشاء الـ nodejs-service بعده

لكن الـ depends_on يضمن فقط أن الـ Container سيبدأ بالترتيب الصحيح
لكنه لا يضمن أن الـ MySQL جاهز لاستقبال الاتصالات فعليًا
فقد يبدأ الـ mysql-container لكن الـ MySQL Server داخله قد يحتاج بعض الوقت ليكون جاهزًا لاستقبال الاتصالات
في هذه الحالة قد يفشل الاتصال من الـ Node.js لأن الـ MySQL لم يكن جاهزًا بعد

لكن لا تقلق في أغلب الحالات البسيطة مثل مشروعنا سيعمل بشكل طبيعي
وإذا كنت تريد حلاً أكثر دقة فيمكنك استخدام depends_on مع condition بهذا الشكل:

services:
  mysql-service:
    image: mysql:9.6.0
    container_name: mysql-container
    environment:
      - MYSQL_ROOT_PASSWORD=tabarani-very-secret
      - MYSQL_DATABASE=tabarani-app
    volumes:
      - mysql-tabarani-db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 20s
      retries: 10

  nodejs-service:
    build: .
    container_name: nodejs-container
    ports:
      - "3000:3000"
    env_file:
      - .env
    depends_on:
      mysql-service:
        condition: service_healthy

volumes:
  mysql-tabarani-db:

هنا قمنا بإضافة healthcheck للـ mysql-service لتحديد كيف نتحقق من صحة الـ MySQL داخل الـ Container
وهى عن طريق تنفيذ الأمر mysqladmin ping داخل الـ Container للتحقق مما إذا كان الـ MySQL جاهزًا أم لا
ونخبره أن الـ timeout هو 20s و retries هو 10 لذا سيحاول التحقق من صحة الـ MySQL لمدة تصل إلى 20 ثانية مع إعادة المحاولة حتى 10 مرات

ثم في الـ nodejs-service استخدمنا depends_on مع condition: service_healthy لنخبر الـ Docker Compose أن يبدأ الـ nodejs-service فقط عندما يكون الـ mysql-service وجاهزًا لاستقبال الاتصالات

بالطبع الأمر الذي نكتبه في الـ healthcheck يعتمد على نوع الـ Image الذي نستخدمه، في حالتنا استخدمنا mysql:9.6.0 الذي يحتوي على أداة mysqladmin التي يمكن استخدامها للتحقق من صحة الـ MySQL، أما إذا كنت تستخدم Image أخرى قد تحتاج إلى كتابة أمر مختلف للتحقق من الصحة


على أي حال، في أغلب الحالات البسيطة يمكنك الاعتماد فقط على depends_on بدون condition أو healthcheck وستجد أن الأمور تعمل بشكل جيد، خاصة إذا كان الـ MySQL لا يحتاج وقت طويل ليكون جاهزًا
لكن عندما تريد ضمان أن الـ Node.js لا يبدأ إلا عندما يكون الـ MySQL جاهزًا بالفعل، فإن استخدام healthcheck مع depends_on هو الحل الأفضل

الآن بعد أن أضفنا depends_on لنقم بتشغيل المشروع مرة أخرى باستخدام docker compose up:

docker compose up -d
[+] Running 3/3
 ✔ Network nodejs-mysql-app_default  Created                                                                                                                                                                                                      0.0s
 ✔ Container mysql-container         Healthy                                                                                                                                                                                                     30.1s
 ✔ Container nodejs-container        Started                                                                                                                                                                                                     30.2s

لاحظ أن الـ mysql-container أصبح Healthy بعد حوالي 30.1 ثانية، وهذا يعني أن الـ MySQL أصبح جاهزًا لاستقبال الاتصالات
وبالتالي الـ nodejs-container بدأ بعده مباشرةً في الثانية 30.2 بعد أن أصبح الـ mysql-container جاهز

الآن لنقم بعمل POST /products مرة أخرى لنتأكد أن الاتصال بين الـ Node.js والـ MySQL يعمل بشكل صحيح:

> curl -X POST -H "Content-Type: application/json" -d '{"name": "Product 1", "price": 9.99}' http://localhost:3000/products
{"id":1,"name":"Product 1","price":9.99}

تم إضافة المنتج بنجاح إلى قاعدة البيانات، مما يعني أن الاتصال بين الـ Node.js والـ MySQL يعمل بشكل صحيح الآن بعد أن أضفنا depends_on مع healthcheck

لنتأكد أيضًا أن المنتج موجود في قاعدة البيانات من خلال عمل GET /products:

> curl http://localhost:3000/products
[{"id":1,"name":"Product 1","price":9.99}]

كل شيء يعمل بشكل جيد الآن

لنقم بإيقاف المشروع باستخدام docker compose down:

> docker compose down
[+] Running 3/3
 ✔ Container nodejs-container        Removed                                                                                                                                                                                                      1.0s
 ✔ Container mysql-container         Removed                                                                                                                                                                                                      1.9s
 ✔ Network nodejs-mysql-app_default  Removed                                                                                                                                                                                                      0.7s

الملف النهائي لـ docker-compose.yml

بعد كل ما شرحناه، هذا هو الملف النهائي لـ docker-compose.yml الخاص بمشروعنا:

services:
  mysql-service:
    image: mysql:9.6.0
    container_name: mysql-container
    environment:
      - MYSQL_ROOT_PASSWORD=tabarani-very-secret
      - MYSQL_DATABASE=tabarani-app
    volumes:
      - mysql-tabarani-db:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 20s
      retries: 10

  nodejs-service:
    build: .
    container_name: nodejs-container
    ports:
      - "3000:3000"
    env_file:
      - .env
    depends_on:
      mysql-service:
        condition: service_healthy

volumes:
  mysql-tabarani-db:

ملف واحد فقط به كل شيء يخص مشروعنا من Services و Volumes و Healthcheck وكل الإعدادات الخاصة بكل Service
وعندما نريد أن نشغل المشروع كل ما علينا هو كتابة docker compose up وسيقوم الـ Docker Compose بكل العمل من بناء الـ Images وإنشاء الـ Containers وربطهم معًا في Network واحدة وتشغيلهم بالترتيب الصحيح
وإذا أردنا إيقاف المشروع كل ما علينا هو كتابة docker compose down وسيقوم الـ Docker Compose بإيقاف وحذف كل شيء بالترتيب الصحيح أيضًا
هذا هو جمال الـ Docker Compose، كل شيء في مكان واحد وبأمر واحد يمكنك تشغيل أو إيقاف مشروعك بكل سهولة

docker compose up --build

إذا قمت بتعديل كود الـ Node.js أو أي ملف في المشروع وتريد إعادة بناء الـ Image الخاصة بالـ Node.js وتشغيل الـ Containers مرة أخرى
يمكنك استخدام --build مع أمر docker compose up:

> docker compose up -d --build
[+] Building 1.8s (12/12) FINISHED                                                                                                                                                                                                docker:desktop-linux
...
 => [nodejs-service 1/5] FROM docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997                                                                                                            1.4s
 => => resolve docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997                                                                                                                           1.4s
 => [nodejs-service internal] load build context                                                                                                                                                                                                  0.0s
 => => transferring context: 200B                                                                                                                                                                                                                 0.0s
 => [nodejs-service auth] library/node:pull token for registry-1.docker.io                                                                                                                                                                        0.0s
 => CACHED [nodejs-service 2/5] WORKDIR /app                                                                                                                                                                                                      0.0s
 => CACHED [nodejs-service 3/5] COPY package*.json .                                                                                                                                                                                              0.0s
 => CACHED [nodejs-service 4/5] RUN npm install                                                                                                                                                                                                   0.0s
 => [nodejs-service 5/5] COPY . .                                                                                                                                                                                                                 0.0s
...
[+] Running 4/4
 ✔ nodejs-service                    Built                                                                                                                                                                                                        0.0s
 ✔ Network nodejs-mysql-app_default  Created                                                                                                                                                                                                      0.0s
 ✔ Container mysql-container         Healthy                                                                                                                                                                                                     30.0s
 ✔ Container nodejs-container        Started                                                                                                                                                                                                     30.1s

هذا الأمر سيعيد بناء أي Image يتم بناؤها من Dockerfile باستخدام build: ثم يعيد إنشاء الـ Containers
أما إذا لم تستخدم --build فإن الـ Docker Compose سيستخدم الـ Image المبنية سابقًا بدون إعادة بنائها، وهذا يعني أن أي تغييرات قمت بها في كود الـ Node.js أو أي ملف آخر لن تنعكس في الـ Container الجديد الذي سيتم إنشاؤه، لأنه سيستخدم الـ Image القديم الذي تم بناؤه قبل التغييرات

docker compose ps

مثلما يوجد أمر docker container list لعرض الـ Containers الموجودة، يوجدلدينا أمر docker compose ps لعرض الـ Services الموجودة في مشروع الـ Docker Compose الحالي:

> docker compose ps
NAME               IMAGE                             COMMAND                  SERVICE          CREATED         STATUS                   PORTS
mysql-container    mysql:9.6.0                       "docker-entrypoint.s…"   mysql-service    5 minutes ago   Up 5 minutes (healthy)   3306/tcp, 33060/tcp
nodejs-container   nodejs-mysql-app-nodejs-service   "npm start"              nodejs-service   5 minutes ago   Up 4 minutes             0.0.0.0:3000->3000/tcp

هنا عرض لنا الـ Services الموجودة في مشروع الـ Docker Compose الحالي، مع تفاصيل عن كل Service مثل اسم الـ Container الذي تم إنشاؤه
والـ Image المستخدمة، وحالة الـ Service، واسم الـ Service، والـ Ports والـ COMMAND الذي يتم تشغيله داخل الـ Container

docker compose ps

لعرض الـ logs الخاصة بجميع الـ Services:

> docker compose logs
nodejs-container  |
nodejs-container  | > nodejs-mysql-app@1.0.0 start
nodejs-container  | > node app.js
nodejs-container  |
nodejs-container  | Connected to MySQL and products table is ready
nodejs-container  | Server is running on port 3000
mysql-container   | 2026-02-18 22:36:05+00:00 [Note] [Entrypoint]: Entrypoint script for MySQL Server 9.6.0-1.el9 started.
...
mysql-container   | 2026-02-18T22:36:07.067405Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '9.6.0'  socket: '/var/run/mysqld/mysqld.sock'  port: 3306  MySQL Community Server - GPL.

ويمكنك عرض الـ logs الخاصة بـ Service معين:

> docker compose logs nodejs-service
nodejs-container  |
nodejs-container  | > nodejs-mysql-app@1.0.0 start
nodejs-container  | > node app.js
nodejs-container  |
nodejs-container  | Connected to MySQL and products table is ready
nodejs-container  | Server is running on port 3000

ولمتابعة الـ logs بشكل مباشر يمكنك إضافة -f:

> docker compose logs -f

docker compose stop و docker compose start

لإيقاف جميع الـ Containers بدون حذفها نستخدم docker compose stop:

> docker compose stop
[+] Stopping 2/2
 ✔ Container nodejs-container  Stopped                                                                                                                                                                                                            0.9s
 ✔ Container mysql-container   Stopped                                                                                                                                                                                                            1.2s

هكذا كأننا قمنا بعمل docker container stop لكل Container في ملف الـ docker-compose.yml

ولإعادة إنشاءها:

> docker compose start
[+] Running 2/2
 ✔ Container mysql-container   Healthy                                                                                                                                                                                                            5.3s
 ✔ Container nodejs-container  Started                                                                                                                                                                                                            0.2s

هكذا كأننا قمنا بعمل docker container start لكل Container في ملف الـ docker-compose.yml

والفرق بين stop و down هو أن stop يوقف الـ Containers فقط بدون حذفها بينما down يوقف ويحذف كل شيء

لنقم بعمل docker compose down لإيقاف وحذف كل شيء لكي لا ننسى:

> docker compose down
[+] Running 3/3
 ✔ Container nodejs-container        Removed                                                                                                                                                                                                      1.0s
 ✔ Container mysql-container         Removed                                                                                                                                                                                                      1.7s
 ✔ Network nodejs-mysql-app_default  Removed                                                                                                                                                                                                      0.6s

الخلاصة

في هذه المقالة تعرفنا على أداة Docker Compose وكيف تجعل حياتنا أسهل بكثير عند التعامل مع تطبيقات تتكون من عدة Containers
بدلاً من كتابة أوامر طويلة ومتكررة في كل مرة، أصبح بإمكاننا تعريف كل شيء في ملف واحد وتشغيل المشروع بأمر واحد فقط

لقد تعلمنا في هذه المقالة:

  • لماذا نحتاج الـ Docker Compose: لأن إدارة عدة Containers يدويًا تصبح مرهقة وعرضة للأخطاء البشرية خاصة مع زيادة عدد الخدمات في المشروع
  • بنية ملف docker-compose.yml: يتكون من ثلاثة أقسام رئيسية هي services لتعريف الـ Containers، و volumes لتعريف الـ Named Volumes، و networks لتعريف شبكات مخصصة إن احتجنا
  • تعريف Service من Image جاهزة: باستخدام image: مثلما فعلنا مع MySQL
  • تعريف Service من Dockerfile: باستخدام build: مثلما فعلنا مع تطبيق Node.js
  • الـ Network التلقائية: الـ Docker Compose ينشئ Network تلقائيًا لجميع الـ Services ويربطهم بها، مما يسمح لهم بالتواصل مع بعضهم باستخدام أسماء الـ Services أو الـ Containers
  • الـ depends_on: لتحديد ترتيب بدء الـ Services وضمان أن Service معينة لا تبدأ إلا بعد أن تكون Service أخرى قد بدأت
  • الـ healthcheck: لتحديد كيفية التحقق من أن Service ما جاهزة فعليًا لاستقبال الاتصالات، واستخدامها مع depends_on مع condition: service_healthy لضمان الترتيب الصحيح
  • أوامر الـ Docker Compose الأساسية:
    • docker compose up لإنشاء وتشغيل جميع الـ Services
    • docker compose up --build لإعادة بناء الـ Images قبل التشغيل
    • docker compose down لإيقاف وحذف جميع الـ Containers والـ Networks
    • docker compose stop و docker compose start لإيقاف وتشغيل الـ Containers بدون حذفها
    • docker compose ps لعرض حالة الـ Services
    • docker compose logs لعرض الـ logs الخاصة بالـ Services

النقطة الأهم التي يجب أن تتذكرها هي أن الـ Docker Compose لا ينشئ شيئًا مختلفًا عما كنا نفعله يدويًا
بل هو فقط أداة تتيح لنا كتابة كل هذه الأوامر والإعدادات في ملف واحد منظم ثم تنفيذها جميعًا بأمر واحد
وهذا ما يجعله أداة لا غنى عنها في أي مشروع حقيقي يستخدم Docker

أرجو أن تكون قد استفدت من هذه المقالة