الأدلة التقنية

HAProxy على Ubuntu: Reverse Proxy وLoad Balancer أمام خدماتك

عندما يعمل تطبيقك على أكثر من خادم، يوزع HAProxy الطلبات بينها ويستبعد المعطل منها تلقائياً. نثبته مع Docker Compose، ونكتب إعداده خطوة بخطوة، ونضيف شهادات HTTPS.

HAProxy على Ubuntu: Reverse Proxy وLoad Balancer أمام خدماتك

لنفرض أن تطبيقك يعمل على سيرفر Server واحد، وهذا هو الحل الأبسط الذي يبدأ به الجميع، ولكن المشكلة أن هذا السيرفر يصبح نقطة الفشل الوحيدة Single Point of Failure، فإذا توقف لتحديث النظام أو لعطل في القرص أو لأن عدد الزوار تضاعف فجأة فسوف تتوقف الخدمة كلها معه. وقد تفكر في زيادة موارد السيرفر نفسه Scale Up، وهذا يؤجل المشكلة ولا يحلها، والسبب أن السيرفر الأكبر يبقى سيرفراً واحداً يتوقف كله إذا توقف. لذلك فالحل المعتاد أن تشغل التطبيق على سيرفرين أو أكثر Scale Out، وتضع أمامها نقطة دخول واحدة تستقبل كل الطلبات وتوزعها عليها، وتفحص كل سيرفر باستمرار، فإذا تعطل أحدها استبعدته حتى يعود ويبقى الموقع يعمل بالباقي، وهذه هي وظيفة ال Load Balancer.

ويؤدي HAProxy هذه الوظيفة منذ أكثر من عشرين عاماً، وهو مفتوح المصدر Open Source ومعروف بسرعته وقلة ما يستهلكه من المعالج CPU والذاكرة RAM، ويعمل أيضاً Reverse Proxy عادياً حيث يستقبل الطلبات على المنفذين Ports رقم 80 و443، ويقرأ اسم النطاق Domain في كل طلب ويوجهه إلى الخدمة المعنية، ويتولى كذلك إنهاء TLS أي TLS Termination، وبالتالي تصل الطلبات إلى التطبيقات داخل شبكتك عبر HTTP عادي ولا يحتاج كل تطبيق إلى شهادة خاصة به.

وفي هذا الدليل سوف نشغل HAProxy على Ubuntu 24.04 من الـ Image الرسمي مع Docker Compose، ثم نكتب ملف إعداد Configuration File يوجه نطاقين إلى مجموعتين من السيرفرات، ومعه فحوص الصحة Health Checks وشهادات Let's Encrypt وصفحة إحصاءات محمية.

وسوف نناقش في هذا المقال ما يلي:

  • متى يناسبك HAProxy ومتى يناسبك Nginx Proxy Manager أو Traefik.
  • أقسام ملف haproxy.cfg، ثم التثبيت بملف Compose وكتابة الإعداد وفحصه قبل التشغيل.
  • التوجيه حسب النطاق، وتوزيع الحمل Load Balancing مع فحوص الصحة، واستبعاد السيرفر المعطل تلقائياً.
  • شهادات Let's Encrypt عبر Certbot دون إيقاف الخدمة، ولماذا لا نعتمد على ACME المدمج في HAProxy بعد.
  • وضع TCP لقواعد البيانات، وصفحة الإحصاءات، وإعادة التحميل دون قطع الاتصالات، والسجلات.
  • التحقق من النجاح، والنسخ الاحتياطي، والتحديث، وأشهر المشكلات وحلولها.

متى يناسبك HAProxy، ومتى يناسبك غيره؟

اختر HAProxy إذا كان للتطبيق نفسه أكثر من سيرفر وتريد توزيع الحمل بينها، واختره أيضاً إذا احتجت إلى تمرير خدمات غير HTTP مثل قواعد البيانات Databases أو SSH في وضع TCP أي TCP Mode، وهو مناسب لمن يريد إعداداً نصياً صريحاً يحفظه في Git ويراجعه سطراً سطراً، ولمن يهمه الأداء تحت ضغط كبير.

وفي المقابل لن تجد في HAProxy بعض ما اعتدت عليه في أدوات أخرى، فلا توجد واجهة ويب Web UI للإعداد، ولا يصدر شهادات Let's Encrypt تلقائياً في إعداده الافتراضي، ولا يكتشف ال Containers من وسوم Docker أي Docker Labels، والجدول التالي يلخص الفرق:

المعيارHAProxyNginx Proxy ManagerTraefik
طريقة الإعدادملف نصي واحد haproxy.cfgواجهة ويبوسوم Docker وملفات YAML
شهادات Let's Encryptعبر أداة خارجية مثل Certbot، أو ACME المدمج وهو ما زال تجريبياًتلقائية من الواجهةتلقائية
اكتشاف الـ Containersلالا، تضيف كل مضيف يدوياًنعم، من الوسوم
توزيع الحمل وفحوص الصحةمتقدمة، وهي غرضه الأساسيمحدودةمتوفرة
وضع TCPنعمعبر Streamsنعم

فإذا كانت خدماتك كلها Containers على سيرفر واحد، فالأرجح أن Nginx Proxy Manager أو Traefik أيسر لك، والسبب أن ميزة HAProxy الكبرى هي توزيع الحمل على عدة سيرفرات وليس لديك هنا إلا سيرفر واحد، وتجد المقارنة المفصلة بين الثلاثة في مقارنة Nginx Proxy Manager وTraefik وHAProxy.

