مقدمة عن 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 Composemysql-service: اسم الـServiceوهو ما يمثل الـContainerالخاص بالـMySQLimage: الـImageالتي سيتم استخدامها لإنشاء الـContainerوهيmysql:9.6.0في حالتناcontainer_name: اسم الـContainerالذي نريده، هذا اختياري ويمكنك تركه ليتم إنشاؤه بشكل افتراضي من قبل الـDocker Composeenvironment:: الـ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لإنشاء وتشغيل جميع الـServicesdocker compose up --buildلإعادة بناء الـImagesقبل التشغيلdocker compose downلإيقاف وحذف جميع الـContainersوالـNetworksdocker compose stopوdocker compose startلإيقاف وتشغيل الـContainersبدون حذفهاdocker compose psلعرض حالة الـServicesdocker compose logsلعرض الـlogsالخاصة بالـServices
النقطة الأهم التي يجب أن تتذكرها هي أن الـ Docker Compose لا ينشئ شيئًا مختلفًا عما كنا نفعله يدويًا
بل هو فقط أداة تتيح لنا كتابة كل هذه الأوامر والإعدادات في ملف واحد منظم ثم تنفيذها جميعًا بأمر واحد
وهذا ما يجعله أداة لا غنى عنها في أي مشروع حقيقي يستخدم Docker
أرجو أن تكون قد استفدت من هذه المقالة