شرح شبكات 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 حل أسماء الحاويات على الإطلاق. لا تجد الحاويات هناك بعضها إلا عبر عناوين IP، وتبحث عن أسماء المضيفين الخارجية من خلال نسخة من /etc/resolv.conf الخاصة بالمضيف. لذلك لا تتطابق الإرشادات المتعلقة بربط الحاويات من عام 2016 مع ما تراه الآن.
لا تحتاج إلى ports: لربط خدمتين
تنشر ports: منفذ الحاوية على المضيف. وهي مخصصة لحركة الشبكة الواردة من خارج Docker. ولا علاقة لها بحركة الشبكة بين الخدمات، التي تعمل أصلاً عبر نطاق المنافذ بالكامل على شبكة المشروع.
لذلك فإن ports: - "5432:5432" التي يضيفها كثيرون إلى خدمة قاعدة البيانات لا تفيد، بل تسبب ضرراً حقيقياً: فهي تعرّض Postgres على الواجهة العامة للخادم. احذفها. إذا أردت الوصول إلى قاعدة البيانات من حاسوبك المحمول لإجراء عملية ترحيل، فاربطها بواجهة loopback باستخدام "127.0.0.1:5432:5432"، ثم اتصل بها عبر نفق SSH. يوضّح كيفية عمل المنافذ والخدمات التي تستمع على Linux الفرق بين socket يستمع، ومنفذ منشور، وقاعدة جدار ناري.
expose: مخصصة للتوثيق فقط في Compose. ولا تفتح أي شيء، لأنه لم يكن هناك ما هو مغلق بين الحاويات الموجودة على الشبكة نفسها.
متى يكون network_mode host مناسباً، وما تكلفته
يلغي وضع المضيف مساحة أسماء الشبكة الخاصة بالحاوية، ويتيح للعملية استخدام واجهات المضيف مباشرة.
services:
probe:
image: alpine:3.20
network_mode: host
command: sleep infinityتوجد أسباب فعلية لاستخدامه. لا تستطيع عملية تحتاج إلى رؤية حركة البث أو البث المتعدد على الشبكة المحلية، مثل اكتشاف الأجهزة لخادم وسائط أو مركز أتمتة منزلية، رؤية تلك الحركة من خلف bridge، لأن bridge لا يمررها إلى الحاوية. ويحتاج وكيل المراقبة الذي يقرأ عدادات واجهات المضيف إلى واجهات المضيف. كما أنك تتجاوز خطوة ترجمة العناوين، وهذا مهم عند معدلات الحزم المرتفعة.
التكاليف محددة.
يتوقف ports: عن العمل. يحذّر Docker من تجاهل المنافذ المنشورة عند استخدام وضع شبكة المضيف، وترتبط الحاوية بأي منافذ ترتبط بها العملية. تتعارض حاويتان تستخدمان وضع المضيف وتريدان المنفذ 8080، وتتوقف الثانية مع bind: address already in use.
يختفي حل الأسماء بحسب اسم الخدمة في الاتجاهين. لا تكون الحاوية على شبكة المشروع، لذلك لا تستطيع حل db، كما لا تستطيع الخدمات الأخرى حل اسمها. لكنها تظل قادرة على الوصول إليها عبر المنافذ المنشورة على المضيف، عادةً عند 127.0.0.1. وعلى Linux، يمكنها أيضاً الوصول مباشرة إلى حاوية bridge عبر عنوان IP الخاص بتلك الحاوية وعلى أي منفذ يستمع إليه، لأن المضيف لديه وصول مفتوح إلى الحاويات على شبكات bridge الخاصة به، ولأن حاوية وضع المضيف تشارك مكدس شبكة المضيف. تتغير هذه العناوين عند إعادة إنشاء الحاويات، لذلك يكون المنفذ المنشور على loopback هو المسار الثابت.
تختفي العزلة. إذا ربطت عملية 0.0.0.0 داخل حاوية تستخدم وضع المضيف، فإنها تستمع على كل واجهة في خادمك، بما فيها الواجهة العامة، تماماً مثل حزمة مثبّتة باستخدام apt. ولهذا جانب إيجابي واحد: تمر هذه الحركة عبر مسار الإدخال المعتاد، ولذلك تنطبق عليها قواعد UFW، وهذا لا ينطبق على المنافذ المنشورة.
وضع المضيف ميزة في Linux Docker Engine. يدعمه Docker Desktop بدءاً من الإصدار 4.34 فقط، وبعد تفعيله، مع قيود إضافية تتمثل في عدم قدرة الحاويات على ربط عناوين IP الخاصة بالمضيف، ومعالجة TCP وUDP فقط. إذا كان نصف فريقك يستخدم خوادم Linux والنصف الآخر يستخدم Docker Desktop، فتوقع أن يتصرف الملف نفسه بشكل مختلف.
استخدم وضع المضيف عندما تحتاج إلى واجهات المضيف. لا تستخدمه لإصلاح مشكلة اتصال، لأنه يستبدل المشكلة عادةً بمشكلة أصعب.
ربط مشروعي 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 للحالات التي يجب فيها تصفية منفذ منشور.
كيفية تصحيح المشكلة باستخدام أربعة أوامر
ابدأ بتحديد الشبكة الفعلية لكل حاوية:
docker network inspect shop_defaultيسرد المقطع Containers كل حاوية متصلة مع عنوانها. إذا غابت خدمة عن هذه القائمة، فهي على شبكة مختلفة، أو تستخدم وضع المضيف، أو لا تعمل.
اختبر حل الأسماء من حاوية مؤقتة متصلة بالشبكة نفسها، حتى لا تحتاج إلى أي أدوات داخل صورك نفسها:
docker run --rm --network shop_default busybox nslookup db
docker run --rm --network shop_default busybox nc -zv db 5432يشير فشل nslookup إلى مشكلة في حل الأسماء أو عضوية الشبكة. إذا نجح nslookup وفشل nc، فهذا يعني أن الاسم والشبكة سليمين وأن المشكلة في الاتصال نفسه. إذا أبلغ nc عن Connection refused، فالسبب المعتاد هو أن الخدمة لا تستمع على ذلك المنفذ، أو تستمع على 127.0.0.1 داخل حاويتها بدلاً من 0.0.0.0. يحدث ذلك كثيراً مع خوادم التطوير، ويكون الإصلاح في عنوان الربط الخاص بالتطبيق، وليس في Docker. إذا ظل nc منتظراً ثم انتهت المهلة، فابحث عن شيء يحذف الحركة أثناء مرورها، مثل قاعدة جدار ناري، أو حاوية متوقفة مؤقتاً أو لا تستجيب.
هناك عطل آخر يبدو كأنه خطأ في Docker. إذا استطاعت الحاويات التواصل بعضها مع بعض، لكنها لم تستطع الوصول إلى جهاز على شبكة مكتبك أو 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.