المتطلبات Requirements

  • سيرفر Ubuntu 24.04 عليه Docker وCompose، وإذا لم يكن جاهزاً فاتبع دليل تثبيت Docker على Ubuntu، ثم دليل تأمين خادم VPS من أول دخول.
  • الموارد Resources: لموقع صغير أو متوسط يكفي HAProxy معالج واحد ونحو 50 ميجابايت من الذاكرة.
  • نطاق تدير سجلات DNS أي DNS Records الخاصة به، وفي هذا الدليل يشير السجلان app.example.com وapi.example.com إلى العنوان العام Public IP للسيرفر 203.0.113.10.
  • سيرفرات التطبيق خلفه: 10.0.0.11 و10.0.0.12 لتطبيق app على المنفذ 8080، و10.0.0.21 لتطبيق api على المنفذ 3000، والأفضل أن تكون مع سيرفر HAProxy على شبكة خاصة Private Network حتى لا تمر الطلبات بينها عبر الإنترنت.
  • المنفذان 80/tcp و443/tcp مفتوحان من الإنترنت، ولاحظ أن المنفذ 80 ضروري لـالتحقق من ملكية النطاق عبر HTTP أي HTTP-01 Challenge حتى لو كانت كل خدماتك عبر HTTPS.
⚠️
لا يستمع على المنفذين 80 و443 في السيرفر الواحد إلا برنامج واحد، فإذا كان لديك Nginx Proxy Manager أو Traefik يعمل على هذا السيرفر فاختر أحدهما، أو ضع HAProxy على سيرفر مستقل أمام عدة سيرفرات، وهو الاستخدام الذي صمم له أصلاً.

مما يتكون ملف haproxy.cfg؟

ملف الإعداد مقسم إلى أقسام Sections، حيث يبدأ كل قسم بكلمة في أول السطر وتليها أسطر مزاحة بمسافات، والجدول التالي يبين وظيفة كل قسم:

القسموظيفته
globalإعدادات العملية (Process) كلها، مثل السجلات (Logs) والحد الأقصى للاتصالات وخيارات TLS
defaultsقيم افتراضية ترثها الأقسام التي تليه، مثل الوضع (mode http أو mode tcp) والمهل الزمنية (Timeouts)
frontendنقطة الاستقبال: المنافذ التي يستمع عليها، والقواعد التي تقرر إلى أين يذهب الطلب
backendمجموعة الخوادم التي تخدم الطلب، وطريقة توزيعه بينها، وفحص صحتها

والقواعد مبنية على قوائم التحكم ACLs، والقائمة هي شرط له اسم، مثل «اسم النطاق هو app.example.com» أو «المسار يبدأ بـ /api»، ثم تستخدم هذا الاسم في سطر مثل use_backend be_app if host_app، وبهذا الشكل تقرأ القاعدة كجملة عادية: أرسل الطلب إلى be_app إذا كان النطاق هو نطاق التطبيق.

التثبيت Installation

يصدر HAProxy فرعاً جديداً كل ستة أشهر تقريباً، والفروع ذات الرقم الزوجي هي فروع LTS أي Long Term Support وتتلقى الإصلاحات لسنوات، وآخرها الفرع 3.4 الذي صدر في يونيو 2026 ويستمر دعمه حتى الربع الثاني من 2031 كما تعرض الصفحة الرئيسية للمشروع. وسوف نثبت هنا الإصدار 3.4.6 الصادر في 28 سبتمبر 2026 برقمه الكامل وليس latest ولا 3.4، والسبب أن الوسم العام Tag يتغير مع كل إصدار جديد، فتجد نفسك يوماً ما تعمل بإصدار لم تختره بعد أول docker compose pull.

أين نضع ملفات HAProxy على السيرفر؟

sudo mkdir -p /opt/haproxy/conf /opt/haproxy/certs/sites
sudo chown -R $USER: /opt/haproxy
cd /opt/haproxy
  • conf/haproxy.cfg: ملف الإعداد.
  • certs/default.pem: شهادة افتراضية موقعة ذاتياً Self-signed، يعرضها HAProxy لمن يطلب نطاقاً لا يعرفه.
  • certs/sites/: شهادات النطاقات الحقيقية، ملف PEM لكل شهادة.

نولد كلمة مرور لصفحة الإحصاءات

الآن سوف نولد كلمة مرور عشوائية لصفحة الإحصاءات ونحفظها في .env، ويقرؤها HAProxy من متغير بيئة Environment Variable، وبالتالي لا تظهر في ملف الإعداد ولا في مستودع Git إذا حفظت فيه الإعداد:

echo "STATS_PASSWORD=$(openssl rand -hex 32)" > .env
chmod 600 .env

نبدأ بشهادة افتراضية Default Certificate

لا يستمع HAProxy على المنفذ 443 دون شهادة واحدة على الأقل، وشهادة Let's Encrypt لن تصدر قبل أن يعمل HAProxy نفسه، فنحن أمام مشكلة الدجاجة والبيضة، لذلك نبدأ بشهادة موقعة ذاتياً تبقى هي الشهادة الافتراضية:

openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes -days 3650 -subj "/CN=invalid" -keyout /tmp/default.key -out /tmp/default.crt
cat /tmp/default.crt /tmp/default.key > certs/default.pem
rm /tmp/default.key /tmp/default.crt
sudo chown 99:99 certs/default.pem
sudo chmod 600 certs/default.pem

ولاحظ أن HAProxy داخل الـ Image الرسمي يعمل بالمستخدم haproxy ومعرفه 99 وليس بالمستخدم root، لذلك نجعل ملفات المفاتيح الخاصة Private Keys ملكاً له وحده، فلا يقرؤها غيره على السيرفر.

