شرح شبكات Docker Compose: DNS والمنافذ ووضع المضيف
تعرّف إلى شبكة المشروع الافتراضية، وDNS باسم الخدمة، ومتى يفيد host mode، وكيف تشارك شبكة بين مشروعين، ولماذا قد يتجاوز المنفذ المنشور UFW.
ما الذي ينشئه Compose قبل بدء تطبيقك
تبدأ شبكة Docker Compose بقاعدة واحدة: ينشئ docker compose up شبكة خاصة للمشروع، ويربط كل خدمة بها، ويتيح لهذه الخدمات الوصول إلى بعضها باستخدام اسم الخدمة. لا تحتاج إلى كتابة سطر networks: واحد لتحقيق ذلك. ينشأ معظم الالتباس حول شبكات Compose من عدم معرفة أن الشبكة الافتراضية موجودة مسبقاً.
إليك ملفاً صغيراً. احفظه باسم compose.yaml داخل دليل يُسمّى shop.
services:
web:
image: nginx:1.27
ports:
- "8080:80"
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: exampleشغّل المشروع واعرض ما أنشأه Docker:
docker compose up -d
docker network lsتتضمن القائمة الآن شبكة باسم shop_default. يسمّيها Compose باسم <project>_default، ويكون اسم المشروع افتراضياً هو اسم الدليل بأحرف صغيرة. يمكنك تغييره باستخدام docker compose -p myproject up -d أو باستخدام name: myproject على المستوى الأعلى في الملف. برنامج تشغيل الشبكة هو bridge، وهو محوّل افتراضي داخل المضيف. يحصل كل حاوية على عنوان ضمن شبكة فرعية خاصة، وتُترجم حركة المرور الصادرة إلى عنوان المضيف عند خروجها.
يحذف docker compose down هذه الشبكة مجدداً. لذلك يمكن لحاوية قديمة من مشروع سابق أن تمنع حذف الشبكة: يرفض Docker العملية ويعرض error while removing network: network shop_default has active endpoints، ويكون الحل بإيقاف الحاوية التي لا تزال مرتبطة بالشبكة أو إزالتها.
إذا كان Compose جديداً عليك، فمن المفيد قراءة بنية ملف Compose وأوامر دورة الحياة أولاً، لأن كل ما يلي يفترض أنك تعرف كيفية بدء المشروع وإيقافه.
DNS حسب اسم الخدمة هو الجزء الذي يفوّته المبتدئون
على أي شبكة يعرّفها المستخدم، يشغّل Docker خادم DNS مضمّناً يرى كل حاوية عنوانه على 127.0.0.11. ويحل أسماء الخدمات إلى عناوين الحاويات الحالية. لذلك يصل web إلى قاعدة البيانات باسم المضيف db وعلى المنفذ 5432، من دون أي إعداد.
docker compose exec web getent hosts dbيطبع ذلك سطراً مثل 172.18.0.2 db. إذا لم يطبع شيئاً، فهذا يعني أن الخدمتين ليستا على الشبكة نفسها.
الخطأ الذي يرتكبه الجميع تقريباً مرة واحدة هو استخدام localhost في إعدادات التطبيق. داخل الحاوية، يشير localhost إلى الحاوية نفسها، وليس إلى المضيف ولا إلى الخدمة الأخرى. وتعرض عملاء Postgres ذلك بوضوح:
could not connect to server: Connection refused
Is the server running on host "localhost" (127.0.0.1) and accepting
TCP connections on port 5432?يجب أن تكون سلسلة الاتصال postgresql://postgres:example@db:5432/postgres. جزء المضيف هو اسم الخدمة.
هناك تفصيلان يوفّران الوقت لاحقاً. تُحل الأسماء إلى ما يعمل حالياً، لذلك يعيد docker compose up -d --scale web=3 اسماً واحداً مع ثلاثة عناوين، بينما سيبقي العميل الذي يخزّن DNS مؤقتاً إلى أجل غير محدود اتصاله بحاوية متوقفة. كذلك، لا توفر شبكة bridge القديمة التي يستخدمها docker run العادي من دون --network أي حل للأسماء. لذلك لا تتطابق الإرشادات القديمة عن روابط الحاويات من عام 2016 مع ما تراه الآن.
لا تحتاج إلى ports: لربط خدمتين
تنشر ports: منفذ الحاوية على المضيف. وهي مخصصة لحركة الشبكة الواردة من خارج Docker. ولا علاقة لها بحركة الشبكة بين الخدمات، التي تعمل أصلاً عبر نطاق المنافذ بالكامل على شبكة المشروع.
لذلك فإن ports: - "5432:5432" التي يضيفها كثيرون إلى خدمة قاعدة البيانات لا تفيد، بل تسبب ضرراً حقيقياً: فهي تعرّض Postgres على الواجهة العامة للخادم. احذفها. إذا أردت الوصول إلى قاعدة البيانات من حاسوبك المحمول لإجراء عملية ترحيل، فاربطها بواجهة loopback باستخدام "127.0.0.1:5432:5432"، ثم اتصل بها عبر نفق SSH. يوضّح كيفية عمل المنافذ والخدمات التي تستمع على Linux الفرق بين socket يستمع، ومنفذ منشور، وقاعدة جدار ناري.
expose: مخصصة للتوثيق فقط في Compose. ولا تفتح أي شيء، لأنه لم يكن هناك ما هو مغلق بين الحاويات الموجودة على الشبكة نفسها.
متى يكون network_mode host مناسباً، وما تكلفته
يلغي وضع host مساحة أسماء الشبكة الخاصة بالحاوية، ويتيح للعملية استخدام واجهات الخادم مباشرة.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityتوجد أسباب حقيقية لاستخدامه. لا تستطيع عملية تحتاج إلى رؤية حركة البث العام أو البث متعدد الوجهات على الشبكة المحلية، مثل اكتشاف الأجهزة لخادم وسائط أو مركز أتمتة منزلية، رؤية هذه الحركة من خلف bridge، لأن bridge لا يمررها إلى الحاوية. ويحتاج وكيل مراقبة يقرأ عدادات واجهات الخادم إلى واجهات الخادم نفسها. كما أنك تتجاوز خطوة ترجمة العناوين، وهذا مهم عند معدلات الحزم المرتفعة.
لكن لذلك تكاليف محددة.
يتوقف ports: عن العمل. يحذّر Docker من تجاهل المنافذ المنشورة عند استخدام وضع شبكة host، وتربط الحاوية أي منافذ تربطها العملية داخلها. إذا أرادت حاويتان في وضع host استخدام المنفذ 8080، يحدث تعارض، وتتوقف الحاوية الثانية مع bind: address already in use.
يختفي حل الأسماء باستخدام اسم الخدمة في الاتجاهين. فالحاوية ليست على شبكة المشروع، ولذلك لا تستطيع حل db، كما لا تستطيع الخدمات الأخرى حل اسمها. ولا تصل إليها إلا عبر المنافذ المنشورة على الخادم، وعادةً على 127.0.0.1.
يختفي العزل. إذا ربطت عملية ما 0.0.0.0 داخل حاوية في وضع host، فهي تستمع على كل واجهات خادمك، بما في ذلك الواجهة العامة، تماماً مثل حزمة ثُبّتت باستخدام apt. توجد فائدة واحدة لذلك: تمر هذه الحركة عبر مسار الإدخال المعتاد، ولذلك تنطبق عليها قواعد UFW، بخلاف المنافذ المنشورة.
وضع host ميزة في Linux Docker Engine. ولا يدعمه Docker Desktop إلا بدءاً من الإصدار 4.34، وبعد تفعيله، مع قيود إضافية تمنع الحاويات من ربط عناوين IP الخاصة بالخادم، ولا تدعم إلا TCP وUDP. إذا كان نصف فريقك يستخدم خوادم Linux والنصف الآخر يستخدم Docker Desktop، فتوقع أن يتصرف الملف نفسه بطريقة مختلفة.
استخدم وضع host عندما تحتاج إلى واجهات الخادم. لا تستخدمه لإصلاح مشكلة اتصال، لأنه يستبدل المشكلة عادةً بمشكلة أصعب.
ربط مشروعي Compose باستخدام شبكة خارجية
لا تكون الشبكة التي ينشئها أحد المشروعين مرئية للمشروع الآخر. لذلك لا يستطيع Reverse Proxy في proxy/compose.yaml الوصول إلى تطبيق في app/compose.yaml، حتى على الخادم نفسه. الحل هو استخدام شبكة لا يملكها أيٌّ من المشروعين.
أنشئ الشبكة مرة واحدة يدوياً:
docker network create edgeثم عرّفها كشبكة خارجية في كل مشروع. في جهة الـproxy:
services:
proxy:
image: traefik:v3.1
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- edge
networks:
edge:
name: edge
external: trueفي جهة التطبيق:
services:
app:
image: nginx:1.27
networks:
- edge
- internal
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: example
networks:
- internal
networks:
edge:
name: edge
external: true
internal:يخبر external: true Compose بالاتصال بشبكة موجودة مسبقاً بدلاً من إنشاء شبكة جديدة، وبتركها موجودة في docker compose down. أما المفتاح المنفصل name: فأهم مما يبدو: من دونه يبحث Compose عن شبكة اسمها edge حرفياً، ومعه يمكنك تسمية الشبكة باسم في ملفك وباسم آخر على المضيف.
إذا لم تكن الشبكة موجودة، يرفض Compose بدء التشغيل ويذكر أن الشبكة عُرّفت كشبكة خارجية، لكن تعذّر العثور عليها. أنشئها أولاً.
لاحظ ما يفعله ملف التطبيق مع internal. توجد قاعدة البيانات على شبكة المشروع المحلية فقط، لذلك لا يستطيع الـproxy الوصول إليها، ولا يستطيع ذلك إلا app. تؤدي إضافة internal: true ضمن شبكة إلى خطوة إضافية، إذ تزيل مسارها إلى العالم الخارجي بالكامل. وهذا إعداد افتراضي مناسب لقاعدة البيانات، مع كلفة يجب معرفتها قبل تطبيقه: لا يستطيع أي container على شبكة داخلية تنزيل أي شيء، لذلك سيتوقف entrypoint الذي يشغّل apt-get update أو pip install عند بدء التشغيل، ثم يفشل بسبب انتهاء مهلة الاتصال.
للاطلاع على إعداد كامل يتضمن قواعد التوجيه والشهادات، راجع تشغيل عدة تطبيقات خلف نسخة واحدة من Traefik.
المنافذ المنشورة تتجاوز UFW
هذا هو الجزء من شبكة Compose الذي يتحول إلى حادثة أمنية. تنشر منفذاً، وتتحقق من أن UFW نشط ويرفض كل شيء باستثناء SSH، ومع ذلك تظل الخدمة قابلة للوصول من الإنترنت.
sudo ufw status
curl http://203.0.113.10:8080يقول UFW إن المنفذ محظور. لكن curl يعرض الصفحة رغم ذلك. لا يوجد عطل. يكتب Docker قواعد ترجمة العناوين وإعادة التوجيه الخاصة به مباشرةً في iptables، وتُعاد توجيه حركة المرور المتجهة إلى منفذ حاوية منشور إلى الحاوية بدلاً من تسليمها إلى الخادم، لذلك لا تمر عبر السلسلة التي يديرها UFW لحركة المرور المتجهة محلياً. كما تُطابق قواعد Docker قبل قواعد UFW.
الحل المختصر هو نشر المنفذ حيث تحتاج إليه فقط:
ports:
- "127.0.0.1:8080:80"يربط ذلك جانب الخادم بواجهة loopback، لذلك يمكن الوصول إلى المنفذ من الخادم نفسه ومن خلال نفق SSH، ولا يمكن الوصول إليه من أي مكان آخر. ضع نقطة الدخول العامة خلف reverse proxy ينشر المنفذين 80 و443 عن قصد. يشرح سبب نشر Docker للمنافذ متجاوزاً UFW مباشرة وكيفية إصلاح ذلك التفاصيل كاملة، بما في ذلك سلسلة DOCKER-USER للحالات التي يجب فيها تصفية منفذ منشور.
كيفية تصحيح المشكلة باستخدام 4 أوامر
ابدأ بالتحقق من الشبكة التي يوجد عليها كل container فعلياً:
docker network inspect shop_defaultيسرد الجزء Containers كل container متصل مع عنوانه. إذا غابت خدمة عن هذه القائمة، فهي متصلة بشبكة أخرى، أو تعمل في وضع المضيف، أو لا تعمل.
اختبر تحليل أسماء النطاقات من container مؤقت متصل بالشبكة نفسها، حتى لا تحتاج إلى تثبيت أدوات داخل صورك:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432يشير فشل nslookup إلى مشكلة في تحليل الأسماء أو في عضوية الشبكة. إذا نجح nslookup وفشل nc، فهذا يعني أن الخدمة تعمل، لكنها لا تستمع على ذلك المنفذ، أو تستمع على 127.0.0.1 داخل container الخاص بها بدلاً من 0.0.0.0. يحدث ذلك كثيراً مع خوادم التطوير. ويكون الإصلاح في عنوان الربط الخاص بالتطبيق، وليس في Docker.
هناك مشكلة أخرى تبدو كأنها خلل في Docker. إذا استطاعت containers التواصل فيما بينها، لكنها لم تستطع الوصول إلى جهاز على شبكة المكتب أو شبكة VPN، فمن المحتمل أن تتداخل الشبكة الفرعية لـDocker مع تلك الشبكة. يخصص Docker الشبكات بدءاً من 172.17.0.0/16 افتراضياً. انقل مجموعة العناوين في /etc/docker/daemon.json:
{
"default-address-pools": [
{ "base": "10.200.0.0/16", "size": 24 }
]
}ثم شغّل sudo systemctl restart docker وأعد إنشاء الشبكات المتأثرة، لأن الشبكة الموجودة تحتفظ بالشبكة الفرعية التي أُنشئت بها.
FAQ
لماذا لا تستطيع حاوياتي الوصول إلى بعضها باستخدام اسم الخدمة؟
لأنها ليست على الشبكة نفسها. يضع Compose كل خدمة على <project>_default تلقائياً، لكن بمجرد إضافة قائمة networks: إلى خدمة، تصبح هذه القائمة مجموعة الشبكات الكاملة الخاصة بها، ولا تعود الشبكة الافتراضية ضمنية. شغّل docker network inspect <network> وتحقق من ظهور الحاويتين ضمن الكتلة Containers. تحقق أيضاً من أن أياً من الخدمتين لا يستخدم network_mode: host، لأن الحاوية التي تستخدم وضع المضيف لا تكون على أي شبكة Docker، ولا يمكنها حل أسماء الخدمات.
هل أحتاج إلى نشر المنافذ لكي تصل خدمة إلى خدمة أخرى؟
لا. على شبكة Compose، يمكن لجميع الحاويات الأخرى على الشبكة الوصول إلى كل منفذ في كل حاوية. يوجد ports: فقط لتعريض الحاوية أمام حركة الشبكة القادمة من خارج Docker، وexpose: للتوثيق. نشر منفذ قاعدة البيانات عادة شائع ومكلف، لأنه يضع قاعدة البيانات على الواجهة العامة لخادمك.
ما الفرق بين شبكتي bridge وhost؟
تمنح bridge الحاوية مساحة أسماء شبكة وعنواناً خاصين بها على محوّل افتراضي، مع حل تلقائي للأسماء بين الحاويات وترجمة لحركة الشبكة الصادرة. أما host فتمنح الحاوية مكدس شبكة المضيف مباشرة: من دون عنوان منفصل، أو حل لأسماء الخدمات، أو نشر للمنافذ، أو عزل عن المستمعات الأخرى على المضيف. شبكة bridge هي الافتراضية وهي الخيار الصحيح، إلا إذا كانت العملية تحتاج إلى واجهات المضيف.
كيف أصل حاويات موجودة في ملفي Compose مختلفين؟
أنشئ شبكة مشتركة باستخدام docker network create edge، ثم عرّفها في الملفين باستخدام external: true وألحق بها الخدمات التي تحتاج إلى الاتصال. لن ينشئها Compose ولن يحذفها. إذا تخطيت خطوة الإنشاء، يرفض Compose التشغيل ويبلغ عن أن الشبكة معلنة كشبكة خارجية لكنها غير موجودة.
لماذا يمكن الوصول إلى حاويتي من الإنترنت رغم أن UFW يحظر المنفذ؟
لأن Docker يعالج المنفذ المنشور باستخدام قواعد إعادة التوجيه التي يضيفها إلى iptables. تُطابق هذه القواعد قبل قواعد UFW، كما أن حركة المرور المُعاد توجيهها لا تمر عبر السلسلة التي يرشحها UFW أصلاً. اربط جهة المضيف بالواجهة loopback باستخدام "127.0.0.1:8080:80"، وضع كل ما هو عام خلف Reverse Proxy على المنفذين 80 و443.