SSD Nodes Learn
الأدلة Matt Connorبقلم Matt Connor · آخر تحديث في 2026-07-22

وكيل Traefik العكسي: 5 تطبيقات، Compose واحد

شغّل خمس تطبيقات خلف عنوان IP واحد عبر Traefik v3 على Docker Compose: توجيه بقاعدة Host، شهادات TLS تلقائية من Let's Encrypt، وفخ acme.json يمنع الإقلاع.

عنوان IP واحد، خمس تطبيقات، منفذ 443 واحد

يملك خادم VPS الخاص بك عنوان IPv4 عامًا واحدًا ومنفذ TCP واحدًا هو 443. تريد أن تشغّل عليه Gitea، ونسخة تجريبية (staging) من تطبيقك، ولوحة تحكم داخلية، وصفحة حالة، ومستقبِل webhooks: خمسة أسماء مضيف (hostnames)، وجهاز واحد. الوكيل العكسي (reverse proxy) هو العملية التي تملك المنفذين 80 و443، وتقرأ ترويسة Host في كل طلب، وتُسلِّمه إلى الحاوية الصحيحة. هذا بالضبط ما يفعله Traefik، فهو يحصل على شهادة لكل اسم مضيف ويجدّدها دون أن تشغّل certbot يدويًا ولو مرة واحدة.

ما يفصل Traefik عن كتلة server {} في nginx هو من أين تأتي إعداداته. مع nginx تحرّر ملفًا ثم تعيد التحميل، وتبقى دورة حياة الشهادة مهمة منفصلة — وهي بالضبط الخطوات التي تتبعها حين تُصدر شهادات Let's Encrypt بواسطة certbot على nginx، حيث يعمل مؤقّت التجديد خارج خادم الويب كليًا. أما مزوّد Docker الخاص بـ Traefik فيراقب تدفق أحداث Docker ويقرأ labels (الوسوم) الموضوعة على حاوياتك: شغّل حاوية تحمل وسم قاعدة Host() وتصبح قابلة للتوجيه خلال ثانية واحدة؛ أوقفها فيختفي المسار. وهذا هو الفخ أيضًا. الإعدادات التي تكمن في الوسوم تكمن في خمسة أماكن في آنٍ واحد، والوسم الخاطئ صامت: الحاوية ببساطة لا تُوجَّه، وTraefik لا يقول شيئًا.

الأسماء الأربعة

  • Entrypoints (نقاط الدخول) هي مقابس استماع (sockets). ستُعرّف اثنتين: web على :80 وwebsecure على :443.
  • Routers (الموجّهات) تطابق طلبًا (Host(...)) وتربطه بخدمة. تُطلَب الشهادات لكل موجّه على حدة، عبر tls.certresolver.
  • Services (الخدمات) هي الخلفية (backend): حاوية والمنفذ الذي تستمع عليه داخل شبكة Docker.
  • Middlewares توضع بين الموجّه والخدمة: المصادقة الأساسية (basic auth)، قوائم السماح لعناوين IP، إعادة كتابة الترويسات، عمليات إعادة التوجيه.

تُمرَّر الإعدادات الساكنة (نقاط الدخول، والمزوّدات، وACME) عبر سطر أوامر Traefik أو داخل traefik.yml، وتغييرها يعني إعادة تشغيل Traefik. أما الإعدادات الديناميكية (الموجّهات، والخدمات، وmiddlewares) فتصل من وسوم الحاويات وتُعاد تحميلها فورًا دون إعادة تشغيل. الخلط بين النوعين هو السبب المعتاد وراء «الخيار الذي لا يفعل شيئًا».

ملف Compose

شبكة Docker واحدة مشتركة باسم proxy هي العمود الفقري. لا يصل 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=3000

نفّذ docker compose up -d، ثم docker compose logs -f traefik. كل تطبيق إضافي هو نسخة من كتلة gitea باسم موجّه خاص بها، وقاعدة Host() خاصة بها، ومنفذ داخلي خاص بها. وتثبيت Nextcloud يعمل داخل Docker مع TLS ونسخ احتياطية يندمج بالطريقة نفسها: أزل منافذه المنشورة، وأرفقه بشبكة proxy، ودع وسوم الموجّه تتولى اسم المضيف والشهادة.

خمسة تفاصيل هناك تستحق الشرح.

exposedByDefault=false يجعل الحاوية غير مرئية لـ Traefik حتى تحمل traefik.enable=true. أغفل هذا الخيار وستحصل كل حاوية تشغّلها مستقبلًا — بما فيها حاوية postgres المؤقتة التي شغّلتها للتحقق من شيء ما — على مسار يُنشأ لها تلقائيًا.