ملف Compose

محتوى الملف /opt/haproxy/compose.yaml كما يلي:

services:
  haproxy:
    image: haproxy:3.4.6
    container_name: haproxy
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
      - "127.0.0.1:8404:8404"
    sysctls:
      net.ipv4.ip_unprivileged_port_start: 0
    environment:
      STATS_PASSWORD: ${STATS_PASSWORD}
    volumes:
      - ./conf:/usr/local/etc/haproxy:ro
      - ./certs:/etc/haproxy/certs:ro
    networks:
      - edge
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "5"

networks:
  edge:
    name: haproxy_edge
    driver_opts:
      com.docker.network.bridge.name: br-haproxy
    ipam:
      config:
        - subnet: 172.30.0.0/24
          gateway: 172.30.0.1

في الإعداد أعلاه لاحظ التالي:

  • الإعداد net.ipv4.ip_unprivileged_port_start: 0 يسمح لمستخدم غير root بالاستماع على المنفذين 80 و443 داخل الـ Container، ويوصي به توثيق الـ Image، ولا يغير شيئاً في نواة Kernel السيرفر لأنه يخص شبكة الـ Container وحدها.
  • ننشر منفذ صفحة الإحصاءات 8404 على 127.0.0.1 فقط، فلا يصل إليه أحد من الإنترنت.
  • نربط مجلد conf كاملاً وليس الملف وحده، والسبب أن أغلب المحررات تحفظ الملف بإنشاء نسخة جديدة منه، فإذا ربطت الملف مباشرة بقي الـ Container يرى النسخة القديمة ولن تفهم لماذا لا يطبق تعديلك.
  • نثبت شبكة Docker أي Docker Network على 172.30.0.0/24 ونسمي الجسر Bridge باسم br-haproxy، والسبب أن Certbot سوف يستمع على عنوان البوابة Gateway 172.30.0.1 عند تجديد الشهادات كما سيأتي، وإذا كانت هذه الشبكة مستخدمة عندك فاختر غيرها وعدل العنوان في كل الأقسام. وإذا أردت أن تفهم أنواع الشبكات في Docker وكيف يوزع نطاقات العناوين عليها، فقد شرحنا ذلك بالتفصيل في دليل شبكات Docker وأفضل الممارسات.
  • نحدد حجم سجلات Docker، والسبب أن HAProxy يكتب سطراً لكل طلب، ودون هذا الحد قد تملأ السجلات القرص بعد أسابيع.
  • لا يوجد في الـ Image أمر curl ولا أداة مشابهة، لذلك لم نضف healthcheck، وتراقب حالة السيرفرات خلفه من صفحة الإحصاءات.

ملف الإعداد

محتوى الملف /opt/haproxy/conf/haproxy.cfg كاملاً، وسوف نشرح أجزاءه في الأقسام التالية:

global
    log stdout format raw local0
    maxconn 4096
    stats socket /var/lib/haproxy/admin.sock mode 600 level admin
    ssl-default-bind-options ssl-min-ver TLSv1.2

defaults
    mode http
    log global
    option httplog
    option forwardfor
    timeout connect 5s
    timeout client 30s
    timeout server 30s
    timeout http-request 10s

frontend web
    bind :80
    bind :443 ssl crt /etc/haproxy/certs/default.pem crt /etc/haproxy/certs/sites/ alpn h2,http/1.1

    acl acme     path_beg /.well-known/acme-challenge/
    acl host_app hdr(host) -i app.example.com
    acl host_api hdr(host) -i api.example.com

    http-request redirect scheme https code 301 if !{ ssl_fc } !acme
    http-request set-header X-Forwarded-Proto https if { ssl_fc }

    use_backend be_certbot if acme
    use_backend be_app if host_app
    use_backend be_api if host_api
    default_backend be_unknown

backend be_app
    balance roundrobin
    option httpchk
    http-check send meth GET uri /health ver HTTP/1.1 hdr Host app.example.com
    http-check expect status 200
    default-server inter 5s fall 3 rise 2
    server app1 10.0.0.11:8080 check
    server app2 10.0.0.12:8080 check

backend be_api
    server api1 10.0.0.21:3000 check

backend be_certbot
    server certbot 172.30.0.1:8888

backend be_unknown
    http-request return status 404 content-type text/plain string "unknown host\n"

frontend stats
    bind :8404
    stats enable
    stats uri /
    stats refresh 10s
    stats auth admin:"${STATS_PASSWORD}"

في الإعداد أعلاه لاحظ التالي:

  • يرسل log stdout format raw السجلات إلى المخرج القياسي Standard Output، فتقرؤها بالأمر docker compose logs ولا تحتاج إلى سيرفر syslog.
  • نكتب maxconn صراحة، والسبب أنه إذا غاب حسبه HAProxy من حدود النظام، وقد يخرج برقم أكبر بكثير مما يحتمله السيرفر فعلاً.
  • يضيف option forwardfor الترويسة Header X-Forwarded-For وفيها عنوان الزائر الحقيقي، ويضيف سطر set-header الترويسة X-Forwarded-Proto: https، فيعرف التطبيق أن الزائر جاء عبر HTTPS ولا يحاول تحويله مرة أخرى.
  • يفعل alpn h2,http/1.1 بروتوكول HTTP/2 للمتصفحات التي تدعمه.
  • يعيد be_unknown الرمز 404 لأي نطاق لم تعرفه ولا يمرره إلى أي تطبيق، وبالتالي لا يصل إلى تطبيقك من يطلب السيرفر بعنوانه أو بنطاق غريب يشير إليه.

