توجيه حاويات Docker عبر VPN باستخدام Gluetun
تختفي المنافذ عند استخدام network_mode: service:gluetun لأن الحاوية تشارك مساحة الشبكة. تعرّف إلى ملف Compose الصحيح ونشر المنافذ على حاوية VPN.
لماذا تختفي المنافذ عند توجيه حاويات Docker عبر VPN
لتوجيه حاويات Docker عبر VPN، تمنح حاوية واحدة نفق VPN، ثم تربط الحاويات الأخرى بمساحة أسماء الشبكة الخاصة بها باستخدام network_mode: "service:gluetun". هذا الربط هو الجزء الذي يسبب الالتباس. لا تعود للحاوية المرتبطة شبكة خاصة بها، لذلك تختفي معها المنافذ المنشورة واسم خدمة Docker الخاص بها. انشر المنافذ على حاوية VPN بدلاً من ذلك، ويمكن للحاويات الأخرى الوصول إلى التطبيق باستخدام اسم حاوية VPN.
إذا أبقيت كتلة ports: في الحاوية المرتبطة، فسيرفض Docker إنشاءها أصلاً:
Error response from daemon: conflicting options: port publishing and the container type network modeالأداة المستخدمة هنا هي Gluetun، وهي حاوية تتصل بموفّر VPN تجاري عبر WireGuard أو OpenVPN، وتطبّق جداراً نارياً خاصاً بها. الإصدار v3.41.3 هو الإصدار الحالي اعتباراً من August 2026. تستخدم الأمثلة Mullvad مع WireGuard، لذلك تحتاج إلى حساب ومفتاح من موفّرك. إذا كنت تفضّل إنهاء النفق على عتاد تملكه، فإن تشغيل خادم WireGuard خاص بك على VPS ينشئ الطرف الآخر، بينما يضيف wg-easy في Docker واجهة ويب لذلك.
ما الذي يفعله network_mode: "service:gluetun" فعلياً
يحصل كل Docker container عادةً على network namespace خاص به: واجهاته الخاصة، وجدول التوجيه الخاص به، وقواعد جدار الحماية الخاصة به، ومقابس الاستماع الخاصة به. يتجاوز الوضع service: هذه الخطوة، ويشغّل الـcontainer داخل namespace الخاص بـgluetun. يعني استخدام namespace واحد وجود عنوان IP واحد، وهذا يغيّر ستة أمور.
- لا يملك التطبيق عنواناً خاصاً به. عنوانه هو عنوان gluetun.
- لا يتصل التطبيق بأي Docker network، لذلك لا يُسجَّل اسم خدمته ولا يمكن حله. يجب أن تستخدم الـcontainers الأخرى
gluetun. - تتصل الـcontainers الموجودة داخل namespace الواحد ببعضها عبر
localhost. - لا يمكن لـcontainerين في namespace واحد الاستماع على المنفذ نفسه. توضح وثائق Gluetun ذلك صراحةً: لا يوجد حل بديل.
- ترتبط Capabilities بالـcontainer، لا بالـnamespace. يملك Gluetun كلاً من
NET_ADMINو/dev/net/tunلأنه ينشئ واجهة النفق. ولا يرث الـcontainer المتصل هذه الصلاحيات. - يرفض Compose أي ملف تضبط فيه إحدى الخدمات كلاً من
network_modeوnetworks. صِل gluetun بشبكاتك، وسيعمل التطبيق عبره.
يؤدي إعادة تشغيل gluetun إلى قطع اتصال كل ما هو متصل به. هذا سلوك موثق، ولذلك يعيد gluetun تشغيل عملية VPN داخل الـcontainer بدلاً من الخروج عند فشل الاتصال. بعد إعادة تشغيل gluetun أو إعادة إنشائه يدوياً، أعد تشغيل الـcontainers المتصلة به.
ملف Compose الذي يعمل
services:
gluetun:
image: qmcgaw/gluetun:v3
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- SERVER_CITIES=Amsterdam
- TZ=Europe/Amsterdam
env_file:
- ./gluetun.env
volumes:
- ./gluetun:/gluetun
ports:
- 127.0.0.1:8080:8080/tcp
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
container_name: qbittorrent
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- ./downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedتمثل العلامة :v3 أحدث إصدار مستقر في السلسلة v3. وتشير العلامة :latest إلى آخر commit في فرع master، وهو مسار التطوير المتغير. لذلك ثبّت :v3 على جهاز لا تريد أن تستكشف أعطاله يوم الثلاثاء.
يجب أن تطابق WEBUI_PORT=8080 المنفذ المنشور، لأن qBittorrent يستمع داخل مساحة أسماء gluetun، وتوجّه قاعدة النشر حركة مرور الخادم إلى المنفذ 8080 هناك. إذا غيّرت أحد الرقمين دون الآخر، فلن يستجيب المنفذ لأي شيء. تُبقي 127.0.0.1:8080:8080 واجهة الويب على عنوان loopback المحلي للخادم. أما 8080:8080 المجردة فتنشر المنفذ على كل واجهة وتضيف قاعدة جدار ناري خاصة بها، وهذا ما يتيح لـمنافذ Docker المنشورة تجاوز ufw مباشرة.
شغّل الحاويات، ثم تحقق منها بالترتيب التالي:
docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30يجب أن تعرض docker compose ps gluetun بالحالة healthy، وqbittorrent بالحالة running. ثم تحقّق من عنوان الخروج من داخل مساحة الأسماء. هذا هو الاختبار الذي يحدد نتيجة كل ما سواه:
docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"يجب أن يحتوي الحقل ip في JSON على عنوان موفر VPN لديك. إذا كان العنوان الخاص بخادمك، فهذا يعني أن التطبيق ليس داخل النفق، ولن يعمل أي مما يلي كما هو موضح.
أبقِ المفاتيح خارج ملف Compose
يحتوي gluetun.env على بيانات الاعتماد، ويبقى خارج مستودع git:
WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32تأتي القيمتان من ملف إعداد WireGuard تنشئه في منطقة حسابك لدى موفّر الخدمة. اضبط وضع الملف على 600. كن واضحاً بشأن ما يوفّره ذلك: يبقى المفتاح خارج مستودعك، لكن docker inspect gluetun يطبع جميع متغيرات البيئة لأي شخص يمكنه الوصول إلى Docker socket. يشرح ملفات البيئة والأسرار في Docker Compose الخيارات الأقوى.
كيف تتصل حاوية خارج النفق بحاوية داخله
يعمل الاتصال في الاتجاهين، ويستخدم كل اتجاه اسماً مختلفاً. تحتاج الحاويتان إلى شبكة Docker مشتركة. وهذه هي شبكة gluetun، لأن الحاوية المرتبطة به لا تملك شبكة خاصة بها. يشرح كيفية ربط شبكات Docker في Docker Compose الإعدادات الافتراضية.
للاتصال من الخارج إلى الداخل، استخدم اسم gluetun والمنفذ الذي يستمع عليه التطبيق. تصل حاوية Reverse Proxy إلى واجهة qBittorrent على gluetun:8080. لا تحتاج إلى إدخال ports: لهذا الاتصال، لأن حركة المرور بين الحاويات تبقى على شبكة Docker ولا تمر عبر منفذ على المضيف.
للاتصال من الداخل إلى الخارج، استخدم اسم خدمة الحاوية الأخرى، مثل postgres:5432. يحل Gluetun أسماء الحاويات الأخرى من داخل مساحة الأسماء الخاصة به منذ الإصدار v3.41. ثبّت هذا الإصدار أو إصداراً أحدث إذا تعذر حل اسم ما.
يحدد جدار Gluetun الناري من يُسمح له بفتح اتصال به. يُسمح بحركة المرور القادمة من شبكة Docker الخاصة بـ gluetun. أما العميل الموجود على شبكة فرعية مختلفة، مثل حاسوب محمول على شبكتك المحلية أو حاوية على شبكة bridge منفصلة، فيُسقط اتصاله حتى تسمّي تلك الشبكة الفرعية:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24المعنى الموثق دقيق: شبكات فرعية مفصولة بفواصل، يُسمح لـ Gluetun والحاويات التي تشاركه مكدس الشبكة بالوصول إليها.
الاتصالات الواردة من الإنترنت مشكلة منفصلة. يصل أقران عميل التورنت من جهة VPN، لذلك لا يفيد نشر المنفذ 6881 على المضيف. تحتاج إلى منفذ معاد توجيهه من مزود VPN، وإلى إدراج هذا المنفذ في FIREWALL_VPN_INPUT_PORTS، الذي يسمح بالمنافذ القادمة من جهة خادم VPN. هذه هي الجزئية التي تتركها معظم مكدسات الوسائط المبنية باستخدام Docker Compose معطلة.
مفتاح الإيقاف: ما يحدث عند انقطاع النفق
تظهر فائدة هذا التعقيد عند حدوث عطل. لا يملك الحاوية المرتبطة مساراً بديلاً. ومسارها الوحيد للخروج من الجهاز هو مساحة الأسماء التي تشاركها، لذلك لا يوجد ما يمكنها استخدامه عند توقف النفق. ويفرض جدار Gluetun الناري القاعدة نفسها من الجهة الأخرى: يمر traffic الصادر عبر النفق أو إلى نقطة نهاية خادم VPN، بينما تُسقط كل الاتصالات الأخرى. ولا توجد فترة تتسرّب فيها الحزم عبر الواجهة العادية أثناء إعادة اتصال العميل.
يراقب Gluetun اتصاله الخاص. كل دقيقة، يرسل ICMP echo (طلب ping) إلى العناوين الموجودة في HEALTH_ICMP_TARGET_IPS، والتي تكون افتراضياً 1.1.1.1,8.8.8.8. وكل خمس دقائق، ينشئ اتصال TCP وTLS (أمان طبقة النقل) كاملاً مع HEALTH_TARGET_ADDRESSES، والقيمة الافتراضية هي cloudflare.com:443,github.com:443. وعندما تفشل هذه الاختبارات، يعيد تشغيل VPN داخل الحاوية ويسجّل ذلك:
WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeoutاقرأ سجلات الحاوية المرتبطة مع وضع هذا التسلسل في الاعتبار. إن الأسطر مثل connection refused وoperation not permitted وi/o timeout داخل التطبيق هي نتائج توقف النفق، وليست أسباباً له. وتذكر وثائق Gluetun ذلك صراحةً، لأن المستخدمين يبلّغون عن النتيجة ثم يقضون ساعات في البحث عنها.
يكون HEALTH_RESTART_VPN=on مفعّلاً افتراضياً، وينبغي أن تتركه مفعّلاً. عطّله فقط أثناء تصحيح عطل محدد، لأن النفق المتوقف سيظل متوقفاً عند تعطيله.
الترتيب: منع بدء المكدس قبل جاهزية النفق
تتضمن الصورة Docker healthcheck:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckيشغّل هذا الأمر نسخة ثانية قصيرة العمر من gluetun، وتستعلم هذه النسخة عن health server للنسخة العاملة على http://127.0.0.1:9999/. يجيب النفق العامل بـ200 OK. أما النفق المعطّل فيجيب بـ500 Internal server error مع سلسلة خطأ، وتُعلَّم الحاوية بأنها غير سليمة بعد فشل واحد.
ينتظر condition: service_healthy تحقق ذلك الشرط. أما depends_on: [gluetun] العادي فلا ينتظر إلا بدء الحاوية، ويحدث ذلك قبل اكتمال handshake بعدة ثوانٍ. لذلك يبدأ التطبيق مع شبكة غير عاملة، وغالباً ما يتخلى عن محاولة الاتصال الأولى. يشرح Healthchecks في Docker Compose الصياغة وحقول التوقيت.
هناك حدّ قد يسبب مشكلات. يقيّم Compose هذا الشرط مرة واحدة عند إنشاء الحاوية. ولا يوقف التطبيق أو يعيد تشغيله لاحقاً إذا أصبحت gluetun غير سليمة. تتولى ميزة auto-healing الداخلية في gluetun هذه الحالة بدلاً من ذلك. ولهذا تعيد تشغيل عملية VPN لا الحاوية.
تحقّق من تسرّب DNS قبل الوثوق بالإعداد
يُعد DNS (نظام أسماء النطاقات) التسرب الذي يستمر حتى عند إعداد النفق بشكل صحيح. يشغّل Gluetun محلّل أسماء خاصاً به داخل مساحة الأسماء، ويمرّر الاستعلامات عبر DoT (DNS عبر TLS) إلى Cloudflare افتراضياً: DNS_UPSTREAM_RESOLVER_TYPE=dot وDNS_UPSTREAM_RESOLVERS=cloudflare. اترك القيمتين كما هما، وستكون استعلاماتك مشفّرة وتمر عبر النفق.
الإعداد الذي يتسبب في هذا الخلل هو DNS_UPSTREAM_PLAIN_ADDRESSES. يلجأ إليه المستخدمون عندما يفشل حل اسم ما ويريدون من الموجّه أو محلّل مزود الخدمة الإجابة بدلاً من ذلك. توضّح وثائق Gluetun النتيجة صراحةً: لن تمر حركة DNS كلها عبر نفق VPN، بل ستتسرّب خارجه. تظل حركة الشبكة الخاصة بك محمية. لكن قائمة أسماء المضيفين التي تستعلم عنها لا تظل كذلك. ويغطي DNS الذي يتوقف عن الحل عبر نفق WireGuard الخطأ نفسه في إصدار WireGuard.
لاختبار ذلك، عيّن HTTPPROXY=on على gluetun وانشر 8888:8888/tcp، ثم وجّه متصفحاً إلى ذلك الـproxy وافتح اختباراً لتسرّب DNS. يجب أن تذكر النتيجة مزود الخدمة أو Cloudflare، وألا تذكر موجّهك المنزلي مطلقاً. تحذّر وثائق Gluetun نفسها من أن بعض اختبارات التسرّب تعرض نتائج غير معتادة، لأن محلّل الأسماء داخل مساحة الأسماء يعمل كوسيط تخزين مؤقت محلي، وليس كخادم يقدّم الإجابة النهائية. اعتبر ظهور بلد غير صحيح أو محلّل DNS الخاص بمزود خدمة الإنترنت لديك إشارة التسرّب الفعلية.
إضافة Tailscale بجانب VPN sidecar، وأيهما تكون له الأولوية
Tailscale هي شبكة تراكبية مبنية على WireGuard للوصول إلى أجهزتك، ويشغّلها بعض المستخدمين بجانب VPN مقدّم خدمة للحفاظ على مسار إداري إلى المكدس. نادراً ما يتعارض الاثنان، وهناك سبب يستحق الفهم. توضّح وثائق Tailscale الإعداد الافتراضي: تعمل Tailscale كشبكة تراكبية، وتوجّه حركة المرور بين الأجهزة التي تشغّل Tailscale فقط، ولا تتعامل مع حركة مرور الإنترنت العامة.
لذلك تعتمد الإجابة على إعداد واحد.
- Tailscale في حاويتها الخاصة، مع الإعداد الافتراضي: لا ترى أبداً حركة المرور الصادرة من التطبيق. يتولى Gluetun كل هذه الحركة. تصل Tailscale إلى التطبيق عبر
gluetun:8080، تماماً مثل أي حاوية خارجية أخرى. - ربط Tailscale بمساحة أسماء gluetun عبر
network_mode: "service:gluetun": تحتاج إلىcap_addالخاص بها منnet_adminوnet_raw، لأن الإمكانات لا تنتقل مع مساحة الأسماء. في وضع الشبكات الافتراضي لمساحة المستخدم، يكونTS_USERSPACEمفعّلاً، ولا ينشئ tailscaled أي واجهة على الإطلاق، بل يعمل كـSOCKS5 أو HTTP proxy، ولذلك لا يستطيع تغيير التوجيه. يواصل Gluetun حمل كل حركة المرور. - الإعداد نفسه مع
TS_USERSPACE=false: ينشئ tailscaled جهاز نفق ويثبّت المسارات، لكن لنطاق tailnet100.64.0.0/10فقط، بالإضافة إلى أي مسارات شبكات فرعية تعلنها باستخدامTS_ROUTES. تظل حركة المرور العامة تخرج عبر gluetun. - أي من الإعدادات السابقة مع اختيار exit node،
sudo tailscale set --exit-node=<exit-node-ip>: تستحوذ Tailscale على المسار الافتراضي وتكون لها الأولوية. لا تجمع هذا الإعداد مع gluetun. يوجد مسار افتراضي واحد ومالك واحد.
إذا كانت تلك المسارات المعلنة هي الغرض، وكنت تريد إتاحة الوصول إلى الشبكة الخاصة بأكملها خلف الصندوق بدلاً من الصندوق وحده، فإن تشغيل موجّه شبكة فرعية لـTailscale على VPS يشرح اعتماد المسار، وتمرير IP، وراية جهة العميل التي يتركها TS_ROUTES دون إعداد بمفرده.
إذا كانت Tailscale موجودة لتوفير URL إداري بدلاً من توفير مسار، فإن tailscale serve وtailscale funnel يضعان HTTPS أمام gluetun:8080 لشبكة tailnet الخاصة بك، ولا يفتحها أمام الإنترنت العام إلا funnel.
تظهر إحدى النتائج الجانبية عند تشغيل Tailscale داخل النفق. ترى الأجهزة النظيرة عنوان VPN الخاص بمقدّم الخدمة، لذلك توقّع انتقالها إلى المرحلات بوتيرة أعلى. يعرض tailscale status قيمة relay "..." بجانب جهاز نظير بدلاً من direct عند حدوث ذلك. يعمل الاتصال، لكنه يكون أبطأ. إذا كانت الشبكة التراكبية هي الشيء الوحيد الذي تحتاج إليه فعلياً، فإن الفرق بين WireGuard العادي وTailscale هو نقطة البداية الأفضل.
ما الذي يتعطل، والرسالة التي ستظهر
يرفض Docker إنشاء حاوية التطبيق. يعني Error response from daemon: conflicting options: port publishing and the container type network mode أن كتلة ports: ما زالت مرتبطة بالخدمة المرفقة. انقلها إلى gluetun.
يرفض Compose الملف بالكامل. لا يمكن للخدمة ضبط كل من network_mode وnetworks. ضع الشبكات في gluetun.
لا تستطيع حاوية أخرى حل اسم التطبيق. هذا سلوك صحيح في curl: (6) Could not resolve host: qbittorrent، لأن الحاوية المرفقة لم تنضم إلى أي شبكة ولم تسجّل أي اسم. استخدم gluetun والمنفذ.
لا تبدأ الحاوية المرفقة الثانية. لا يمكن لعمليتين في مساحة أسماء واحدة ربط المنفذ نفسه، وتُبلغ العملية التي تخسر بأن العنوان قيد الاستخدام بالفعل. غيّر المنفذ الداخلي للتطبيق، أو شغّل gluetun ثانياً.
لا يملك التطبيق اتصالاً بالشبكة بعد تعديل gluetun. تؤدي إعادة تشغيل gluetun أو إعادة إنشائه إلى قطع الاتصال عن كل ما هو مرفق به. أعد تشغيل تلك الحاويات.
تُحمَّل الصفحات الصغيرة وتتوقف الصفحات الكبيرة. هذه مشكلة MTU (وحدة النقل القصوى). يضيف النفق حملاً إضافياً، ويسقط شيء ما في المسار الحزم الأكبر من الحجم المسموح به من دون إرسال خطأ. خفّض WIREGUARD_MTU، وجرّب 1400، ثم 1320.
لا تصبح Gluetun سليمة أبداً. يحدد فحص بدء التشغيل المشتبه بهم الأوائل: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. تحقق من انتهاء صلاحية المفتاح، ثم من تقادم قائمة الخوادم، ثم من أن جدار الحماية على المضيف لا يحظر UDP الصادر.
FAQ
لماذا توقفت المنافذ المنشورة لحاويتي عن العمل خلف Gluetun؟
لأن network_mode: "service:gluetun" يضع الحاوية داخل مساحة أسماء الشبكة الخاصة بـGluetun، ولكل مساحة أسماء عنوان IP واحد ومجموعة واحدة من المنافذ التي تستمع إليها. يواصل التطبيق الاستماع، لكن يجب أن تكون قاعدة النشر على الحاوية التي تملك مساحة الأسماء. انقل قائمة ports: إلى خدمة Gluetun. إذا أبقيتها في الخدمة المرتبطة، فلن ينشئها Docker أصلاً: Error response from daemon: conflicting options: port publishing and the container type network mode.
كيف أصل إلى حاوية داخل نفق VPN من حاوية خارجه؟
استخدم اسم خدمة Gluetun والمنفذ الذي يستمع إليه التطبيق، مثل gluetun:8080. لا تملك الحاوية المرتبطة أي شبكة Docker خاصة بها، لذلك لا يُحل اسمها. لا تحتاج حركة المرور بين الحاويات إلى نشر أي منفذ. في الاتجاه المعاكس، تصل حاوية داخل مساحة الأسماء إلى حاوية خارجها باستخدام اسم خدمتها، مثل postgres:5432، في Gluetun v3.41 والإصدارات الأحدث. يُسقط Gluetun اتصال عميل موجود في شبكة فرعية مختلفة، مثل حاسوب محمول على شبكتك المحلية، بواسطة جدار الحماية إلى أن تضيف تلك الشبكة الفرعية إلى FIREWALL_OUTBOUND_SUBNETS.
هل يعمل Gluetun كمفتاح إيقاف عند انقطاع VPN؟
نعم، وذلك لسببين في الوقت نفسه. لا تملك الحاوية المرتبطة أي مسار سوى المسار الموجود في مساحة الأسماء المشتركة، لذلك لا يترك النفق المتوقف أي مسار لها إلى خارج الجهاز. كما يسمح جدار حماية Gluetun بحركة المرور الصادرة عبر النفق وإلى نقطة نهاية خادم VPN فقط. ثم يعيد Gluetun تشغيل VPN داخلياً، ويسجل WARN [vpn] restarting VPN because it failed to pass the healthcheck، بدلاً من الخروج، لأن كل حاوية مرتبطة تفقد شبكتها عند إعادة تشغيل Gluetun نفسه.
Tailscale وGluetun في الحزمة نفسها: أيهما ينقل حركة المرور الصادرة؟
Gluetun في كل إعداد باستثناء إعداد واحد. يوجّه Tailscale حركة المرور بين الأجهزة في tailnet لديك افتراضياً فقط، ويترك حركة المرور العامة دون تغيير. في وضع userspace الافتراضي لصورة الحاوية، لا ينشئ أي واجهة على الإطلاق، لذلك لا يمكنه التأثير في التوجيه. عند استخدام TS_USERSPACE=false، يثبّت مسارات لـ100.64.0.0/10 ولشبكاتك الفرعية المعلنة فقط. الاستثناء هو exit node: يجعل sudo tailscale set --exit-node=<exit-node-ip> من Tailscale المسار الافتراضي، وعندها تكون له الأولوية. اختر منتجاً واحداً لامتلاك المسار الافتراضي بدلاً من تكديس المنتجين معاً.