مقدمة عن الـ Docker Networks
السلام عليكم ورحمة الله وبركاته
يمكنك متابعة السلسلة بالترتيب أو الانتقال مباشرة إلى أي مقال:
المقدمة
في المقالات السابقة تعرفنا على الكثير من المفاهيم الأساسية في عالم الـ Docker
مثل كيفية إنشاء Containers وإنشاءها، وكيفية التعامل مع الـ Images، وكيفية إدارة الـ Volumes وغيرها من المفاهيم المهمة
الآن حان الوقت لنتعرف على مفهوم مهم جدًا في عالم الـ Docker وهو الـ Docker Networks
وهو المفهوم الذي يتيح لنا التحكم في كيفية تواصل الـ Containers مع بعضها البعض ومع العالم الخارجي
لماذا نحتاج الـ Docker Networks ؟
تخيل معي أنك تقوم ببناء تطبيق ويب متكامل
التطبيق يتكون من عدة أجزاء:
Backendيعمل علىNode.jsأوPHPأو أي لغة أخرىDatabaseمثلMySQLأوPostgreSQLCacheمثلRedisLoad BalancerأوReverse ProxyمثلNginx
كل جزء من هذه الأجزاء سيعمل داخل Container منفصل
لكن هذه الـ Containers تحتاج أن تتواصل مع بعضها البعض
مثلًا الـ Backend يحتاج أن يتصل بالـ Database لجلب البيانات
والـ Backend يحتاج أن يتصل بالـ Redis للتعامل مع الـ Cache
والـ Nginx يحتاج أن يوجه الطلبات إلى الـ Backend وهكذا
قلنا أن كل Container معزول عن الآخر بشكل افتراضي، لذا السؤال هنا هو كيف ستتواصل هذه الـ Containers مع بعضها البعض ؟
هنا يأتي دور الـ Docker Networks
الـ Docker Networks تتيح لنا إنشاء شبكات افتراضية تربط الـ Containers ببعضها البعض
بحيث يمكن للـ Containers التواصل فيما بينها بسهولة وأمان
وهذه الشبكات يمكنها أن تكون معزولة عن الشبكة الخارجية أو متصلة بها حسب الحاجة
بالتالي يمكنك ربط أكثر من Container بنفس الـ Network ليتمكنوا من التواصل فيما بينهم بشكل معزول تمامًا عن العالم الخارجي
أو يمكنك فتح الـ Network للاتصال بالإنترنت إذا كان الـ Container يحتاج ذلك
أنواع الـ Docker Networks
الـ Docker يوفر عدة أنواع من الـ Networks التي يمكن استخدامها حسب الحاجة
وDocker ينشئ تلقائيًا بعض الـ Networks الافتراضية عند تثبيته على جهازك
بالتالي عندما نقوم بتنفيذ الأمر docker network list سنرى هذه الـ Networks الافتراضية:
> docker network list
NETWORK ID NAME DRIVER SCOPE
e615c8873cae bridge bridge local
442dc5fb41fb host host local
f17bf1fc9768 none null local
ستلاحظ أن هناك ثلاث Networks افتراضية وهى bridge و host و none
وهى مجردأسماء لأنواع مختلفة من الـ Networks التي يوفرها الـ Docker
بالإضافة إلى أنه يمكننا إنشاء User Defined Networks خاصة بنا بالطبع
عليك أن تفرق بين NAME و DRIVER
بحيث أن الـ NAME هو مجرد اسم ويمكننه أن يتغير
لكن ما يهمنا هنا هو الـ DRIVER، فهو نوع الـ Network وحين نتحدث عن الـ bridge أو الـ host فنحن نشير إلى الـ DRIVER الخاص بالـ Network وليس اسمه
على أي حال، الـ Docker يوفر لنا عدة أنواع من الـ Drivers للـ Networks
وكل نوع من هذه الأنواع له خصائصه واستخداماته المختلفة
وهذه الأنواع هي:
Bridge DriverHost DriverNone DriverOverlay DriverIpvlan DriverMacvlan Driver
في هذا المقال سنركز فقط على الـ Bridge Driver وخصوصًا الـ User Defined Networks التي يمكننا إنشاؤها بأنفسنا والتي تكون من نوع Bridge Driver بشكل افتراضي
وسنعطي نبذة فقط عن الـ Host Driver و None Driver
أما الأنواع الأخرى مثل Overlay Driver و Ipvlan Driver و Macvlan Driver فلن نتطرق لها في هذا المقالة
على أي حال، لنشرح كل نوع من أنواع الـ Drivers هذه بالتفصيل:
Bridge Driver
أي Network يستخدم الـ Bridge Driver يسمى بـ Bridge Network
والـ Bridge Network تعمل كجسر يربط الـ Containers ببعضها البعض
الـ Containers المتصلة بنفس الـ Bridge Network يمكنها التواصل مع بعضها البعض
وهو النوع الافتراضي للـ Networks في الـ Docker
وعندما تقوم بإنشاء Container بدون تحديد Network معينة
فإنه يتصل تلقائيًا بالـ Bridge Network الافتراضية التي ينشئها الـ Docker
لو هناك مجموعة من الـ Containers متصلة بنفس الـ Bridge Network
فهذه الـ Containers يمكنها التواصل مع بعضها البعض داخل هذه الـ Network
ولكنها لا يمكنها التواصل مع الـ Containers المتصلة بشبكات أخرى ولا مع جهازك إلا إذا قمت بعمل Port Mapping كما تعلمنا في المقالات السابقة
يمكننا أيضًا إنشاء Bridge Network مخصصة لنا كما سنرى لاحقًا والتي تسمى بـ User Defined Networks وهي من نوع Bridge Driver بشكل افتراضي
ويفضل دايمًا استخدام User Defined Networks بدلاً من الـ Bridge Network الافتراضية لأنها توفر ميزات أفضل مثل إمكانية التواصل بين الـ Containers باستخدام الأسماء بدلاً من الـ IP Addresses وغيرها من الميزات التي تجعلها أكثر مرونة وسهولة في الاستخدام
Host Driver
أي Network يستخدم الـ Host Driver يسمى بـ Host Network
في هذا النوع، الـ Container سيستخدم نفس الـ Network الخاصة بجهازك مباشرة
بمعنى أنه أي Container متصل بـ Host Network لن يكون هناك عزل بين الـ Container وجهازك الشخصي من حيث الشبكة
الـ Container يعمل كأنه تطبيق عادي على جهازك متصل بنفس الشبكة الخاصة بجهازك بشكل مباشر
هذا النوع مفيد في حالات معينة حيث تحتاج إلى أداء عالي جدًا في الشبكة
ولكنه يقلل من العزل بين الـ Container وجهازك، لذا يجب استخدامه بحذر
Null Driver
أي Network يستخدم الـ Null Driver يسمى بـ None Network
وأحياناً يسمى بـ None Driver
هذا النوع يعني أن الـ Container لا يتصل بأي Network على الإطلاق
الـ Container يكون معزولًا تمامًا عن أي شبكة
لا يمكنه التواصل مع الـ Containers الأخرى ولا مع العالم الخارجي
هذا النوع يستخدم في حالة إنك تريد Container يعيش داخل كهف مظلم فوق جبال الهيمالايا
في عزلة تامة بدون إنترنت أو تواصل مع أي شيء
User Defined Networks
يمكننا بالطبع إنشاء Network مخصصة لنا ولمشاريعنا
وعندما نقوم بإنشاء Network جديدة ستكون من نوع Bridge Network بشكل افتراضي
وطالما أنها Bridge Network بالتالي المتصلة Containers المتصلة بنفس الـ Network يمكنها التواصل ببعضها
وكما قلنا، فلا يفضل استخدام الـ Bridge Network الافتراضية التي ينشئها الـ Docker
بل يستحسن دائمًا إنشاء User Defined Networks خاصة بنا لكل مشروع أو لكل مجموعة من الـ Containers التي نريد أن تتواصل مع بعضها البعض
الميزة هنا أننا يمكننا إنشاء أي عدد من الـ Networks كما نريد
ونربط الـ Containers المختلفة بكل Network حسب الحاجة
وهذا يمنحنا مرونة كبيرة في تنظيم كيفية تواصل الـ Containers مع بعضها البعض
تجهيز المشروع للمثال العملي
سيكون لدينا مشروع بسيط يتكون من Backend يعمل على Node.js و Database تعمل على MySQL
لذا أولًا سنقوم بإنشاء Container للـ MySQL ثم Container للـ Node.js ونجعل الـ Node.js يتصل بالـ MySQL من خلال الـ Network التي سننشئها لاحقًا
إنشاء Container للـ MySQL
أولًا لنقم بإنشاء Container للـ MySQL:
> docker container run -d --rm --name mysql-container -p 3306:3306 -e MYSQL_ROOT_PASSWORD=tabarani-very-secret -e MYSQL_DATABASE=tabarani-app -v mysql-tabarani-db:/var/lib/mysql mysql:9.6.0
1a60ca4ea14c77ce61a8d3d40065083de1a9d30a0896f9708e13d407669140b8
لاحظ أننا عرفنا volume باسم mysql-tabarani-db ليمثل الـ Volume الذي سيحتوي على بيانات الـ MySQL
وحددنا الـ MYSQL_ROOT_PASSWORD ليكون tabarani-very-secret والـ MYSQL_DATABASE ليكون tabarani-app
وفتحنا الـ Port رقم 3306 ليمكننا الاتصال بالـ MySQL من الـ Node.js لاحقًا
لاكن لاحقًا قد لا نحتاج لفتح الـ Port الخاص بالـ MySQL عندما نربطه بالـ Node.js من خلال الـ Network لأننا سنتمكن من الاتصال بالـ MySQL من داخل الـ Network بدون الحاجة لفتح الـ Port للخارج
لكننا كما قلنا فالـ Port Mapping يفتح الـ Port لنتمكن من الاتصال بالـ Container من جهازنا الشخصي أو من خارج الـ Docker بشكل عام
لكن عندما نربط الـ Container الخاص بالـ Node.js بالـ Container الخاص بالـ MySQL من خلال الـ Network فلن نحتاج لفتح الـ Port للـ MySQL لأن الاتصال سيكون داخل الـ Network وليس من خارج الـ Docker وسنرى هذا بشكل عملي لاحقًا
على أي حال، لنرى قائمة الـ Containers للتأكد أن الـ Container الخاص بـ MySQL يعمل:
> docker container list
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
01efe527ccf0 mysql:9.6.0 "docker-entrypoint.s…" 48 seconds ago Up 42 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp mysql-container
الآن لدينا Container يعمل عليه MySQL
الخطوة التالية هي إنشاء Container للـ Node.js
تجهيز Server الـ 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=localhost
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 في هذه المرحلة
كل ما عليك أن تعلمه أننا لدينا تطبيق بسيط جدًا يعمل على Node.js ويتصل بقاعدة بيانات MySQL من خلال إعدادات الاتصال الموجودة في ملف الـ .env
ولدينا GET /products تقوم بجلب جميع المنتجات من قاعدة البيانات و POST /products تقوم بإضافة منتج جديد إلى قاعدة البيانات
بناء الـ Image الخاصة بـ Server الـ Node.js
الآن لنقم ببناء الـ Image الخاص بالـ Node.js:
> docker image build -t nodejs-mysql-app:v1.0 .
...
=> [1/5] FROM docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997 0.0s
=> => resolve docker.io/library/node:25-alpine@sha256:c8d96e95e88f08f814af06415db9cfd5ab4ebcdf40721327ff2172ff25cfb997 0.0s
=> [internal] load build context 0.0s
=> => transferring context: 34.33kB 0.0s
=> CACHED [2/5] WORKDIR /app 0.0s
=> [3/5] COPY package*.json . 0.0s
=> [4/5] RUN npm install 3.0s
=> [5/5] COPY . . 0.1s
...
الآن لدينا Image تم بناؤها بنجاح باسم nodejs-mysql-app:v1.0
محاولة إنشاء الـ Container بدون Network
الآن قبل أن ننشيء Container من هذه الـ Image
دعنا نراجع إعدادات الاتصال بالـ MySQL الموجودة في ملف الـ .env الخاص بنا
# APP Port
PORT=3000
# Database configuration
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=tabarani-very-secret
DB_NAME=tabarani-app
لاحظ أن DB_HOST هو localhost
وهذا يعني أن الـ Container الخاص بالـ Node.js والذي سنسميه nodejs-container سيحاول الاتصال بالـ Container الخاص بالـ MySQL التى أسمينها mysql-container من خلال localhost الخاصة بالـ nodejs-container
لكن بديهيًا أن الـ localhost داخل الـ nodejs-container لا يشير إلى الـ localhost الخاص بالـ mysql-container ولا يشير إلى الـ localhost الخاص بجهازك الشخصي
لذا إذا قمنا بإنشاء الـ Container الخاص بالـ Node.js فسنحصل على خطأ في الاتصال بالـ MySQL
> docker container run --rm --name nodejs-container --env-file .env -p 3000:3000 nodejs-mysql-app:v1.0
> nodejs-mysql-app@1.0.0 start
> node app.js
/app/node_modules/mysql2/promise.js:19
const createConnectionErr = new Error();
^
Error
at Object.createConnectionPromise [as createConnection] (/app/node_modules/mysql2/promise.js:19:31)
at connectDB (/app/app.js:18:28)
at Object.<anonymous> (/app/app.js:45:1)
at Module._compile (node:internal/modules/cjs/loader:1809:14)
at Object..js (node:internal/modules/cjs/loader:1940:10)
at Module.load (node:internal/modules/cjs/loader:1530:32)
at Module._load (node:internal/modules/cjs/loader:1332:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:255:19)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)
at node:internal/main/run_main_module:33:47 {
code: 'ECONNREFUSED',
errno: undefined,
sqlState: undefined
}
Node.js v25.6.0
هذا لأن الـ Node.js حاول الاتصال بالـ localhost:3306 داخل الـ Container الخاص به nodejs-container
لا يوجد MySQL يعمل على localhost:3306 داخل الـ nodejs-container لذا تم رفض الاتصال
بل أن الـ MySQL يعمل داخل الـ Container الخاص به mysql-container على localhost:3306 داخل mysql-container وليس داخل nodejs-container
ولاحظ أيضًا أننا في mysql-container قمنا بعمل Port Mapping للـ MySQL بحيث أن الـ Port رقم 3306 الخاص بالـ MySQL داخل الـ Container تم فتحه على نفس الـ Port رقم 3306 في جهازك الشخصي
الـ Port Mapping هذا يتيح لنا الاتصال بالـ MySQL من جهازنا الشخصي من خلال localhost:3306
لكن هذا لا يعني أن الـ Node.js داخل الـ Container يمكنه الاتصال بالـ MySQL من خلال localhost:3306 لأن الـ localhost داخل الـ Container لا يشير إلى جهازك الشخصي
+-------------------------------------------------------------------+
| Your Device |
| |
| localhost:3000 ---+ +--- localhost:3306 |
| | | |
| Port Mapping Port Mapping |
| | | |
| +----------------|---------------------------|----------------+ |
| | | Docker Engine | | |
| | V V | |
| | +----------------------+ +----------------------+ | |
| | | nodejs-container | | mysql-container | | |
| | | running on port 3000 | | running on port 3306 | | |
| | | | +----------------------+ | |
| | | tries to connect | | |
| | | to port 3306 | | |
| | | inside itself | | |
| | +----------------------+ | |
| | | |
| +-------------------------------------------------------------+ |
| |
+-------------------------------------------------------------------+
هذا رسم يوضح الوضع الحالي الذي نحن فيه
الـ Node.js داخل الـ nodejs-container يحاول الوصول إلى الـ MySQL من خلال localhost:3306 داخل نفسه
لكن لا يوجد MySQL يعمل داخل الـ nodejs-container لذا يتم رفض الاتصال
استخدام الـ Bridge Network الافتراضية
الآن دعنا نحاول حل هذه المشكلة باستخدام الـ Bridge Network التي يوفرها Docker بشكل افتراضي
قلنا أن الـ Docker ينشئ تلقائيًا Bridge Network افتراضية عند تثبيته على جهازك
بالتالي عندما تقوم بإنشاء Container بدون تحديد Network معينة فإن Docker يقوم تلقائيًا بتوصيل الـ Container بالـ Bridge Network الافتراضية
هذه الـ Network تسمح للـ Containers بالاتصال ببعضها البعض باستخدام الـ IP Address الخاص بكل Container
لكن المشكلة هنا أن الـ Bridge Network الافتراضية لا تدعم الـ DNS Resolution أي أنك لا تستطيع الاتصال بالـ Container باستخدام اسمها مباشرة
يجب عليك معرفة الـ IP Address الخاص بالـ Container للاتصال به
دعنا نجرب ذلك
أولاً سنحتاج إلى معرفة الـ IP Address الخاص بالـ mysql-container
> docker container inspect mysql-container
[
{
...
"NetworkSettings": {
...
"Ports": {
"3306/tcp": [
{
"HostIp": "0.0.0.0",
"HostPort": "3306"
}
],
"33060/tcp": null
},
...
"IPAddress": "172.17.0.2",
...
},
...
}
]
سنجد معلومات كثيرة كالعادة ومن ضمنها الـ IPAddress الخاص بالـ mysql-container وهو 172.17.0.2
عليك الانتباه أن هذا الـ IP Address خاص بالـ mysql-container داخل الـ Bridge Network الافتراضية التي ينشئها الـ Docker
وكل Container داخل هذه الـ Network الافتراضية يحصل على IP Address خاص به داخل هذه الـ Network
وأيضًا هذا الـ IP Address قد يتغير في كل مرة تقوم فيها بإعادة إنشاء الـ mysql-container أو إعادة إنشاء الـ Docker
الآن دعنا نعدل ملف الـ .env ليستخدم هذا الـ IP Address بدلاً من localhost
# APP Port
PORT=3000
# Database configuration
DB_HOST=172.17.0.2
DB_PORT=3306
DB_USER=root
DB_PASSWORD=tabarani-very-secret
DB_NAME=tabarani-app
قمنا فقط بتغيير DB_HOST ليكون 172.17.0.2 بدلاً من localhost
الآن دعنا نحاول إنشاء الـ nodejs-container مرة أخرى
> docker container run --rm --name nodejs-container --env-file .env -p 3000:3000 nodejs-mysql-app:v1.0
> nodejs-mysql-app@1.0.0 start
> node app.js
Connected to MySQL and products table is ready
Server is running on port 3000
لاحظ أن الآن نجح الاتصال بالـ MySQL ولاحظ أن الـ nodejs-container يعمل بشكل طبيعي في الـ Foreground بسبب عدم استخدام -d عند إنشاءه
الآن الـ Node.js المتواجد داخل الـ nodejs-container نجح بالاتصال بالـ MySQL من خلال الـ IP Address الخاص بالـ mysql-container
ولأنهما متصلين بنفس الـ Bridge Network الافتراضية التي ينشئها الـ Docker فإنهما يستطيعان التواصل مع بعضهما البعض باستخدام الـ IP Address الخاص بكل منهما داخل هذه الـ Network
+-------------------------------------------------------------------+
| Your Device |
| |
| localhost:3000 ---+ +--- localhost:3306 |
| | | |
| Port Mapping Port Mapping |
| | | |
| +----------------|---------------------------|----------------+ |
| | | Default Bridge Network | | |
| | Container V V Container | |
| | +----------------------+ +----------------------+ | |
| | | nodejs-container | | mysql-container | | |
| | | IP: 172.17.0.3 | ======> | IP: 172.17.0.2 | | |
| | | Port: 3000 | | Port: 3306 | | |
| | | | | | | |
| | | connects to | | MySQL Server | | |
| | | 172.17.0.2:3306 | | | | |
| | +----------------------+ +----------------------+ | |
| | | |
| +-------------------------------------------------------------+ |
| |
+-------------------------------------------------------------------+
الآن الـ Node.js يتصل بالـ MySQL من خلال الـ IP Address 172.17.0.2 داخل الـ Bridge Network الافتراضية
لكن هذا الحل به مشكلة كبيرة
ماذا لو قمنا بحذف الـ mysql-container وإعادة إنشائها من جديد ؟
في هذه الحالة سيتغير الـ IP Address الخاص بالـ mysql-container داخل الـ Bridge Network الافتراضية
بالتالي فسوف يفقد الـ Node.js الاتصال بالـ MySQL لأنه يعتمد على الـ IP Address، فالطالما تغير الـ IP Address فإن الاتصال سيفشل
لنقم بتجربة ذلك
أولًا أبقي الـ nodejs-container يعمل في الـ Foreground حتى نرى النتيجة مباشرة
تذكر أن الـ IP Address الخاص بالـ mysql-container الحالي هو 172.17.0.2
والـ IP Address الخاص بالـ nodejs-container الحالي هو 172.17.0.3 بسبب أنه تم إنشاؤه بعد الـ mysql-container داخل نفس الـ Bridge Network الافتراضية التي ينشئها الـ Docker ويمكنك أن ترى ذلك من خلال تنفيذ الأمر docker container inspect nodejs-container والبحث عن الـ IPAddress داخل الـ NetworkSettings
على أي حال، لنقم بإيقاف وحذف الـ mysql-container الحالي
> docker container stop mysql-container
mysql-container
الآن لا يوجد Container يحجز الـ IP Address 172.17.0.2 لذا إذا قمنا بإنشاء Container جديد للـ MySQL فإنه سيحصل على نفس الـ IP Address السابق
لذا سنقوم بإنشاء Container عشوائية حتى تحجز الـ IP Address 172.17.0.2
> docker container run -it -d --name temp-nodejs-container node:25-alpine
ab846455adb7945f2b7c6f56064a172f9b05c428600bd37758f2202b3d25e2e9
قنا بإنشاء Container عشوائية باسم temp-nodejs-container من node:25-alpine بشكل مباشر وجعلناها -it حتى تظل تعمل في الخلفية
الفكرة من هذا الأمر أن الـ temp-nodejs-container سيحجز الـ IP Address 172.17.0.2 داخل الـ Bridge Network الافتراضية التي ينشئها الـ Docker
الآن إذا قمنا بإنشاء Container جديد للـ MySQL فإنه سيحصل على الـ IP Address التالي وهو 172.17.0.3
الآن سنقوم بإنشاء mysql-container جديد بنفس الإعدادات السابقة
> docker container run -d --rm --name mysql-container -p 3306:3306 -e MYSQL_ROOT_PASSWORD=tabarani-very-secret -e MYSQL_DATABASE=tabarani-app -v mysql-tabarani-db:/var/lib/mysql mysql:9.6.0
31ba2ffe1dd84bdc09e16e7c89b1359846573b231a3939aed4599f328f17b611
الآن دعنا نتحقق من الـ IP Address الخاص بالـ mysql-container الجديد
> docker container inspect mysql-container
[
{
...
"NetworkSettings": {
...
"IPAddress": "172.17.0.4",
...
},
...
}
]
الآن الـ mysql-container الجديد حصل على IP Address جديد وهو 172.17.0.4
لذا لا يمكننا الاعتماد على الـ IP Address في الاتصال بين الـ Containers داخل الـ Bridge Network الافتراضية أو داخل أي Network بشكل عام
ولا نستطيع الاتصال بالـ Container باستخدام اسمه مباشرة لأننا قلنا أن الـ Bridge Network الافتراضية لا تدعم الـ DNS Resolution
لذا الحل الأفضل هو إنشاء Bridge Network بحيث أن Docker يوفر لنا ميزة الـ DNS Resolution داخل هذه الـ Network
بمعنى أن الـ Bridge Network الافتراضية لا تدعم الـ DNS Resolution لكن إذا قمنا بإنشاء Bridge Network خاصة بنا فإنها ستدعم الـ DNS Resolution
والـ DNS Resolution تعني أنه يمكننا الاتصال بالـ Container باستخدام اسمه مباشرة بدلاً من الـ IP Address
فمثلاً بدلاً من استخدام 172.17.0.2 يمكننا استخدام mysql-container مباشرة
لنقم بايقاف وحذف الـ temp-nodejs-container العشوائية التي أنشأناها
> docker container remove -f temp-nodejs-container
temp-nodejs-container
لنقف أيضًا الـ nodejs-container ونحذفه حتى نعيد إنشائه لاحقًا على الـ Network الجديدة التي سننشئها
طالما الـ nodejs-container يعمل في الـ Foreground فقط قم بالضغط CTRL + C لإيقافه وسيتم حذفه تلقائيًا بسبب استخدام --rm
وأخيرًا قم بحذف الـ mysql-container الحالي حتى نعيد إنشائه لاحقًا على الـ Network الجديدة التي سننشئها
> docker container remove -f mysql-container
mysql-container
أوامر الـ Docker Networks
يوجد عدة أوامر مهمة للتعامل مع الـ Docker Networks وتستطيع معرفتها من خلال تنفيذ الأمر docker network --help
لكن نحن سنركز على الأوامر الأساسية التي سنستخدمها بشكل عملي
وهى كالتالي:
docker network list: لعرض جميع الـNetworksالموجودة على الجهازdocker network create <name>: لإنشاءNetworkجديدةdocker network inspect <name>: لعرض تفاصيلNetworkمعينةdocker network connect <network> <container>: لربطContainerبـNetworkمعينةdocker network disconnect <network> <container>: لفصلContainerمنNetworkمعينةdocker network remove <name>: لحذفNetwork
لقد تعرفنا عل أمر docker network list في بداية المقالة عندما قمنا بعرض الـ Networks الافتراضية التي ينشئها الـ Docker
إنشاء User Defined Network
الحل الأفضل والأكثر عملية هو إنشاء User Defined Network خاصة بنا
الـ User Defined Network توفر لنا ميزة رائعة وهي الـ DNS Resolution بشكل افتراضي داخل هذه الـ Network
هذا يعني أننا نستطيع الاتصال بالـ Container باستخدام اسمها مباشرة بدلاً من الـ IP Address
فمثلاً بدلاً من استخدام 172.17.0.2 يمكننا استخدام mysql-container مباشرة
دعنا ننشئ User Defined Network باسم tabarani-network
> docker network create tabarani-network
a8180b142dab3ff99a9b2768638911e0c777616b570d52423d609f259a03152b
الآن دعنا نتأكد من أن الـ Network تم إنشاؤها
> docker network ls
NETWORK ID NAME DRIVER SCOPE
a8180b142dab tabarani-network bridge local
b1c2d3e4f5a6 bridge bridge local
c7d8e9f0a1b2 host host local
d3e4f5a6b7c8 none null local
كما ترى الآن لدينا Network جديدة باسم tabarani-network من نوع bridge
لاحظ أنها من نوع bridge لأن الـ User Defined Networks تكون من نوع Bridge Driver بشكل افتراضي
ويمكنك إنشاء Network من نوع Host Driver أو Null Driver باستخدام --driver عند إنشاء الـ Network ثم كتابة نوع الـ Driver الذي تريده
مثل docker network create --driver host my-host-network لإنشاء Host Network جديدة باسم my-host-network لكن في حالتنا نحن نريد Bridge Network لذا سنتركها من نوع Bridge Driver بشكل افتراضي
الآن سنقوم بإنشاء mysql-container جديد متصل بالـ tabarani-network عن طريق استخدام --network عند إنشاء الـ Container وتحديد اسم الـ Network الذ1ي نريده
> 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
ee7e4d7be82d61c750eff85d93ae47d1824edafae6b64bbef19a13207cca35d5
لاحظ أنه نفس الأمر السابق لكن أضفنا --network tabarani-network لربط الـ mysql-container بالـ tabarani-network التي أنشأناها
وأيضًا لقد تخلصنا من -p 3306:3306 لأننا لن نحتاج لفتح الـ Port الخاص بالـ MySQL للخارج بل كل شيء سيتم داخل الـ tabarani-network
الآن دعنا نعدل ملف الـ .env الخاص بمشروع الـ Node.js ليستخدم اسم الـ Container الخاص بالـ MySQL بدلاً من الـ IP Address
# 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
كل ما قمنا به هو تغيير DB_HOST ليكون mysql-container بدلاً من localhost أو IP Address
الآن يمكننا إنشاء الـ nodejs-container على نفس الـ Network وسترى أن الاتصال بالـ MySQL سيتم بنجاح باستخدام اسم الـ Container مباشرة
> docker container run --rm --name nodejs-container --network tabarani-network --env-file .env -p 3000:3000 nodejs-mysql-app:v1.0
> nodejs-mysql-app@1.0.0 start
> node app.js
Connected to MySQL and products table is ready
Server is running on port 3000
وكما توقعنا فقد نجح الاتصال بالـ MySQL باستخدام اسم الـ Container الخاص به mysql-container بدلاً من الـ IP Address
وحتى لو قمنا بحذف الـ mysql-container وإعادة إنشائه مرة أخرى وحصل على IP Address جديد داخل الـ tabarani-network فإن هذا لن يؤثر على الاتصال بين الـ Node.js والـ MySQL لأنهما يتصلان ببعضهما البعض باستخدام أسماء الـ Containers وليس الـ IP Addresses
+-------------------------------------------------------------------+
| Your Device |
| |
| localhost:3000 ---+ |
| | |
| Port Mapping |
| | |
| +----------------|--------------------------------------------+ |
| | | tabarani-network (Custom Bridge) | |
| | | with DNS Resolution | |
| | V | |
| | +----------------------+ +----------------------+ | |
| | | nodejs-container | | mysql-container | | |
| | | IP: 172.18.0.3 | ------> | IP: 172.18.0.4 | | |
| | | Port: 3000 | DNS | Port: 3306 | | |
| | | | | | | |
| | | connects using name: | | MySQL Server | | |
| | | mysql-container:3306 | | | | |
| | +----------------------+ +----------------------+ | |
| | | |
| +-------------------------------------------------------------+ |
| |
+-------------------------------------------------------------------+
هذا بالطبع رسم تخيلي يوضح كيف أن الـ nodejs-container يتصل بالـ mysql-container باستخدام اسمه مباشرة داخل الـ tabarani-network
والـ DNS Resolution داخل الـ tabarani-network سيقوم بترجمة اسم الـ Container وهو mysql-container إلى الـ IP Address الخاص به داخل هذه الـ Network ليتم الاتصال به بنجاح
مهما تغير الـ IP Address الخاص بالـ mysql-container داخل الـ tabarani-network فإن الاتصال بين الـ Node.js والـ MySQL سيظل يعمل بشكل طبيعي طالما أنهما متصلان بنفس الـ Network ويستخدمان أسماء الـ Containers للاتصال ببعضهما البعض
لاحظ أننا لغينا الـ Port Mapping الخاص بالـ MySQL لأننا لا نحتاج لفتح الـ Port للخارج بل كل شيء يتم داخل الـ tabarani-network
لكن بالطبع نحتاج لفتح الـ Port الخاص بالـ Node.js للخارج حتى نتمكن من الوصول إلى الـ Node.js من جهازنا الشخصي من خلال localhost:3000
لأنه API Server في الأصل
الآن الـ nodejs-container يعمل في الـ Foreground بسبب أننا لم نستخدم -d عند إنشاءه
لذا سنقوم بالضغط CTRL + C لإيقافه وسيتم حذفه تلقائيًا بسبب استخدام --rm
ثم نعيد إنشاءه مرة أخرى لكن هذه المرة سنستخدم -d لجعله يعمل في الخلفية
> docker container run -d --rm --name nodejs-container --network tabarani-network --env-file .env -p 3000:3000 nodejs-mysql-app:v1.0
97bbffa663e9bc575c7fda8d91cd67bf6fd1bbb6af463a23a70d02a3b4d8b5c7
docker network inspect
لكي نرى تفاصيل أكثر عن الـ Network التي أنشأناها يمكننا استخدام الأمر docker network inspect ثم نمرر له اسم الـ Network
> docker network inspect tabarani-network
[
{
"Name": "tabarani-network",
...
"Driver": "bridge",
...
"Containers": {
"97bbffa663e9bc575c7fda8d91cd67bf6fd1bbb6af463a23a70d02a3b4d8b5c7": {
"Name": "nodejs-container",
"EndpointID": "d3e5ef8ea8c732eba665082af78db563b9c90f556bfa441978b13cc4f8d8cd81",
"MacAddress": "46:08:9c:03:cf:86",
"IPv4Address": "172.20.0.3/16",
"IPv6Address": ""
},
"ee7e4d7be82d61c750eff85d93ae47d1824edafae6b64bbef19a13207cca35d5": {
"Name": "mysql-container",
"EndpointID": "17737fbc5f1d735830be68a20221dc9e06e6cad222ff962da80bcd94566a63a5",
"MacAddress": "ca:0f:5f:61:e3:c3",
"IPv4Address": "172.20.0.2/16",
"IPv6Address": ""
}
},
...
}
]
النتيجة تعطيك معلومات مفصلة عن الـ Network بالطبع
وستجد ضمن هذه المعلومات قسم خاص بالـ Containers المتصلة بهذه الـ Network
وستجد أن الـ nodejs-container والـ mysql-container متصلان بهذه الـ Network مع الـ IP Address الخاص بكل منهما داخل هذه الـ Network
والـ IP Address كما قلنا فأنه ليس ثابت ويتغير بحسبب ترتيب إنشاء الـ Containers داخل هذه الـ Network
لهذا السبب أول Container يحصل على 172.20.0.2 والثاني يحصل على 172.20.0.3 والثالث يحصل على 172.20.0.4 وهكذا
docker network disconnect
إذا أردنا فصل Container معين من Network معينة
يمكننا استخدام الأمر docker network disconnect متبوعًا باسم الـ Network ثم اسم الـ Container
لنقم بفصل الـ nodejs-container من الـ tabarani-network:
> docker network disconnect tabarani-network nodejs-container
الآن لنفحص الـ Network مرة أخرى لنتأكد من أن الـ Container قد تم فصله:
> docker network inspect tabarani-network
[
{
"Name": "tabarani-network",
...
"Driver": "bridge",
...
"Containers": {
"ee7e4d7be82d61c750eff85d93ae47d1824edafae6b64bbef19a13207cca35d5": {
"Name": "mysql-container",
"EndpointID": "17737fbc5f1d735830be68a20221dc9e06e6cad222ff962da80bcd94566a63a5",
"MacAddress": "ca:0f:5f:61:e3:c3",
"IPv4Address": "172.20.0.2/16",
"IPv6Address": ""
}
},
...
}
]
كما ترى الآن الـ nodejs-container غير موجود في قائمة الـ Containers المتصلة بالـ tabarani-network
بالتالي إنقطع الاتصال بين الـ Node.js والـ MySQL لأنهما لم يعودا متصلين بنفس الـ Network
docker network connect
لكي نعيد الاتصال بين nodejs-container والـ tabarani-network مرة أخرى
يمكننا استخدام الأمر docker network connect متبوعًا باسم الـ Network ثم اسم الـ Container الذي نريد ربطه بهذه الـ Network
> docker network connect tabarani-network nodejs-container
هكذا تم ربط الـ nodejs-container مرة أخرى بالـ tabarani-network
لنتحقق من ذلك من خلال فحص الـ Network مرة أخرى:
> docker network inspect tabarani-network
[
{
"Name": "tabarani-network",
...
"Driver": "bridge",
...
"Containers": {
"97bbffa663e9bc575c7fda8d91cd67bf6fd1bbb6af463a23a70d02a3b4d8b5c7": {
"Name": "nodejs-container",
"EndpointID": "d3e5ef8ea8c732eba665082af78db563b9c90f556bfa441978b13cc4f8d8cd81",
"MacAddress": "46:08:9c:03:cf:86",
"IPv4Address": "172.20.0.3/16",
"IPv6Address": ""
},
"ee7e4d7be82d61c750eff85d93ae47d1824edafae6b64bbef19a13207cca35d5": {
"Name": "mysql-container",
"EndpointID": "17737fbc5f1d735830be68a20221dc9e06e6cad222ff962da80bcd94566a63a5",
"MacAddress": "ca:0f:5f:61:e3:c3",
"IPv4Address": "172.20.0.2/16",
"IPv6Address": ""
}
},
...
}
]
هكذا يمكننا ربط أي Container بأي Network في أي وقت نريد ويمكننا أيضًا فصل أي Container من أي Network في أي وقت نريد
وهذا يعطيك مرونة كبيرة في إدارة الاتصالات بين الـ Containers داخل الـ Docker
docker network remove
إذا أردنا حذف Network معينة لم نعد بحاجة إليها
يمكننا استخدام الأمر docker network remove متبوعًا باسم الـ Network التي نريد حذفها
بعد التأكد من أن لا يوجد أي Container متصل بهذه الـ Network
لننشيء Network جديدة ثم نحذفها:
> docker network create temp-network
6d95accb3ce7d080e22bc564d5da6176be3cdaa33cdc5023ac723d125c969b42
لنتأكد بأن الـ Network تم إنشاؤها:
> docker network list
NETWORK ID NAME DRIVER SCOPE
e615c8873cae bridge bridge local
442dc5fb41fb host host local
f17bf1fc9768 none null local
a8180b142dab tabarani-network bridge local
6d95accb3ce7 temp-network bridge local
الآن لنحذف الـ temp-network:
> docker network remove temp-network
temp-network
لنتحقق من أن الـ Network قد تم حذفها:
> docker network list
NETWORK ID NAME DRIVER SCOPE
e615c8873cae bridge bridge local
442dc5fb41fb host host local
f17bf1fc9768 none null local
a8180b142dab tabarani-network bridge local
الآن تم حذف الـ Network بنجاح
بالطبع يمكنك حذف الـ tabarani-network إذا أردت ولكن تأكد أولًا من أن جميع الـ Containers المتصلة بها قد تم فصلها أو حذفها
لأنك لا تستطيع حذف Network إذا كان هناك أي Container متصل بها
> docker network remove tabarani-network
Error response from daemon: error while removing network: network tabarani-network id a8180b142dab3ff99a9b2768638911e0c777616b570d52423d609f259a03152b has active endpoints
exit status 1
كما ترى في الرسالة السابقة لا يمكن حذف الـ tabarani-network لأن هناك Containers متصلة بها
لذا يجب علينا أولًا فصل الـ Containers المتصلة بها أو حذفها ثم بعد ذلك يمكننا حذف الـ Network بدون أي مشكلة
لنقم بحذف كلا من الـ nodejs-container والـ mysql-container أولًا
> docker container remove -f nodejs-container mysql-container
nodejs-container
mysql-container
الآن بعد أن تم حذف الـ Containers المتصلة بالـ tabarani-network يمكننا حذف الـ Network نفسها
> docker network remove tabarani-network
tabarani-network
الآن تم حذف الـ tabarani-network بنجاح
الخلاصة
في هذا المقال تعرفنا على مفهوم مهم جدًا في عالم الـ Docker وهو الـ Docker Networks
وهذا المفهوم يعتبر من الأساسيات التي يجب على أي مطور يعمل مع الـ Docker أن يتقنها
لقد تعلمنا في هذا المقال:
- لماذا نحتاج الـ
Docker Networks: لأن الـContainersمعزولة افتراضيًا عن بعضها البعض، والـDocker Networksتتيح لها التواصل مع بعضها البعض ومع العالم الخارجي - أنواع الـ
Docker Networks: تعرفنا على عدة أنواع من الـNetworksمثلBridge DriverوHost DriverوNone Driver، وركزنا على الـBridge Driverلأنه الأكثر استخدامًا - مشكلة الـ
Bridge Networkالافتراضية: شرحنا أن الـBridge Networkالافتراضية التي ينشئها الـDockerلا تدعم الـDNS Resolution، مما يعني أننا نحتاج لاستخدام الـIP Addressesللاتصال بين الـContainers، وهذا غير عملي لأن الـIP Addressesقد تتغير - الحل باستخدام
User Defined Networks: شرحنا أن إنشاءUser Defined Networksخاصة بنا يوفر ميزة الـDNS Resolutionبشكل افتراضي، مما يتيح لنا الاتصال بالـContainersباستخدام أسمائها مباشرة بدلاً من الـIP Addresses - أوامر الـ
Docker Networksالأساسية: شرحنا كيفية استخدام الأوامر التالية:docker network listلعرض جميع الـNetworksdocker network createلإنشاءNetworkجديدةdocker network inspectلعرض تفاصيلNetworkمعينةdocker network connectلربطContainerبـNetworkdocker network disconnectلفصلContainerمنNetworkdocker network removeلحذفNetwork
النقطة الأهم التي يجب أن تتذكرها هي أنه يفضل دائمًا استخدام User Defined Networks بدلاً من الـ Bridge Network الافتراضية
لأنها توفر ميزة الـ DNS Resolution وتجعل الاتصال بين الـ Containers أكثر مرونة وسهولة في الاستخدام
في المقالات القادمة سنتعرف على مفهوم مهم آخر وهو الـ Docker Compose
وهو أداة تتيح لنا إدارة عدة Containers معًا بسهولة من خلال ملف واحد
وستجد أن الـ Docker Networks تلعب دورًا مهمًا جدًا في الـ Docker Compose لأنها تتيح لنا ربط الـ Containers ببعضها البعض بشكل منظم وسهل