افحص الإعداد قبل أن تشغل HAProxy

قبل التشغيل الأول افحص الملف بالأمر haproxy -c، وهذا الأمر يقرأ الإعداد ويتحقق منه ثم يخرج دون أن يستمع على أي منفذ:

docker compose run --rm --no-deps haproxy haproxy -c -V -f /usr/local/etc/haproxy/haproxy.cfg

والمخرج سوف يكون كما يلي:

Configuration file is valid

بعد ذلك شغل الخدمة وتابع السجل:

docker compose up -d
docker compose logs --tail 20 haproxy
[NOTICE]   (1) : Initializing new worker (8)
[NOTICE]   (1) : Loading success.

ويعمل HAProxy داخل الـ Container بوضع master-worker، أي عملية رئيسية Master Process تشرف على عمليات عاملة Worker Processes هي التي تخدم الطلبات، وبهذا الوضع تستطيع إعادة التحميل Reload دون قطع الاتصالات كما سنرى.

إذا أردت التثبيت من حزمة Ubuntu أو من PPA

إذا أردت تشغيل HAProxy كخدمة systemd عادية دون Docker، فحزمة Ubuntu 24.04 الرسمية فيها HAProxy 2.8، وهو فرع LTS أقدم، أما الفرع 3.4 فيوفره Vincent Bernat مشرف حزم HAProxy في Debian عبر مستودع PPA للفرع 3.4، وتجد أوامر إضافته بحسب نظامك في صفحة haproxy.debian.net. ويبقى ملف الإعداد نفسه صالحاً، ولكن موضعه يصبح /etc/haproxy/haproxy.cfg، وإعادة التحميل تكون بالأمر sudo systemctl reload haproxy.

التوجيه حسب النطاق وفحوص الصحة

في القسم frontend web يقرأ HAProxy الترويسة Host في كل طلب، فإذا كانت app.example.com تحقق الشرط host_app وذهب الطلب إلى be_app، وإذا كانت api.example.com ذهب إلى be_api، ولاحظ أن أسطر use_backend تطبق بالترتيب، فأول شرط يتحقق هو الذي يقرر ولا ينظر HAProxy إلى ما بعده.

وفي be_app يوزع balance roundrobin الطلبات على السيرفرين بالتناوب، والكلمة check في سطر كل سيرفر تجعل HAProxy يفحصه باستمرار، ومع option httpchk وhttp-check send يصبح الفحص طلب HTTP حقيقياً إلى المسار /health وليس مجرد فتح اتصال TCP، ثم يشترط http-check expect status 200 أن يعود الرمز 200. والفرق مهم، فالتطبيق قد يقبل الاتصال على المنفذ بينما هو عالق أو فقد اتصاله بقاعدة البيانات، وفحص TCP وحده سوف يعتبره سليماً ويستمر في إرسال الزوار إليه.

أما السطر default-server inter 5s fall 3 rise 2 فمعناه أن يفحص HAProxy كل سيرفر كل 5 ثوان، ويستبعده بعد ثلاثة فحوص فاشلة متتالية، ويعيده بعد فحصين ناجحين، فإذا توقف app1 خدم app2 الزوار وحده خلال 15 ثانية تقريباً، وظهر في السجل سطر مثل:

[WARNING]  (8) : Server be_app/app1 is DOWN, reason: Layer4 connection problem, info: "ECONNREFUSED returned by OS (Connection refused)", check duration: 0ms. 1 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.

غير المسار /health إلى مسار يعيد 200 في تطبيقك، والأفضل أن يتحقق هذا المسار من اتصال التطبيق بقاعدة بياناته ولا يعيد 200 دائماً، والسبب أن الفحص الذي ينجح دائماً لا يكشف شيئاً. وانتبه أيضاً للجلسات Sessions، فإذا حفظها التطبيق في ذاكرة السيرفر نفسه وليس في قاعدة بيانات أو Redis، فسوف يفقد المستخدم جلسته ويطلب منه الدخول من جديد حين ينتقل طلبه إلى السيرفر الآخر، وفي هذه الحالة استخدم balance source، أو اجعل الجلسات مشتركة بين السيرفرات وهو الحل الأنسب.

شهادات TLS مع Certbot

يحتاج HAProxy إلى ملف PEM واحد لكل شهادة فيه الشهادة والسلسلة Certificate Chain والمفتاح الخاص، والخيار crt /etc/haproxy/certs/sites/ يقرأ كل ملفات المجلد بالترتيب الأبجدي، ثم يختار HAProxy لكل زائر الشهادة التي تطابق النطاق الذي طلبه عبر SNI، أما الشهادة الأولى في السطر أي default.pem فيعرضها لمن لا تطابق طلبه أي شهادة.

وسوف نصدر الشهادة بـ Certbot في وضع standalone، وفي هذا الوضع يشغل Certbot سيرفر ويب صغيراً مؤقتاً يرد على طلب التحقق HTTP-01، ولكن المنفذ 80 محجوز لـ HAProxy، لذلك يستمع Certbot على 172.30.0.1:8888، ويمرر إليه HAProxy كل طلب يبدأ بـ /.well-known/acme-challenge/ عبر be_certbot، وبهذا الشكل لا تتوقف الخدمة أثناء الإصدار ولا أثناء التجديد.

تثبيت Certbot

تعليمات Certbot الرسمية توصي بتثبيته عبر snap، وتضيف الحزمة مؤقتاً Timer يفحص الشهادات مرتين يومياً ويجدد ما اقترب انتهاؤه:

