Traefik مع Docker Compose: 5 تطبيقات خلف IP واحد
شغّل 5 تطبيقات خلف عنوان IP واحد مع Traefik v3 وDocker Compose، وتعلّم التوجيه عبر Host وتجديد Let's Encrypt وفخ acme.json الذي يمنع بدء التشغيل.
عنوان IP واحد، و5 تطبيقات، ومنفذ 443 واحد
لدى VPS الخاص بك عنوان IPv4 عام واحد ومنفذ TCP واحد هو 443. وتريد تشغيل Gitea، ونسخة تجريبية من تطبيقك، ولوحة معلومات داخلية، وصفحة حالة، ومستقبلاً لاستقبال webhooks عليه: 5 أسماء مضيفين على خادم واحد. الـReverse Proxy هو العملية التي تستحوذ على المنفذين :80 و:443، وتقرأ الرأس Host في كل طلب، ثم تمرّره إلى الحاوية المناسبة. ينفّذ Traefik ذلك، ويحصل على شهادة لكل اسم مضيف ويجددها من دون أن تضطر إلى تشغيل certbot يدوياً. ويمكن لـNginx وCaddy أيضاً إنهاء TLS لأسماء المضيفين الخمسة نفسها بكفاءة، لذلك إذا لم تحسم اختيارك بعد، فمن المفيد مقارنة الوكلاء الثلاثة وفق طريقة إدارة الشهادات وتكلفة الإعداد لكل تطبيق قبل ربط كل شيء بأحدها.
ما يميّز Traefik عن كتلة nginx server {} هو مصدر إعداداته. في nginx، تعدّل ملفاً ثم تعيد تحميل الخدمة، بينما تبقى دورة حياة الشهادة مهمة منفصلة. هذا هو سير العمل الذي تتبعه عندما تصدر شهادات Let's Encrypt باستخدام certbot مع nginx، إذ يعمل مؤقت التجديد خارج خادم الويب بالكامل. يراقب Docker provider في Traefik تدفق أحداث Docker، ويقرأ labels من حاوياتك: شغّل حاوية تحمل label لقاعدة Host()، فتصبح قابلة للتوجيه خلال ثانية؛ أوقفها، فيختفي المسار. وهذه هي المشكلة أيضاً. عندما تكون الإعدادات موجودة في labels، فإنها تتوزع على 5 أماكن في الوقت نفسه. وإذا كان label خاطئاً، فلن يظهر خطأ؛ بل لن يوجّه Traefik الحاوية ببساطة، ولن يعرض أي رسالة.
الأسماء الأربعة
- نقاط الدخول هي مقابس الاستماع. ستعرّف نقطتين:
webعلى:80وwebsecureعلى:443. - الموجّهات تطابق الطلب (
Host(...)) وتربطه بخدمة. تُطلب الشهادات لكل موجّه عبرtls.certresolver. - الخدمات هي الواجهة الخلفية: حاوية والمنفذ الذي تستمع إليه داخل شبكة Docker.
- البرمجيات الوسيطة تقع بين الموجّه والخدمة: مصادقة أساسية، وقوائم سماح لعناوين IP، وإعادة كتابة الرؤوس، وإعادة التوجيه.
هذه الأسماء الأربعة هي تسميات Traefik للمهام التي كنت ستكتبها يدوياً بخلاف ذلك: الموجّه هو server_name، والخدمة هي هدف proxy_pass، والبرمجيات الوسيطة هي توجيهات الرؤوس والمصادقة التي تضبطها بنفسك عند إنشاء كتلة خادم reverse proxy في nginx سطراً بعد سطر.
يُمرَّر الإعداد الثابت، مثل نقاط الدخول وموفّري الإعداد وACME، إلى Traefik في سطر الأوامر أو في traefik.yml، ويعني تغييره إعادة تشغيل Traefik. يصل الإعداد الديناميكي، مثل الموجّهات والخدمات والبرمجيات الوسيطة، من تسميات الحاويات، ويُعاد تحميله فوراً. الخلط بين النوعين هو السبب المعتاد لظهور رسالة «لا يفعل العلم الذي أضفته شيئاً».
ملف Compose
تمثّل شبكة Docker مشتركة واحدة باسم proxy العمود الفقري للإعداد. ولا يستطيع Traefik الوصول إلى حاوية إلا إذا كانت الحاوية وTraefik متصلين بهذه الشبكة.
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.5
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.network=proxy
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
# while you iterate, point at staging so a mistake costs nothing:
# - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --api.dashboard=true
- --log.level=INFO
- --accesslog=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=le
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS
gitea:
image: gitea/gitea:1 # major-only pin keeps this demo copy-pasteable; pin an exact release in production
restart: unless-stopped
volumes:
- ./gitea:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000docker compose up -d، ثم docker compose logs -f traefik. كل تطبيق إضافي هو نسخة من الكتلة gitea، مع اسم router خاص به، وHost() خاص به، ومنفذ داخلي خاص به. ويمكنك إضافة تثبيت Nextcloud يعمل داخل Docker مع TLS ونسخ احتياطية بالطريقة نفسها: احذف منافذه المنشورة، وأوصله بـproxy، ودع labels الخاصة بالـrouter تتولى اسم المضيف والشهادة.
هناك خمس نقاط مهمة في هذا الإعداد.
تجعل exposedByDefault=false الحاوية غير مرئية لـTraefik إلى أن تحمل traefik.enable=true. إذا حذفت هذا الإعداد، فسيُنشئ Traefik مساراً لكل حاوية تبدأها، بما في ذلك حاوية postgres المؤقتة التي شغّلتها للتحقق من أمر ما.
يخبر providers.docker.network=proxy Traefik بالشبكة التي يجب استخدامها عندما تكون الحاوية متصلة بعدة شبكات. إذا حذفته، فقد يختار Traefik عنوان IP الخاطئ للحاوية، ويظهر ذلك في صورة خطأ 502 يبدو كأنه خلل في التطبيق.
يمثّل loadbalancer.server.port=3000 المنفذ داخل الحاوية؛ إذ يستمع Gitea على المنفذ 3000 هناك. لاحظ أن أياً من حاويات التطبيقات لا ينشر منفذاً، بل Traefik وحده يفعل ذلك.
يحوّل redirect في نقطة الدخول web الطلبات النصية غير المشفّرة إلى استجابة 308 عبر HTTPS. ويظل المنفذ 80 مفتوحاً على أي حال، لأن تحدي ACME عبر HTTP يحتاج إليه، وكذلك المستخدمون الذين يكتبون اسم مضيف مجرداً.
تكرار $$ في تجزئة basic-auth هو طريقة Compose لتهريب المحارف، وليس خطأً مطبعياً. أنشئها باستخدام htpasswd -nbB admin 'your-password' من الحزمة apache2-utils، ثم كرّر كل $.
الشهادة وفخ acme.json
tlschallenge=true يحدد TLS-ALPN-01: يتصل Let's Encrypt بخادمك على المنفذ 443، ويستجيب Traefik للتحدي داخل مصافحة TLS. البديل هو HTTP-01 على المنفذ 80. استبدل السطر tlschallenge في قائمة command: الخاصة بـTraefik بهذين السطرين:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webكلا الخيارين يعمل. ويتطلب كلاهما أن يشير DNS العام لاسم المضيف إلى VPS الخاص بك مسبقاً، وأن تتمكن جهة إصدار الشهادات من حل الاسم والاتصال به من خارج الشبكة. أنشئ سجل A وسجل AAAA أولاً، وتحقق منهما باستخدام dig +short git.example.com، ثم شغّل Traefik.
إليك الفخ الذي يبدد ساعة من العمل. يحتفظ Traefik بمفتاح حساب ACME وكل شهادة صادرة في ملف acme.json واحد. إذا كان هذا الملف قابلاً للقراءة من المجموعة أو من جميع المستخدمين، يطبع Traefik سطراً قريباً جداً من هذا ثم يتوقف:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600الحل الصحيح هو ما ورد أعلاه: اربط الدليل باستخدام bind mount، ودَع Traefik ينشئ الملف بنفسه بالوضع الصحيح. إذا أنشأت acme.json باستخدام touch، فإن umask جعله بالوضع 644. أصلحه على الخادم المضيف:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikأنشئ نسخة احتياطية من هذا الدليل مع volumes الخاصة بتطبيقك. فقدانه يمكن معالجته، إذ ستُصدَر الشهادات من جديد، لكن إعادة إصدار شهادات خمسة أسماء مضيفين دفعة واحدة قد تؤدي إلى بلوغ حدود المعدل.
استخدم CA التجريبي أثناء التكرار. أزل التعليق من السطر caserver، وتأكد من عمل كل route، ثم أعد التعليق إلى السطر واحذف acme.json حتى يتم طلب شهادات الإنتاج من جديد. يسمح Let's Encrypt في بيئة الإنتاج بإصدار خمس شهادات مكررة أسبوعياً للمجموعة نفسها من أسماء المضيفين، كما يحد من عمليات التحقق الفاشلة المتكررة للاسم نفسه. تصدر بيئة Staging شهادات غير موثوقة، ويحذرك متصفحك منها. يشير هذا التحذير إلى نجاح العملية، كما أن حدودها أعلى بكثير.
لوحة المعلومات هي واجهة تحكم وليست عرضاً تجريبياً
تضبط معظم أدلة البدء السريع --api.insecure=true، ما يتيح عرض لوحة المعلومات على المنفذ 8080 من دون مصادقة. وعلى خادم يملك عنوان IP عاماً، يكشف ذلك بنية التوجيه وأسماء المضيفين وأسماء الـmiddleware ومنافذ الواجهات الخلفية لأي شخص يفحص الخادم.
تُعدّ التسميات الموجودة على خدمة traefik أعلاه البديل المناسب: توجّه لوحة المعلومات مثل أي تطبيق آخر، عبر اسم مضيف فعلي واتصال TLS، وخلف basicauth. وتربط service=api@internal الموجّه بواجهة API المضمّنة في Traefik. ويمكنك تشديد الحماية أكثر بربط قائمة سماح لعناوين IP، وتطبيق عناصرها من اليسار إلى اليمين. إذا كان عنوان مكتبك ديناميكياً، فاضبط النطاق على الشبكة الفرعية التي توزّعها شبكة WireGuard VPN تستضيفها بنفسك على VPS نفسه، ولا تصل إلى لوحة المعلومات إلا عبر النفق:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authتصبح كلمة مرور basicauth مشتركة غير قابلة للدفاع عنها عندما تحتاج خمسة تطبيقات إلى حساباتها الخاصة. وتتيح خانة الـmiddleware نفسها استخدام forwardauth لإحالة قرار المصادقة إلى Authentik، وهو خادم تسجيل دخول موحّد تستضيفه بنفسك، بحيث تصبح لوحة المعلومات وكل route مجاور لها خلف عملية تسجيل دخول واحدة يمكنك إبطالها من مكان واحد.
مقبس Docker يملك صلاحيات root
/var/run/docker.sock هي واجهة API يمكنها إنشاء حاوية تُجري mount لـ / من المضيف. ويعادل الوصول إليها امتلاك صلاحيات root على الجهاز، ويحتاج Traefik إليها لقراءة labels.
أبقِ :ro على mount، لكن وضّح ما الذي يحققه: فهو يجعل ملف المقبس للقراءة فقط. ولا يمنع طلبات POST إلى Docker API من المرور عبره. التخفيف الفعلي هو عدم منح Traefik الوصول إلى المقبس مطلقاً، ووضع proxy يرشّح الطلبات بينهما:
dockerproxy:
image: tecnativa/docker-socket-proxy # pin the current tag
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxyاحذف volume الخاص بالمقبس من Traefik، ووجّه provider إلى proxy:
--providers.docker.endpoint=tcp://dockerproxy:2375يحتفظ Traefik بإمكانية القراءة من الحاويات والشبكات، ويفقد القدرة على إنشاء أي شيء.
جدار الحماية والمنافذ والقاعدة التي يخطئ فيها الجميع
منفذان مفتوحان، إضافة إلى SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableالمنافذ التي ينشرها Docker تتجاوز ufw. يضيف Docker قواعده الخاصة إلى iptables، وتُقيَّم هذه القواعد قبل سلاسل ufw. لذلك يمكن الوصول من الإنترنت إلى حاوية شُغِّلت باستخدام ports: ["3000:3000"]، حتى مع وجود قاعدة deny في ufw لذلك المنفذ. الدفاع هنا بنيوي، وليس إعداداً لجدار الحماية: انشر المنافذ من Traefik فقط، وامنح كل حاوية أخرى networks: [proxy] ولا شيء غيره. إذا احتاج شيء فعلاً إلى الوصول إلى المضيف، فاربطه بواجهة loopback، "127.0.0.1:3000:3000".
استكشاف الأخطاء وإصلاحها: أخطاء ستظهر لك فعلياً
404 page not found، وتعرضها Traefik. لم يطابق أي router الطلب. بالترتيب حسب الاحتمال: تفتقد الحاوية traefik.enable=true (مع ضبط exposedByDefault=false)؛ أو أن قاعدة Host() لا تطابق الاسم الذي كتبته؛ أو أن اسم router في إحدى التسميات يختلف عن اسمه في تسمية أخرى (يجب أن تكون routers.gitea.rule وrouters.gitea.entrypoints الكلمة نفسها)؛ أو أنك وضعت اسم المضيف بين علامتي اقتباس بدلاً من backticks. تتطلب Traefik v3 استخدام backticks داخل matchers.
502 Bad Gateway. طابق أحد الـrouters الطلب، لكن تعذّر الوصول إلى الbackend. في الغالب لا تكون الحاوية متصلة بشبكة proxy، فتحقق من docker inspect -f '{{json .NetworkSettings.Networks}}' gitea. والاحتمال الآخر هو وجود loadbalancer.server.port غير صحيح: إذ إنك أعطيتها منفذاً منشوراً، أو أن التطبيق يستمع على منفذ آخر. يذكر السجل محاولة الاتصال: dial tcp 172.18.0.5:8080: connect: connection refused.
يحذرك المتصفح، وتكون الشهادة صادرة إلى TRAEFIK DEFAULT CERT. لا توجد شهادة لاسم المضيف هذا، وقد قدّمت Traefik شهادتها المؤقتة الموقعة ذاتياً. اقرأ أسطر ACME:
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comلا يشير DNS إلى الخادم بعد. أصلح السجل، وانتظر انتهاء TTL، ثم أعد تشغيل Traefik.
Invalid response from http://git.example.com/.well-known/acme-challenge/... في تحدي HTTP: لا يصل المنفذ 80 إلى Traefik من خارج الخادم، وغالباً يكون السبب جداراً نارياً على مستوى المزوّد أمام VPS، وليس ufw.
لا تصدر الشهادات مطلقاً، ويستخدم DNS لديك Cloudflare مع تفعيل السحابة البرتقالية. تنهي Cloudflare اتصال TLS عند الحافة، ولا يمكن لـTLS-ALPN-01 الاكتمال عبرها. اضبط السجل على DNS-only أثناء الإصدار، أو بدّل إلى تحدي DNS-01 باستخدام API token. كما أن DNS-01 هو التحدي الوحيد الذي يصدر شهادات wildcard.
حلقة إعادة توجيه. تنهي جهة ما أمام Traefik اتصال TLS مسبقاً، ثم تمرّر اتصالاً غير مشفّر إلى :80؛ فتعيد إعادة التوجيه في entrypoint إرساله إلى HTTPS. أزل إحدى عمليتي إعادة التوجيه.
الإبقاء عليه قيد التشغيل
يجب تفعيل وحدة Docker عند الإقلاع (systemctl is-enabled docker)، ويعيد restart: unless-stopped تشغيل الحزمة بعد إعادة التشغيل. وللحصول على تحكم صريح، أنشئ وحدة systemd صغيرة تشغّل docker compose -f /srv/edge/compose.yml up -d باستخدام RemainAfterExit=yes. يوفّر لك ذلك systemctl status edge والتحكم في ترتيب التشغيل.
ثبّت وسم Traefik (traefik:v3.5، وليس latest). غيّرت الترقية من v2 إلى v3 صياغة القواعد وأسماء المزوّدين. وقد يعيد latest تحميل إعداد لم يعد يفهمه دون أي تنبيه. نفّذ الترقية عن قصد: اقرأ ملاحظات الترحيل، وغيّر الوسم، ثم docker compose up -d traefik، وراقب السجل. إذا كنت لا تزال تستخدم وسم v2، فراجع دليل ترحيل Traefik من v2 إلى v3 الذي يشرح كل إعادة تسمية، ووضع التوافق، وطريقة التراجع التي تحافظ على شهاداتك.
أنشئ نسخة احتياطية من ./letsencrypt ومن وحدة تخزين بيانات كل تطبيق. لا يحتفظ Traefik بأي حالة أخرى لا يمكنك إعادة بنائها من ملف compose.
ما الذي يتعطل عند التوسع
السقف الأول ليس معدل النقل، بل الخادم الواحد: يشكّل Traefik واحد على VPS واحد نقطة فشل واحدة لخمسة تطبيقات، وacme.json هو تخزين في ملف مسطّح، لذلك ستؤدي كتابته من مثيلين لـTraefik إلى إفساد الملف. يتطلب التوسع الأفقي نقل تخزين الشهادات خارج ملف، أو إنهاء TLS في موضع آخر.
المشكلة الثانية هي الاتصالات طويلة الأمد. تتعارض Server-sent events والتحميلات الكبيرة والعملاء البطيئون مع مهلات الاستجابة الخاصة بـentrypoint؛ وتتحكم --entryPoints.websecure.transport.respondingTimeouts.readTimeout وwriteTimeout وidleTimeout المرتبطة بها في هذه المهلات. تمر WebSockets من دون إعداد إضافي.
المشكلة الثالثة هي القرص. يكتب --accesslog=true إلى stdout، ويحتفظ Docker بـjson-file driver بهذه البيانات إلى الأبد ما لم تحدد حداً لها. اضبط logging.options.max-size على خدمة Traefik، أو اكتب access log إلى ملف ودوّره دورياً.
لا يتطلب أي من ذلك orchestrator. لكنه يتطلب خادماً تتحكم فيه، مع عنوان IP فعلي والمنفذين 80 و443 مفتوحين للعالم؛ ويكفي VPS صغير واحد لتوفير جميع الاعتمادات.
FAQ
هل ما زلت أحتاج إلى certbot إذا كنت أشغّل Traefik؟
لا. يطلب محلّل ACME في Traefik الشهادة ويجدّدها لكل اسم مضيف يوجّه إليه، ويخزّن جميع الشهادات في acme.json. يظل Certbot الأداة المناسبة عندما ينهي nginx أو خادم آخر TLS بنفسه. تشغيل الأداتين مع أسماء المضيفين نفسها يستنفد حدود المعدل في Let's Encrypt فقط.
لماذا يعيد الحاوي 404 عبر Traefik؟
تعني استجابة 404 التي يقدّمها Traefik أنه لم يطابق أي موجه الطلب على الإطلاق. تحقق من أن الحاوي يتضمن traefik.enable=true، وهو مطلوب بعد ضبط exposedByDefault=false، وأن قيمة Host() تطابق الاسم الذي أدخلته، وأن اسم الموجه متطابق في كل label خاص بهذا التطبيق. يتطلب Traefik v3 أيضاً استخدام backticks داخل أداة المطابقة، وليس علامات الاقتباس.
ما الفرق بين 404 و502 في هذه الحالة؟
تعني 404 أن التوجيه لم يحدث. أما 502 فتعني أن أحد الموجهات طابق الطلب، لكن الخدمة الخلفية رفضت الاتصال. أكثر أسباب 502 شيوعاً هي أن الحاوي غير متصل بشبكة proxy، وأن loadbalancer.server.port يشير إلى منفذ منشور بدلاً من المنفذ الذي يستمع إليه التطبيق داخل الحاوي. يذكر access log العنوان المحدد الذي اتصل به Traefik.
هل يكفي تركيب Docker socket بوضع القراءة فقط؟
يجعل الخيار :ro ملف socket للقراءة فقط، لكنه لا يجعل واجهة API التي خلفه للقراءة فقط. تظل طلبات POST تمر عبره، ويعادل الوصول إلى Docker API امتلاك root على الخادم. الترتيب الأقوى هو الحاوي docker-socket-proxy الموضح أعلاه. فهو يتيح لـTraefik قراءة الحاويات والشبكات فقط، ويمنع عمليات الكتابة تماماً.
هل يستطيع Traefik إصدار شهادة wildcard؟
نعم، ولكن عبر تحدي DNS-01 فقط، مع استخدام API token لدى مزود DNS الخاص بك. يتحقق كل من TLS-ALPN-01 وHTTP-01 من اسم مضيف واحد، ولا يمكن لأي منهما إصدار شهادة wildcard. يُستخدم DNS-01 أيضاً عندما ينهي CDN مثل Cloudflare TLS أمام VPS الخاص بك، ولا يكتمل التحديان الآخران.