providers.docker.network=proxy يخبر Traefik بالشبكة التي عليه استخدامها حين تكون الحاوية مرتبطة بأكثر من شبكة. أغفل هذا الخيار وقد يختار Traefik عنوان IP خاطئًا للحاوية، وهو ما يظهر على هيئة خطأ 502 يبدو وكأنه عطل في التطبيق.

loadbalancer.server.port=3000 هو المنفذ داخل الحاوية؛ فـ Gitea تستمع على المنفذ 3000 هناك. لاحظ أن أي حاوية تطبيق لا تنشر منفذًا على الإطلاق — Traefik وحده من يفعل ذلك.

إعادة التوجيه على نقطة الدخول web تحوّل طلبات HTTP غير المشفّرة إلى استجابة 308 نحو HTTPS. يبقى المنفذ 80 مفتوحًا رغم ذلك: يحتاجه تحدي ACME عبر HTTP، ويحتاجه أيضًا البشر الذين يكتبون اسم المضيف مجردًا من أي بروتوكول.

علامة $$ المضاعفة في تجزئة (hash) المصادقة الأساسية هي إفلات (escaping) خاص بـ 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 الخاص بك — فسلطة إصدار الشهادات (certificate authority) تحلّ الاسم وتتصل من الخارج. أنشئ سجل 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 ينشئ الملف بنفسه بالوضع (mode) الصحيح. وإذا كنت قد أنشأت acme.json بالأمر touch، فإن umask لديك جعله بالوضع 644. أصلحه على المضيف:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

احتفظ بنسخة احتياطية من هذا المجلد مع وحدات تخزين تطبيقاتك (volumes). فقدانه أمر يمكن تجاوزه — فالشهادات تُعاد إصدارها — لكن إعادة إصدار خمسة أسماء مضيف دفعة واحدة تقودك مباشرة إلى حدود المعدّل (rate limits).

استخدم سلطة الإصدار التجريبية (staging) أثناء التكرار. أزل التعليق عن سطر caserver، واجعل كل مسار يعمل، ثم أعد التعليق عليه واحذف acme.json كي تُطلَب شهادات الإنتاج من جديد. يسمح Let's Encrypt في وضع الإنتاج بخمس شهادات مكررة أسبوعيًا لمجموعة متطابقة من أسماء المضيف، ويحدّ من محاولات التحقق الفاشلة المتكررة لنفس الاسم. أما staging فيصدر شهادات غير موثوقة — يحذّرك المتصفح، وهذا التحذير هو الدليل على أن الأمر نجح — بحدود أوسع بكثير.

لوحة التحكم سطح تحكم، لا عرض توضيحي

تضبط معظم الأدلة السريعة الخيار --api.insecure=true، الذي يقدّم لوحة التحكم على المنفذ 8080 دون أي مصادقة. وعلى جهاز بعنوان IP عام، هذا يسلّم بنية التوجيه لديك، وأسماء المضيف، وأسماء middlewares، ومنافذ backend إلى أي شخص يمسح الجهاز.

الوسوم الموضوعة على خدمة 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

مقبس Docker يعني root

/var/run/docker.sock هو واجهة برمجية (API) قادرة على إنشاء حاوية تربط (mount) جذر نظام الملفات / الخاص بالمضيف. الوصول إليه يعادل الحصول على صلاحيات root على الجهاز، ويحتاجه Traefik لقراءة الوسوم.

أبقِ :ro على الربط (mount)، لكن كن واضحًا بشأن ما يمنحك إياه فعلًا: إنه يجعل ملف المقبس (socket) للقراءة فقط. وهو لا يمنع طلبات POST الموجَّهة إلى واجهة Docker من المرور عبره. المعالجة الحقيقية هي ألا تسلّم 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