sudo snap install --classic certbot
sudo ln -s /snap/bin/certbot /usr/bin/certbot
certbot --version

ولاحظ أن طلب التحقق يأتي من الـ Container إلى السيرفر نفسه، فإذا كانت سياسة جدار الحماية Firewall أي UFW هي رفض الوارد فسوف يمنعه، لذلك اسمح بالمنفذ 8888 من جسر HAProxy وحده:

sudo ufw allow in on br-haproxy to 172.30.0.1 port 8888 proto tcp

سكربت يجهز الشهادة بعد كل تجديد Deploy Hook

يحفظ Certbot الشهادة والمفتاح في ملفين منفصلين داخل /etc/letsencrypt/live/ ولا يقرؤهما إلا root، لذلك نكتب سكربت Deploy Hook يجمعهما في ملف PEM واحد داخل certs/sites/، ثم يفحص الإعداد ويعيد تحميل HAProxy. وبعد كل إصدار أو تجديد ناجح يشغل Certbot كل ملف تنفيذي في المجلد /etc/letsencrypt/renewal-hooks/deploy/ كما يشرح توثيق Certbot، ويضع مسار الشهادة في المتغير RENEWED_LINEAGE.

محتوى الملف /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh:

#!/bin/sh
set -e
name=$(basename "$RENEWED_LINEAGE")
dir=/opt/haproxy/certs/sites
cat "$RENEWED_LINEAGE/fullchain.pem" "$RENEWED_LINEAGE/privkey.pem" > "$dir/.$name.pem.tmp"
chown 99:99 "$dir/.$name.pem.tmp"
chmod 600 "$dir/.$name.pem.tmp"
mv "$dir/.$name.pem.tmp" "$dir/$name.pem"
cd /opt/haproxy
docker compose exec -T haproxy haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
docker compose kill -s USR2 haproxy
sudo chmod 755 /etc/letsencrypt/renewal-hooks/deploy/haproxy.sh

في السكربت أعلاه لاحظ التالي:

  • اسم الملف المؤقت يبدأ بنقطة، والسبب أن HAProxy يتجاهل هذه الملفات عند قراءة المجلد، ثم ينقله mv إلى اسمه النهائي دفعة واحدة، فلا يقرأ HAProxy ملفاً نصف مكتوب.
  • الترتيب fullchain.pem ثم privkey.pem يضع شهادة النطاق أولاً، ثم الشهادة الوسيطة Intermediate Certificate، ثم المفتاح، وهذا هو الترتيب الذي يطلبه HAProxy كما سنوضح في قسم المشكلات.
  • إذا فشل الفحص أوقف set -e السكربت قبل إعادة التحميل وأعاد Certbot رسالة خطأ، وبالتالي يبقى HAProxy يعمل بالإعداد القديم السليم.

إصدار الشهادة الأولى

تأكد أولاً أن السجلين يشيران إلى السيرفر بالأمر dig +short app.example.com وأن HAProxy يعمل، ثم اطلب شهادة واحدة للنطاقين:

sudo certbot certonly --standalone --http-01-address 172.30.0.1 --http-01-port 8888 -d app.example.com -d api.example.com -m [email protected] --agree-tos --no-eff-email

يحفظ Certbot هذه الخيارات في /etc/letsencrypt/renewal/app.example.com.conf ويستخدمها في كل تجديد، ومنذ الإصدار 5.3.0 يشغل Certbot سكربتات المجلد deploy مع الإصدار الأول عبر certonly أيضاً وليس مع التجديد فقط، فيظهر الملف certs/sites/app.example.com.pem ويعيد HAProxy التحميل مباشرة. بعد ذلك اختبر التجديد دون إصدار شهادة حقيقية:

sudo certbot renew --dry-run
💡
تفرض Let's Encrypt حدوداً على عدد الطلبات وعلى المحاولات الفاشلة، لذلك إذا فشل الإصدار فلا تكرر الأمر مرات متتالية، وإنما اقرأ رسالة Certbot وأصلح DNS أو المنفذ أولاً، وجرب بالخيار --dry-run قبل الطلب الحقيقي.

ماذا عن ACME المدمج في HAProxy؟

وقد يتساءل البعض: لماذا نحتاج إلى Certbot أصلاً وفي HAProxy عميل ACME مدمج ACME Client منذ الفرع 3.2؟ والإجابة أن التوثيق الرسمي للفرع 3.4 ما زال يصنفه ميزة تجريبية Experimental لا تعمل إلا مع السطر expose-experimental-directives في القسم global، رغم أنه يدعم التحقق عبر HTTP أي HTTP-01 Challenge والتحقق من ال DNS أي DNS-01 Challenge، وأضاف الفرع 3.4 طريقة التحقق dns-persist-01 التي يكفيها سجل TXT ثابت. والمشكلة الأكبر أن HAProxy لا يكتب الشهادات التي يصدرها إلى القرص، والسبب أن معماريته غير متزامنة Non-blocking ولا يصح أن يصل إلى القرص بعد تحميل الإعداد، فعليك أن تستخرجها بنفسك من الـ Stats Socket بالأمر dump ssl cert، وإلا ضاعت عند إعادة تشغيل الـ Container. لهذا نعتمد Certbot في بيئة الإنتاج Production، ومن أراد التجربة فالخطوات في دليل ACME في wiki المشروع.

وضع TCP للخدمات غير HTTP

في وضع TCP لا يقرأ HAProxy محتوى الاتصال وإنما يمرره كما هو إلى السيرفر خلفه، ولهذا يناسب قواعد البيانات وSSH وأي بروتوكول Protocol آخر. والمثال التالي يوجه PostgreSQL إلى سيرفر أساسي، وينتقل إلى سيرفر احتياطي إذا تعطل الأول، فأضفه في آخر haproxy.cfg:

defaults
    mode tcp
    log global
    option tcplog
    timeout connect 5s
    timeout client 1h
    timeout server 1h

frontend pg
    bind :5432
    default_backend be_pg

backend be_pg
    server db1 10.0.0.31:5432 check
    server db2 10.0.0.32:5432 check backup

في الإعداد أعلاه لاحظ أن الإضافة تبدأ بقسم defaults ثان، فترث منه الأقسام التي تليه وضع TCP ومهلاً أطول تناسب الاتصالات الطويلة، ولا يتغير شيء في أقسام HTTP السابقة، أما الكلمة backup فمعناها أن db2 لا يستقبل أي اتصال ما دام db1 سليماً. بعد ذلك انشر المنفذ على الشبكة الخاصة وحدها في compose.yaml، مثلاً بإضافة "10.0.0.5:5432:5432" إلى ports، حيث 10.0.0.5 هو عنوان سيرفر HAProxy على الشبكة الخاصة، ولا تفتح منفذ قاعدة البيانات على الإنترنت أبداً.

⚠️
لا يعرف HAProxy أي السيرفرين هو الأساسي في نسخ PostgreSQL المتماثل Replication، فإذا تعطل db1 وانتقلت الاتصالات إلى db2 فيلزمك أن تقوم بترقية db2 أي Promote إلى سيرفر أساسي يقبل الكتابة، عبر أداة مثل Patroni أو يدوياً.
📌
في 21 أكتوبر 2018 انقطع الاتصال بين مركز بيانات GitHub الأساسي في الساحل الشرقي الأمريكي وبقية شبكتها لمدة 43 ثانية فقط أثناء صيانة روتينية، ولكن أداة Orchestrator التي تدير قواعد بيانات MySQL نقلت الكتابة تلقائياً إلى مركز البيانات في الساحل الغربي، وكانت في الشرق كتابات لم تنسخ بعد إلى الغرب، فصار لدى GitHub نسختان مختلفتان من البيانات، واستمر تراجع الخدمة 24 ساعة و11 دقيقة حتى أعاد الفريق البيانات وطابقها كما يشرح تقرير GitHub عن الحادثة. والدرس أن نقل الاتصالات إلى قاعدة البيانات الاحتياطية هو الجزء السهل، أما التأكد من أن البيانات فيها كاملة قبل أن تقبل الكتابة فهو قرار يجب أن تخطط له مسبقاً، سواءً نفذته يدوياً أو بأداة.

صفحة الإحصاءات Stats Page

صفحة الإحصاءات تعرض كل frontend وbackend، وحالة كل سيرفر UP أو DOWN، وعدد الجلسات والأخطاء، وقد حميناها بطبقتين: المنفذ منشور على 127.0.0.1 فقط، والصفحة تطلب كلمة مرور عبر stats auth وتعيد الرمز 401 لمن لا يرسلها. ولفتحها من جهازك افتح نفق SSH أي SSH Tunnel، ثم افتح http://localhost:8404/ وادخل باسم admin وكلمة المرور الموجودة في .env:

ssh -L 8404:127.0.0.1:8404 [email protected]

ولاحظ أن HAProxy يقرأ المتغير ${STATS_PASSWORD} لأنه بين علامتي تنصيص مزدوجتين، وهذا شرط قسم متغيرات البيئة في التوثيق، فإذا كتبته دون تنصيص أو بين علامتين مفردتين فسوف تصبح كلمة المرور هي النص ${STATS_PASSWORD} نفسه. ولا تضف stats admin إلا إذا احتجت إلى تعطيل السيرفرات من الصفحة، والسبب أنه يعطي كل من يدخلها القدرة على إخراج سيرفر من الخدمة.

التحقق من الإعداد وإعادة التحميل دون انقطاع

بعد كل تعديل على haproxy.cfg افحصه داخل الـ Container العامل، ثم أرسل الإشارة USR2 إلى العملية الرئيسية:

cd /opt/haproxy
docker compose exec haproxy haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg && docker compose kill -s USR2 haproxy

وعند وصول الإشارة تقرأ العملية الرئيسية الإعداد الجديد وتنشئ عمليات عاملة جديدة، وتكمل العمليات القديمة الاتصالات المفتوحة ثم تخرج كما يشرح دليل الإدارة، وسوف ترى في السجل:

[WARNING]  (1) : Former worker (8) exited with code 0 (Exit)

وإذا وصل إلى HAProxy إعداد خاطئ رغم ذلك فسوف يرفضه ويستمر بالعمليات القديمة، ويكتب في السجل Failed to load worker، ولكن الـ Container لن يقلع بهذا الإعداد عند أول إعادة تشغيل للسيرفر، لذلك لا تتخط الفحص. ولا تستخدم docker compose restart لهذا الغرض، والسبب أنه يوقف العملية كلها ويقطع كل الاتصالات المفتوحة.

السجلات Logs

يكتب option httplog سطراً لكل طلب، وفي السطر عنوان الزائر، والـ frontend والـ backend، والسيرفر الذي خدم الطلب، والأزمنة، ورمز الحالة، والطلب نفسه:

docker compose logs -f haproxy
203.0.113.50:40458 [02/Oct/2026:09:15:59.325] web~ be_app/app2 0/0/1/3/4 200 512 - - ---- 1/1/0/0/0 0/0 "GET https://app.example.com/ HTTP/2.0"

والأرقام 0/0/1/3/4 هي بالترتيب زمن استقبال الطلب، وزمن الانتظار في الطابور Queue، وزمن الاتصال بالسيرفر، وزمن استجابته، والزمن الكلي بالمللي ثانية، ويشرح الحقول كلها قسم صيغة سجل HTTP. أما الحقل ---- فهو حالة إنهاء الجلسة Termination State، وتشرح رموزه صفحة حالة الجلسة عند الانقطاع، فإذا رأيت فيه SC مثلاً فمعناه أن السيرفر خلفه رفض الاتصال.

التحقق من النجاح

  • يعرض الأمر docker compose ps الـ Container haproxy بحالة Up.
  • يعود الأمر curl -I http://app.example.com بالرمز 301 وترويسة location: https://app.example.com/.
  • يعود الأمر curl -I https://app.example.com بالرمز 200، وإذا كررت الطلب فسوف ترى في السجل أن app1 وapp2 يخدمان الطلبات بالتناوب.
  • يعود الأمر curl -sk https://203.0.113.10/ -H 'Host: other.example.com' بالنص unknown host.
  • يعرض الأمر echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates شهادة Let's Encrypt بتاريخ انتهاء بعد نحو 90 يوماً.
  • تعرض صفحة الإحصاءات كل السيرفرات بحالة UP، والآن أوقف التطبيق على 10.0.0.11 فسوف يتحول app1 إلى DOWN خلال 15 ثانية ويبقى الموقع يعمل.
  • لا يستجيب العنوان http://203.0.113.10:8404 من خارج السيرفر.
  • يعرض الأمر systemctl list-timers | grep certbot مؤقت التجديد، وينجح الأمر sudo certbot renew --dry-run.

النسخ الاحتياطي Backup والاستعادة Restore

لا يحفظ HAProxy حالة تحتاج إلى نسخ، فكل ما تنسخه هو الإعداد والشهادات وإعداد Certbot:

sudo tar czf /root/haproxy-$(date +%F).tar.gz /opt/haproxy /etc/letsencrypt

في المجلد /opt/haproxy توجد الملفات compose.yaml و.env وconf وcerts، وفي /etc/letsencrypt توجد الشهادات ومفاتيح حساب Let's Encrypt أي Account Keys وإعدادات التجديد وسكربت ال Deploy Hook. ولأن ملف الإعداد نص عادي فاحفظه أيضاً في مستودع Git خاص، وبالتالي تعرف من غير ماذا ومتى عندما يتوقف شيء بعد تعديل.

وللاستعادة على سيرفر جديد ثبت Docker وCertbot، ثم فك النسخة في مواضعها، وأضف قاعدة UFW، وشغل الخدمة:

sudo tar xzf /root/haproxy-2026-10-02.tar.gz -C /
sudo ufw allow in on br-haproxy to 172.30.0.1 port 8888 proto tcp
cd /opt/haproxy
docker compose up -d
sudo certbot renew --dry-run

بعد ذلك غير سجلات DNS إلى العنوان الجديد، وتذكر أن النسخة فيها المفاتيح الخاصة لكل شهادة وكلمة مرور صفحة الإحصاءات، لذلك خزنها مشفرة Encrypted خارج السيرفر.

التحديث Upgrade إلى إصدار أحدث

  1. راجع سجل التغييرات Changelog للفرع 3.4، فالإصدارات التصحيحية Patch Releases داخل الفرع، مثل الانتقال من 3.4.6 إلى 3.4.7، فيها إصلاحات فقط وتحديثها آمن في العادة، أما الانتقال إلى فرع جديد فاقرأ إعلانه أولاً، والسبب أن بعض الكلمات تتغير أو تهمل Deprecated بين الفروع.
  2. خذ نسخة احتياطية كما سبق.
  3. قبل أن تغير شيئاً افحص إعدادك الحالي بالإصدار الجديد دون أن تمس الـ Container العامل:
cd /opt/haproxy
docker run --rm --env-file .env -v "$PWD/conf":/usr/local/etc/haproxy:ro -v "$PWD/certs":/etc/haproxy/certs:ro haproxy:3.4.7 haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
  1. إذا نجح الفحص فغير الوسم Tag في compose.yaml إلى الإصدار الجديد برقمه الكامل، وأعد إنشاء الـ Container:
docker compose pull
docker compose up -d
docker compose logs --tail 20 haproxy

وأثناء إعادة إنشاء الـ Container تنقطع الاتصالات المفتوحة بضع ثوان، لذلك اختر وقتاً قليل الزوار، وللرجوع Rollback أعد الوسم القديم ونفذ docker compose up -d من جديد.

مشكلات شائعة وحلولها

503 Service Unavailable: No server is available

معنى هذه الرسالة أن HAProxy استقبل الطلب واختار الـ backend، ولكنه لم يجد فيه سيرفراً سليماً واحداً، وفي السجل يظهر <NOSRV> مكان اسم السيرفر، لذلك افتح صفحة الإحصاءات أو ابحث في السجل عن سبب الاستبعاد:

docker compose logs haproxy | grep -E 'is DOWN|NOSRV' | tail -n 5
  • Layer4 connection problem أو Layer4 timeout: لا يصل HAProxy إلى المنفذ، فتحقق أن التطبيق يعمل ويستمع على 0.0.0.0 وليس على 127.0.0.1، وراجع جدار الحماية على سيرفر التطبيق، وجرب من سيرفر HAProxy بالأمر curl -i http://10.0.0.11:8080/health.
  • Layer7 wrong status: الفحص يصل ولكن التطبيق يعيد رمزاً غير 200، فربما المسار /health غير موجود، أو أن التطبيق يحول الطلب إلى صفحة الدخول بالرمز 302، والحل أن تغير مسار الفحص أو الرمز المتوقع.
  • إذا لم يظهر أي سيرفر DOWN فالأرجح أن الطلب ذهب إلى backend غير الذي تتوقعه، فراجع اسم النطاق في شروط acl.