أزل وحدة تخزين المقبس من Traefik ووجّه المزوّد نحو الوكيل (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"] قابلة للوصول من الإنترنت حتى مع وجود قاعدة رفض في ufw أمامها مباشرة. الدفاع هنا بنيوي، لا مسألة ضبط لجدار الحماية: انشر المنافذ من Traefik فقط، وامنح كل حاوية أخرى networks: [proxy] ولا شيء غيره. وإذا احتاج شيء ما فعلًا للوصول إلى المضيف، فاربطه بـ loopback: "127.0.0.1:3000:3000".

استكشاف الأخطاء: الرسائل التي سترها فعلًا

404 page not found، يقدّمها Traefik نفسه. لم يطابق أي موجّه الطلب. بترتيب الاحتمال: الحاوية تفتقر إلى traefik.enable=true (مع ضبط exposedByDefault=false)؛ أو قاعدة Host() لا تطابق الاسم الذي كتبته؛ أو اسم الموجّه في وسم يختلف عن اسم الموجّه في وسم آخر (يجب أن تكون routers.gitea.rule وrouters.gitea.entrypoints بالكلمة نفسها)؛ أو أنك وضعت اسم المضيف بين علامتي اقتباس بدلًا من backticks. يشترط Traefik v3 استخدام backticks داخل عبارات المطابقة (matchers).

502 Bad Gateway. طابق موجّه الطلب لكن تعذّر الوصول إلى 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. كما أن DNS-01 هو التحدي الوحيد الذي يصدر شهادات wildcard.

حلقة إعادة توجيه (redirect loop). شيء ما أمام Traefik ينهي TLS بالفعل ويمرّر حركة HTTP غير مشفّرة إلى :80؛ فتعيد نقطة الدخول (entrypoint) توجيه الطلب إلى HTTPS. أزل إحدى إعادتي التوجيه.

إبقاؤه يعمل

يجب أن تكون وحدة Docker مفعّلة عند الإقلاع (systemctl is-enabled docker)، ويعيد restart: unless-stopped تشغيل المكدّس (stack) بعد كل إعادة إقلاع. وللحصول على تحكّم صريح، تمنحك وحدة 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.

ما الذي ينهار عند التوسّع

السقف الأول ليس الإنتاجية (throughput)، بل الجهاز الواحد: نسخة واحدة من Traefik على VPS واحد هي نقطة فشل وحيدة لخمسة تطبيقات، وملف acme.json تخزين بصيغة ملف مسطّح (flat file) — وكتابة نسختين من Traefik فيه في آنٍ واحد ستُتلفه. التوسّع الأفقي يعني نقل تخزين الشهادات خارج ملف واحد، أو إنهاء TLS في مكان آخر.

الثاني هو الاتصالات طويلة الأمد. الأحداث المرسلة من الخادم (server-sent events)، والتحميلات الكبيرة، والعملاء البطيئون تصطدم كلها بمهلات استجابة نقطة الدخول (entrypoint)؛ والخيارات المتحكّمة بذلك هي --entryPoints.websecure.transport.respondingTimeouts.readTimeout وشقيقاه writeTimeout وidleTimeout. أما WebSockets فتمر دون أي إعداد إضافي.

الثالث هو القرص. يكتب --accesslog=true إلى stdout، ويحتفظ مشغّل json-file الخاص بـ Docker بذلك إلى الأبد ما لم تضع سقفًا له. اضبط logging.options.max-size على خدمة Traefik، أو اكتب سجلّ الوصول إلى ملف ودوّره (rotate) دوريًا.

لا شيء من هذا يحتاج إلى أداة تنسيق حاويات (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() تطابق الاسم الذي كتبته، وأن اسم الموجّه متطابق في كل وسم خاص بهذا التطبيق. كما يشترط Traefik v3 استخدام backticks داخل عبارة المطابقة، لا علامتي الاقتباس.

ما الفرق بين 404 و502 هنا؟

الرمز 404 يعني أن التوجيه لم يحدث أصلًا؛ والرمز 502 يعني أن موجّهًا طابق الطلب لكن backend رفض الاتصال. المرشّحان المعتادان لخطأ 502 هما حاوية غير مرتبطة بشبكة proxy، أو loadbalancer.server.port يشير إلى منفذ منشور بدلًا من المنفذ الذي يستمع عليه التطبيق داخل الحاوية. يوضح سجلّ الوصول العنوان الدقيق الذي حاول Traefik الاتصال به.

هل يكفي ربط مقبس Docker للقراءة فقط؟

الخيار :ro يجعل ملف المقبس للقراءة فقط، لا الواجهة البرمجية (API) خلفه — فطلبات POST ما زالت تمر عبره، والوصول إلى واجهة Docker يعادل صلاحيات root على المضيف. الترتيب الأقوى هو حاوية docker-socket-proxy الموضحة أعلاه، التي لا تكشف لـ Traefik إلا عمليات القراءة الخاصة بالحاويات والشبكات، وتمنع الكتابة تمامًا.

هل يستطيع Traefik إصدار شهادة wildcard؟

فقط عبر تحدي DNS-01، باستخدام رمز API لمزوّد DNS الخاص بك. يتحقق كل من TLS-ALPN-01 وHTTP-01 من اسم مضيف واحد فقط ولا يمكنهما إنتاج شهادة wildcard. وDNS-01 هو أيضًا الحل حين تُنهي شبكة توصيل محتوى (CDN) مثل Cloudflare اتصال TLS أمام خادم VPS الخاص بك ولا يكتمل التحديان الآخران أبدًا.