HAProxy لا يقلع بعد تعديل الإعداد

إذا كان الـ Container يعيد التشغيل باستمرار فاقرأ السجل بالأمر docker compose logs --tail 30 haproxy، وسوف تجد أن رسائل [ALERT] تذكر الملف ورقم السطر، مثل parsing [/usr/local/etc/haproxy/haproxy.cfg:42]. وأشيع الأخطاء كلمة مكتوبة خطأً، أو use_backend يشير إلى backend غير موجود، أو ملف شهادة لا يقرؤه المستخدم 99، فأصلح الخطأ ثم افحص بالأمر docker compose run --rm --no-deps haproxy haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg حتى يخرج دون رسائل [ALERT].

inconsistencies between private key and certificate

يعتبر HAProxy أول شهادة في ملف PEM هي شهادة النطاق، ثم يتحقق أنها تطابق المفتاح الخاص، فإذا جاءت الشهادة الوسيطة أولاً رفض الملف بهذه الرسالة. والترتيب الصحيح هو شهادة النطاق ثم الشهادات الوسيطة ثم المفتاح، وهو ما يخرج من cat fullchain.pem privkey.pem. ولا تستخدم chain.pem وحده ولا cert.pem وحده، والسبب أن الأول تنقصه شهادة النطاق، والثاني تنقصه الشهادة الوسيطة فترفض بعض المتصفحات والتطبيقات الشهادة لأن السلسلة ناقصة، وتحقق من الملف بالأمر:

sudo openssl crl2pkcs7 -nocrl -certfile /opt/haproxy/certs/sites/app.example.com.pem | openssl pkcs7 -print_certs -noout

والسطر الأول في المخرج يجب أن يكون subject=CN=app.example.com، والذي يليه شهادة Let's Encrypt الوسيطة.

المنفذ 80 أو 443 مستخدم

إذا ظهرت عند التشغيل رسالة مثل Bind for 0.0.0.0:80 failed: port is already allocated أو address already in use فهناك برنامج آخر يستمع على المنفذ، وغالباً هو Nginx Proxy Manager أو Traefik، أو Nginx أو Apache مثبت من حزمة، فاعرف البرنامج أولاً:

sudo ss -ltnp | grep -E ':(80|443) '
docker ps --format '{{.Names}} {{.Ports}}' | grep -E ':(80|443)->'

ثم قرر: إما أن توقف البرنامج الآخر وتنقل خدماته إلى HAProxy، وإما أن تبقيه وتضع HAProxy على سيرفر مستقل، ولا تشغل الاثنين على المنفذين نفسيهما.

فشل التجديد بعد إضافة قاعدة تحويل

قد تضيف يوماً قاعدة تحول كل طلبات HTTP إلى HTTPS دون استثناء المسار /.well-known/acme-challenge/، أو تضع use_backend لنطاق قبل سطر be_certbot، وفي الحالتين لن يصل طلب Let's Encrypt إلى Certbot، وسوف تظهر في رسالة Certbot عبارة Invalid response أو Connection refused. والحل أن تبقي الشرط !acme في سطر التحويل، وتجعل use_backend be_certbot أول قاعدة توجيه، ثم تجرب sudo certbot renew --dry-run، ولاحظ أن هذا الخطأ لا يظهر يوم التعديل وإنما بعد أسابيع عندما يحين موعد التجديد، لذلك نفذ هذا الاختبار بعد كل تعديل على قسم frontend web.

الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

  • السيرفر الواحد نقطة فشل وحيدة، وزيادة موارده تؤجل المشكلة فقط، أما HAProxy أمام سيرفرين أو أكثر مع فحوص صحة حقيقية على مسار مثل /health فيستبعد السيرفر المعطل خلال ثوان ويبقى الموقع يعمل.
  • ثبت الإصدار برقمه الكامل مثل haproxy:3.4.6، وافحص الإعداد بالأمر haproxy -c قبل كل تشغيل أو إعادة تحميل، وأعد التحميل بالإشارة USR2 وليس بإعادة تشغيل الـ Container.
  • Certbot في وضع standalone خلف HAProxy يصدر الشهادات ويجددها دون إيقاف الخدمة، وسكربت ال Deploy Hook يجمع الشهادة والمفتاح بالترتيب الصحيح ويعيد التحميل، أما ACME المدمج فما زال تجريبياً في الفرع 3.4.
  • أبق صفحة الإحصاءات على 127.0.0.1 مع كلمة مرور من متغير بيئة، وافتحها عبر نفق SSH، ولا تضف stats admin دون حاجة.
  • في وضع TCP ينقل HAProxy الاتصالات إلى قاعدة البيانات الاحتياطية، ولكن ترقيتها والتأكد من اكتمال بياناتها مسؤوليتك أنت.

سجل التحديثات Changelog

  • أكتوبر 2026: كتابة الدليل.
نشرة عرب رووت | ArabRoot

معرفة تستحق مكاناً في بريدك.

مقالات مختارة وأدوات مفيدة وأفكار لمشروعك القادم، في رسالة واحدة كل أسبوع.

يمكنك إلغاء الاشتراك متى شئت. الخصوصية

تم استلام طلبك. افتح بريدك واضغط رابط التأكيد لإتمام الاشتراك.