دليل التشغيل Hetzner · netcup · STRATO · حتى 2 نوفمبر 2026

nextxch بتوافر عالٍ عبر عدة مزودين

عندما يخرج مزود بأكمله عن الخدمة، ينبغي أن يستمر تدفق البريد: دون anycast مكلف، ودون موازنات حمل مؤسسية، وقابلًا للنشر من Git. يعرض هذا الدليل ثلاثة إعدادات على خوادم منخفضة التكلفة لدى Hetzner وnetcup وSTRATO، ويشرح ثانية بثانية ما يحدث أثناء الانقطاع، ويحسب الموارد والتكلفة. كل ادعاء يخص مزودًا مرتبط بمصدر مع تاريخ التحقق، وكل ادعاء يخص nextxch يذكر الملف المصدري في الشيفرة.

01 · ما يقدمه nextxch للتوافر العالي (تم التحقق في الشيفرة)

nextxch قادر على العمل بنمط Active-Active: تقبل كل عقدة SMTP وIMAP وJMAP وActiveSync وHTTPS، ولا توجد عقدة خاملة ولا جلسة لاصقة. ويحتفظ PostgreSQL بالحالة المشتركة، بينما تُخزَّن الرسائل نفسها ككائنات .eml معنونة بـ SHA-256 في مخزن الكائنات (Blob Store).

القدرةالطريقةالمصدر في الشيفرة
الشبكة الخلفية للعنقود دون RedisPostgreSQL LISTEN/NOTIFY (القناة nextxch_cluster_events) لأحداث مثل البريد الجديد والعلامات وexpunge وقائمة الانتظار؛ وجداول UNLOGGED هي cluster_ephemeral وcluster_nodes؛ ونبضة (Heartbeat) كل 15 ثانية، وتُستبعد العقد بعد 60 ثانية دون نبضةsrc/Nextxch.Storage/Cluster/PostgresClusterCoordinator.cs
مهام القائدpg_try_advisory_lock: تتولى عقدة واحدة بالضبط تجديد شهادات ACME، وإرسال نبضة الترخيص، ومزامنة LDAP، وإرسال تذكيرات التقويمAcmeCertificateManager.cs, ClusterLicenseService.cs, LdapSyncService.cs, CalendarReminderPushService.cs
قائمة انتظار SMTP عبر العقدFOR UPDATE SKIP LOCKED: تحجز كل عقدة عناصر مختلفة من قائمة الانتظار، فلا يحدث تسليم مزدوجsrc/Nextxch.Storage/Postgres/PostgresMailStore.cs
قواعد بيانات تدعم العنقدةPostgreSQL (الافتراضي للعناقيد) وSQL Server ‏(sp_getapplock)؛ وSQLite لعقدة واحدة فقطsrc/Nextxch.Storage/StorageServiceCollectionExtensions.cs
مخزن BlobFileSystem أو S3 (عميل SigV4 خاص مع Content-MD5) أو Hybrid (ذاكرة تخزين مؤقت NVMe محلية + S3). أوضاع Object Lock: Normal وGovernance وCompliance والحجز القانوني (Legal Hold)src/Nextxch.Storage/Blobs/S3BlobStore.cs, HybridBlobStore.cs
الشهادات وDKIMتُخزَّن شهادات ACME ومفاتيح DKIM في قاعدة البيانات؛ وتُشارَك تحديات HTTP-01 عبر العنقود بحيث يمكن لأي عقدة الرد؛ ويُحتفظ بالمفتاح الطرفي عند التجديد (يبقى DANE 3 1 1 صالحًا)src/Nextxch.Security/Tls/AcmeCertificateManager.cs
نقاط نهاية الحالة الصحية/health/live (دون قاعدة بيانات)، و/health/ready (قاعدة البيانات قابلة للوصول، وإلا 503)، و/health (نظرة عامة تشمل نظراء العنقود)src/Nextxch.Server/Program.cs
عنوان IP الحقيقي للعميلبروتوكول PROXY v1/v2 لـ SMTP وIMAP وManageSieve (Cluster:EnableProxyProtocol) وX-Forwarded-For لـ HTTP من TrustedProxyCidrssrc/Nextxch.Core/Abstractions/IClusterCoordinator.cs
نشر المخرجات (Artefacts)docker-compose.yml، وملفات Kubernetes manifests (3 نسخ متماثلة، HPA من 2 إلى 10، PDB، probes)، وHelm chart، وdeb/rpm، وWindows MSIdeploy/kubernetes/, deploy/helm/nextxch/, deploy/packages/
الترخيصتُحتسب صناديق البريد النشطة، لا العقد ولا الـ pods؛ وتُشارَك حالة الترخيص عبر cluster_kvsrc/Nextxch.Server/Services/ClusterLicenseService.cs, الأسعار

المنافذ لكل عقدة: 25 و465 و587 (SMTP)، و143 و993 (IMAP)، و4190 (ManageSieve)، و80/443 (HTTPS وACME وwebmail وActiveSync وJMAP). ويكون POP3 110/995 معطّلًا افتراضيًا (Pop3:Enabled=false في Pop3Settings.cs).

الشرط الإلزامي الوحيد
تحتاج جميع العقد في أي وقت إلى خادم PostgreSQL أساسي واحد بالضبط قابل للكتابة، وإلى مخزن كتل مشترك واحد. ومن دون قاعدة بيانات قابلة للوصول، يُرجع /health/ready الرمز 503 ولا تستطيع العقدة التحقق من المستلمين. ولذلك يعني التوافر العالي عبر المزودين نسخ PostgreSQL ومخزن الكتل نسخًا متماثلًا عبر المواقع والتبديل تلقائيًا. وهذا ما تبنيه الإعدادات الثلاثة.

02 · قواعد خاصة بالبريد تجعل التوافر العالي سهلًا

الوارد: سجل MX يتضمن تجاوز الفشل (Failover) مدمجًا

يرتّب المرسلون سجلات MX حسب الأولوية ويجب عليهم تجربة كل عنوان؛ ومع الأولوية المتساوية يجب عليهم الاختيار عشوائيًا. [1] وإذا تعذّر الوصول إلى هدف، فينبغي ألا تقل فترة إعادة المحاولة عن 30 دقيقة، وألا تقل مدة التخلي عن 4 إلى 5 أيام. [1]

الصادر: SPF وPTR لكل عنوان IP

يجب أن يسرد SPF كل عنوان IP صادر لكل موقع؛ ولا تُحتسب ip4/ip6 ضمن حد الاستعلامات العشرة لـ DNS. [2] ويشترط Gmail وجود DNS أمامي وعكسي صالح؛ ويُضبط PTR لدى كل مزود على حدة. [8][14][27][31]

DKIM وMTA-STS وDANE

تُخزَّن مفاتيح DKIM في قاعدة بيانات العنقود، وتخدم مجموعة DNS واحدة جميع العقد؛ وتتيح المحدِّدات استخدام عدة مفاتيح بالتوازي. [3] ويجب أن تسمي سياسة MTA-STS كل مضيف MX؛ إذ لا تغطي أحرف البدل إلا التسمية الموجودة في أقصى اليسار. [4] ويحتاج DANE إلى سجل TLSA واحد لكل مضيف MX، ويوصى بـ 3 1 1، وإلى منطقة موقّعة بـ DNSSEC. [5]

العملاء: IMAP والإرسال (Submission) وHTTPS

لا تملك برامج البريد تجاوز فشل على غرار MX. فهي تتصل باسم واحد (هنا mail.example.com) تُدار سجلات A الخاصة به بواسطة مهمة فحص صحة. وتُجدَّد اتصالات IDLE كل 29 دقيقة على الأقل، فتنتهي حينها على عقدة سليمة. [7][6]

منطقة DNS لموقعين (قالب)
; Zone example.com (TTL in seconds). IPs are documentation ranges: replace them.
; Site A = Hetzner 192.0.2.10, site B = netcup 198.51.100.20, site C (medium setup) = 203.0.113.30

; Inbound SMTP: equal preference, senders MUST randomize (RFC 5321 section 5.1).
example.com.            3600 IN MX 10 mx1.example.com.
example.com.            3600 IN MX 10 mx2.example.com.

; Static per-site names: never touched by the failover job (stable FCrDNS for PTR).
mx1.example.com.        3600 IN A     192.0.2.10
mx2.example.com.        3600 IN A     198.51.100.20

; Client name (IMAP, submission, HTTPS, webmail, ActiveSync, autodiscover):
; short TTL, kept up to date by the health-check job. Start with all healthy nodes.
mail.example.com.         60 IN A     192.0.2.10
mail.example.com.         60 IN A     198.51.100.20

; SPF: every outbound IP of every site. ip4/ip6 do not count against the 10-lookup limit (RFC 7208 4.6.4).
example.com.            3600 IN TXT   "v=spf1 ip4:192.0.2.10 ip4:198.51.100.20 ip4:203.0.113.30 -all"

; DKIM: nextxch keeps the keys in the cluster database, so one record set serves all nodes.
; Publish the selectors shown under /admin/ (Domains > DKIM).

; MTA-STS: the policy must list every MX host (see the note on gaps).
_mta-sts.example.com.   3600 IN TXT   "v=STSv1; id=20261102"

; DANE (optional, needs a DNSSEC-signed zone): one TLSA record per MX host.
; nextxch reuses the leaf key on renewal, so "3 1 1" stays stable.
_25._tcp.mx1.example.com. 3600 IN TLSA 3 1 1 <sha256-of-spki>
_25._tcp.mx2.example.com. 3600 IN TLSA 3 1 1 <sha256-of-spki>

; PTR (set in the provider console, not in this zone):
;   192.0.2.10    -> mx1.example.com   (Hetzner Console / Robot)
;   198.51.100.20 -> mx2.example.com   (netcup SCP)
اسم EHLO وPTR
يسري اسم مضيف الخادم (mail.example.com، للعملاء وautodiscover والشهادة) على مستوى العنقود كله. وتضبط كل عقدة اسم EHLO/HELO الخاص بها: Smtp:HeloHostname في الإعدادات أو في /admin/ ضمن "Hostname, IPv6 & Ports" (اسم EHLO لكل عقدة، مع فحص PTR لكل عقدة). ويُستخدم في لافتة 220 ورد EHLO وEHLO الصادر. وجّهوا سجل PTR لكل عنوان IP إلى اسمه الثابت mx1/mx2؛ ويعرض مستشار DNS سجل HELO SPF لكل عقدة. وعلى أي حال، يحظر RFC 5321 القسم 4.1.4 رفض رسالة لمجرد عدم تطابق اسم EHLO. [1][8]
اسم EHLO لكل عقدة
# appsettings.Production.json per node (or admin: Hostname, IPv6 & Ports > EHLO name per node)
# Site A (Hetzner):
{ "Smtp": { "ServerHostName": "mail.example.com", "HeloHostname": "mx1.example.com" } }
# Site B (netcup):
{ "Smtp": { "ServerHostName": "mail.example.com", "HeloHostname": "mx2.example.com" } }
# PTR 192.0.2.10 -> mx1.example.com, PTR 198.51.100.20 -> mx2.example.com
سياسة MTA-STS مع جميع مضيفات MX (يقدّمها nextxch لكل نطاق)
# Served by nextxch itself at https://mta-sts.example.com/.well-known/mta-sts.txt
# (admin: Domains > domain > Transport > MTA-STS). Empty MX list = automatic:
# the domain's MX records from DNS + server host name + EHLO names of all nodes.
# Wildcards like *.example.com are allowed. Every change changes the policy id;
# the DNS advisor shows the matching _mta-sts TXT record.
version: STSv1
mode: enforce
mx: mx1.example.com
mx: mx2.example.com
max_age: 604800

03 · كيف يُعاد توجيه العملاء: مقارنة الخيارات

لا تفيد عناوين Failover IP لدى المزود إلا داخل مزود واحد. أما عند تعطل مزود بأكمله، فيبقى DNS الأداة البسيطة والرخيصة.

الخيارالنطاقالتكلفةالتقييم
عدة سجلات MX بالأولوية نفسهاعبر عدة مزودين0 €إلزامي للبريد الوارد، ومعياري [1]
مهمة فحص صحة خاصة + DNS API (Cloudflare وHetzner وdeSEC وnetcup)عبر عدة مزودين0 €موصى به. Cloudflare: قيمة TTL من 60 s (Enterprise من 30 s)، وauto = 300 s. Hetzner DNS: مجاني، وTTL من 60 s، وAPI في Hetzner Console. deSEC: حد أدنى لـ TTL لكل نطاق، مثلًا 3600 s، ولا يُخفَّض إلا عند الطلب. netcup: توجد CCP DNS API، والحد الأدنى لـ TTL غير موثق. [38][39][21][40][29]
Cloudflare Load Balancing (DNS فقط)عبر عدة مزودينبدءًا من 5 USD شهريًافحوصات السلامة (بما فيها TCP وSMTP) كل 15 ثانية خارج Enterprise؛ ولسجلات A/AAAA/CNAME، أما MX/SRV ففي Enterprise فقط. يحل محل المهمة الخاصة لـ mail.example.com. [33][34][35][36]
Cloudflare Spectrumعبر عدة مزودينEnterpriseمنافذ TCP العشوائية (25 و465 و587 و993) متاحة فقط كإضافة Enterprise؛ أما Pro/Business فيقتصران على SSH وMinecraft وRDP. [37]
Hetzner Floating IP (Cloud)مشروع واحد، منطقة شبكة واحدةIPv4 بسعر 3.00 €، وIPv6 بسعر 1.00 € شهريًاداخل Hetzner فقط [9]
عنوان IP لتجاوز الفشل لدى Hetzner (Robot، خوادم مخصصة)مراكز بيانات Hetzner4.20 € شهريًا + 4.90 € رسوم الإعدادالتبديل عبر Robot API خلال 90 إلى 110 ثانية، داخل Hetzner فقط [11]
عنوان IP لتجاوز الفشل لدى netcupموقع netcup نفسه5.00 € شهريًا شاملًا ضريبة القيمة المضافةإعادة التوجيه في SCP في أي وقت مجانًا، داخل netcup فقط [24][25]
مشاركة IP في Akamai/Linodeمركز بيانات واحد، وخادما Linodeابتداءً من 5 USD (Nanode)قائم على BGP (lelastic)، داخل مركز البيانات نفسه فقط؛ NodeBalancer Basic بسعر 10 USD، وTCP/HTTP [41][42]
Anycast / BGP خاصعاممرتفعASN خاص (عقد RIPE عبر العضوية أو عبر LIR راعٍ) بالإضافة إلى نطاق عناوين خاص (لدى Vultr على الأقل /24 IPv4 أو /48 IPv6). وهذا غير متناسب لمعظم تثبيتات nextxch. [43][44]
لماذا يلزم شاهد (Witness)
مع موقعين لا يستطيع أي منهما معرفة ما إذا كان الآخر قد تعطّل أم انقطع الاتصال فقط. وإذا رقّى كلاهما نفسه، فستنتج قاعدتا بيانات متباعدتان (Split Brain). ويقرر etcd بالأغلبية: يكفي عضوان من ثلاثة. لذلك: ثلاثة أصوات لدى ثلاثة مزودين، ويمكن أن يكون الثالث شاهدًا صغيرًا. [48][53]

04 · الإعداد S: موقعان بالإضافة إلى شاهد

للشركات الصغيرة والمتوسطة. يشغّل كل من الموقع A (مثل Hetzner Cloud) والموقع B (مثل netcup VPS) نسخة من nextxch وPostgreSQL تحت إدارة Patroni وعضوًا واحدًا في etcd ومنطقة Garage واحدة. ويحتفظ شاهد صغير C لدى مزود ثالث (مثل STRATO) بعضو etcd الثالث والنسخة الثالثة من Garage ومدقق تجاوز الفشل (Failover) عبر DNS. وترتبط المواقع الثلاثة بشبكة WireGuard متشابكة.

flowchart TB
  U["Users: IMAP, SMTP submission, HTTPS"] -->|"mail.example.com TTL 60"| DNS["DNS with health-check job"]
  S["Remote mail servers"] -->|"MX 10 mx1 / MX 10 mx2"| A1
  S --> B1
  DNS --> A1
  DNS --> B1
  subgraph A["Site A: Hetzner"]
    A1["nextxch"] --> AP[("PostgreSQL primary")]
    A1 --> AG["Garage zone A"]
    AE["etcd"]
  end
  subgraph B["Site B: netcup"]
    B1["nextxch"] --> BP[("PostgreSQL replica")]
    B1 --> BG["Garage zone B"]
    BE["etcd"]
  end
  subgraph C["Witness C: STRATO"]
    CE["etcd"]
    CG["Garage zone C"]
    CF["Failover checker"]
  end
  B1 -.->|"WireGuard"| AP
  AP ==>|"synchronous replication"| BP
  AG <-.-> BG
  BG <-.-> CG
  AE <-.-> BE
  BE <-.-> CE
بنية الإعداد S

لماذا Garage كمخزن للكائنات الثنائية؟

إن حاوية S3 لدى مزود واحد ستختفي باختفاء ذلك المزود، ولا يستطيع nextxch حاليًا الكتابة إلا إلى هدف S3 واحد. ‏Garage مخزن متوافق مع S3 ‏(AGPL-3.0) مصمم لعدة مراكز بيانات عبر الإنترنت (متطلبات المشروع: زمن انتقال 200 ms كحد أقصى، و50 Mbit/s على الأقل). ومع replication_factor = 3 توجد نسخة واحدة في كل منطقة؛ وتستمر القراءة والكتابة ما دامت منطقة واحدة فقط معطلة (نصاب الكتابة 2). ومع منطقتين فقط، سيؤدي فقدان إحداهما إلى جعل المخزن للقراءة فقط، ولهذا يحتفظ الشاهد (Witness) أيضًا بنسخة كاملة. [60][61]

غير مختبر ودون Object Lock
لا يدعم Garage ميزة S3 Object Lock ولا إدارة الإصدارات. والنطاقات ذات احتفاظ WORM (governance/compliance) تجعل nextxch يرسل ترويسات الاحتفاظ، ولذلك تحتاج إلى S3 يدعم Object Lock؛ انظروا الفصل 09 بشأن نسخة GoBD. لم يُختبَر تشغيل nextxch على Garage تحت الحمل لهذا الدليل: تحققوا ببيانات اختبارية قبل الإنتاج. [62]

خطوة بخطوة

  1. طلب الخوادم: A = Hetzner Cloud (المنفذان 25/465 محظوران افتراضيًا؛ اطلبوا رفع الحظر بعد شهر واحد وبعد سداد أول فاتورة، والمنفذ 587 مفتوح)، وB = netcup VPS (أزيلوا سياسة جدار الحماية "netcup Mail block" في SCP)، وC = STRATO VPS كشاهد (لا يحتاج الشاهد إلى المنفذ 25). [13][26]
  2. ضبط PTR: عنوان IP A إلى mx1.example.com (Hetzner Console)، وعنوان IP B إلى mx2.example.com (netcup SCP). [14][27]
  3. التثبيت الأساسي عبر cloud-init (Debian 13): مستخدم النشر وDocker وWireGuard، ودون تسجيل دخول بكلمة مرور.
  4. شبكة WireGuard متشابكة 10.88.0.0/24 بين A وB وC؛ افتحوا منفذ UDP 51820 لعناوين IP للنظراء فقط. [47]
  5. etcd على الثلاثة جميعها (مهلة الانتخاب 10 × RTT على الأقل)، ثم Patroni + PostgreSQL 18 على A وB. [50][53]
  6. Garage على الثلاثة جميعها، وطبّقوا تخطيطًا بثلاث مناطق، وأنشئوا الحاوية (Bucket) والمفتاح. [61]
  7. nextxch عبر Docker Compose على A وB (Cluster:Enabled=true، وسلسلة اتصال متعددة المضيفين، وS3 يشير إلى Garage المحلي). وفي /admin/ اضبطوا اسم المضيف mail.example.com وأصدروا شهادة ACME لـ mail وmx1 وmx2.
  8. DNS كما في الفصل 02، وتحققوا من قائمة MX الخاصة بـ MTA-STS في /admin/، وفعّلوا تجاوز الفشل عبر DNS في /admin/ ضمن النطاقات (يتصرف قائد العنقود فقط) أو بدلًا من ذلك السكربت تحت قفل etcd.
  9. النسخ الاحتياطية كما في الفصل 09، واختبروا الاستعادة. ثم تدرّبوا على انقطاع: أوقفوا الموقع A إيقافًا قسريًا واتبعوا الفصل 08.
cloud-init (بيانات المستخدم)
#cloud-config
# Hetzner Cloud / netcup: paste as user data when creating the VM.
users:
  - name: deploy
    groups: [sudo, docker]
    shell: /bin/bash
    sudo: "ALL=(ALL) NOPASSWD:ALL"
    ssh_authorized_keys:
      - ssh-ed25519 AAAA... ci-deploy
package_update: true
packages: [wireguard, curl, jq, docker.io, docker-compose-v2, unattended-upgrades]
runcmd:
  - [ systemctl, enable, --now, docker ]
  - [ sed, -i, "s/^#\\?PasswordAuthentication.*/PasswordAuthentication no/", /etc/ssh/sshd_config ]
  - [ systemctl, restart, ssh ]
WireGuard: /etc/wireguard/wg0.conf
# /etc/wireguard/wg0.conf on site A (Hetzner). Same pattern on B and C.
# Full mesh: every site has one [Peer] block per other site.
[Interface]
Address    = 10.88.0.1/24
ListenPort = 51820
PrivateKey = <private-key-of-site-A>   # wg genkey, never commit it

[Peer]  # site B (netcup)
PublicKey           = <public-key-of-site-B>
Endpoint            = 198.51.100.20:51820
AllowedIPs          = 10.88.0.2/32
PersistentKeepalive = 25

[Peer]  # site C (witness or third site)
PublicKey           = <public-key-of-site-C>
Endpoint            = 203.0.113.30:51820
AllowedIPs          = 10.88.0.3/32
PersistentKeepalive = 25

# systemctl enable --now wg-quick@wg0
# Firewall: UDP 51820 open only for the two peer IPs.
etcd: /etc/default/etcd
# /etc/default/etcd on site A (Debian package etcd-server). Sites B and C analogue.
# Three members = quorum of two: any single site may fail.
ETCD_NAME="site-a"
ETCD_DATA_DIR="/var/lib/etcd/default"
ETCD_LISTEN_PEER_URLS="http://10.88.0.1:2380"
ETCD_LISTEN_CLIENT_URLS="http://10.88.0.1:2379,http://127.0.0.1:2379"
ETCD_INITIAL_ADVERTISE_PEER_URLS="http://10.88.0.1:2380"
ETCD_ADVERTISE_CLIENT_URLS="http://10.88.0.1:2379"
ETCD_INITIAL_CLUSTER="site-a=http://10.88.0.1:2380,site-b=http://10.88.0.2:2380,site-c=http://10.88.0.3:2380"
ETCD_INITIAL_CLUSTER_STATE="new"
ETCD_INITIAL_CLUSTER_TOKEN="nextxch-ha"
# WAN tuning (etcd tuning guide: election timeout at least 10x the round trip).
# Example for up to ~25 ms RTT between providers:
ETCD_HEARTBEAT_INTERVAL="250"
ETCD_ELECTION_TIMEOUT="2500"
# Traffic stays inside WireGuard. For production add TLS (ETCD_CERT_FILE etc.).
Patroni: /etc/patroni/config.yml
# /etc/patroni/config.yml on site A (Debian/PGDG package "patroni").
# Site B: name/addresses analogue. The witness C runs etcd only, no PostgreSQL.
scope: nextxch
name: pg-site-a

restapi:
  listen: 10.88.0.1:8008
  connect_address: 10.88.0.1:8008

etcd3:
  hosts: 10.88.0.1:2379,10.88.0.2:2379,10.88.0.3:2379

bootstrap:
  dcs:
    ttl: 30                 # defaults; rule: loop_wait + 2 * retry_timeout <= ttl
    loop_wait: 10
    retry_timeout: 10
    maximum_lag_on_failover: 1048576
    synchronous_mode: on    # a 250 OK to a sender is only given after the commit reached a 2nd site
    synchronous_mode_strict: false   # if the replica site is gone, keep accepting mail (falls back to async)
    failsafe_mode: true
    postgresql:
      use_pg_rewind: true
      parameters:
        wal_level: replica
        hot_standby: "on"
        max_wal_senders: 10
        max_replication_slots: 10
        wal_log_hints: "on"
        archive_mode: "on"
        archive_command: "pgbackrest --stanza=nextxch archive-push %p"
  initdb: [ {encoding: UTF8}, data-checksums ]

postgresql:
  listen: 10.88.0.1:5432
  connect_address: 10.88.0.1:5432
  data_dir: /var/lib/postgresql/18/main
  bin_dir: /usr/lib/postgresql/18/bin
  authentication:
    replication: { username: replicator, password: "${PATRONI_REPLICATION_PASSWORD}" }
    superuser:   { username: postgres,   password: "${PATRONI_SUPERUSER_PASSWORD}" }
  pg_hba:
    - host replication replicator 10.88.0.0/24 scram-sha-256
    - host nextxch     nextxch    10.88.0.0/24 scram-sha-256
Garage: /etc/garage.toml
# /etc/garage.toml, identical on all three sites except rpc_public_addr.
metadata_dir = "/var/lib/garage/meta"
data_dir     = "/var/lib/garage/data"
db_engine    = "lmdb"

replication_factor = 3          # one copy per zone (site); writes need 2 of 3
consistency_mode   = "consistent"

rpc_bind_addr   = "10.88.0.1:3901"
rpc_public_addr = "10.88.0.1:3901"
rpc_secret      = "<openssl rand -hex 32, same on all nodes>"

[s3_api]
s3_region     = "garage"
api_bind_addr = "127.0.0.1:3900"   # each nextxch node talks to its local Garage

# After the first start (node ids from `garage node id`):
#   garage layout assign <id-a> -z hetzner -c 500G
#   garage layout assign <id-b> -z netcup  -c 500G
#   garage layout assign <id-c> -z witness -c 500G
#   garage layout show && garage layout apply --version 1
#   garage bucket create nextxch
#   garage key create nextxch-app
#   garage bucket allow --read --write --owner nextxch --key nextxch-app
nextxch: ملف docker-compose.yml لكل موقع
# docker-compose.yml on site A and site B (nextxch only; PostgreSQL/Patroni, etcd,
# Garage and WireGuard run as system services on the same VM).
# Secrets come from .env (never committed): NEXTXCH_LICENSE, PG_PASSWORD, S3_KEY, S3_SECRET
name: nextxch

services:
  nextxch:
    image: nextxch/server:${NEXTXCH_TAG}
    restart: unless-stopped
    network_mode: host            # real client IPs for SPF/DMARC, direct WireGuard reach
    environment:
      ASPNETCORE_URLS: "http://*:80;https://*:443"
      License__Token: "${NEXTXCH_LICENSE}"
      Cluster__Enabled: "true"
      Cluster__NodeId: "${SITE_NAME}"          # hetzner-a / netcup-b
      Storage__Provider: "Postgres"
      # Npgsql multi-host: always connects to the current Patroni primary
      Storage__PostgresConnectionString: "Host=10.88.0.1,10.88.0.2;Port=5432;Database=nextxch;Username=nextxch;Password=${PG_PASSWORD};Target Session Attributes=primary;Host Recheck Seconds=5"
      Storage__BlobBackend: "S3"
      Storage__Compression: "Zstd"
      Storage__S3__EndpointUrl: "http://127.0.0.1:3900"
      Storage__S3__Region: "garage"
      Storage__S3__BucketName: "nextxch"
      Storage__S3__ForcePathStyle: "true"
      Storage__S3__AccessKey: "${S3_KEY}"
      Storage__S3__SecretKey: "${S3_SECRET}"
      Smtp__ServerHostName: "mail.example.com"
      Imap__ServerHostName: "mail.example.com"
      ManageSieve__ServerHostName: "mail.example.com"
      Acme__Enabled: "true"
      Acme__ContactEmail: "postmaster@example.com"
    volumes:
      - nextxch-data:/app/data
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://127.0.0.1/health/ready"]
      interval: 15s
      timeout: 5s
      retries: 3

volumes:
  nextxch-data:

تسمّي سلسلة الاتصال كلا مضيفَي PostgreSQL مع Target Session Attributes=primary. ينشئ nextxch اتصالاته عبر NpgsqlDataSource.Create ‏(PostgresMailStore.cs)؛ ومع عدة مضيفات يبني Npgsql تلقائيًا مصدر بيانات متعدد المضيفات ولا يتصل إلا بالخادم الأساسي الحالي، ويعيد فحص الحالة كل Host Recheck Seconds (الافتراضي 10 ثوانٍ). وبعد تجاوز الفشل في Patroni تعيد العقد الاتصال دون أي تغيير في الإعداد؛ ويعيد اتصال LISTEN الخاص بالعنقود المحاولة كل ثانيتين (PostgresClusterCoordinator.cs). [63]

تجاوز الفشل التلقائي عبر DNS لـ mail.example.com

يتضمن nextxch وكيل تجاوز الفشل (Failover) عبر DNS (اختياري، معطّل افتراضيًا؛ في /admin/ ضمن النطاقات، البطاقة "DNS failover"). تفحص كل عقدة لافتة SMTP على المنفذ 25 وHTTPS /health لكل عقدة عبر عنوان IP العام الخاص بها كل 30 ثانية. ولا يتصرف إلا قائد العنقود: فهو يحمل PostgreSQL advisory lock، ويجمع رؤية جميع العقد بالأغلبية، ويكتب سجلات A/AAAA للأسماء المُعدّة عبر Hetzner DNS (Cloud API) أو Cloudflare API. التخلّف (Hysteresis): بعد N جولة فاشلة تخرج العقدة من DNS، وبعد M جولة سليمة تعود؛ ولا تُزال العقدة الأخيرة أبدًا. ويُسجَّل كل تغيير في سجل الأحداث، وتُبلغ قاعدة التنبيه "DNS failover: node taken out" عبر تنبيهات التشغيل. ويُشفَّر رمز API بحلقة مفاتيح الخادم ولا يُعرض مرة أخرى أبدًا.

وكيل تجاوز الفشل عبر DNS المدمج
# Admin: Domains > DNS failover (cluster). Stored cluster-wide; the token is encrypted
# with the server keyring and never shown again. Equivalent settings:
Provider:          Hetzner DNS (Cloud API, api.hetzner.cloud)   | Cloudflare DNS
Zone:              example.com
Names:             mail.example.com            # A/AAAA follow the healthy nodes
TTL:               60                          # resolvers keep old answers this long
Interval:          30 s
Failures (N):      3                           # 3 x 30 s before a node leaves DNS
Successes (M):     5                           # 5 x 30 s before it returns
Checks:            SMTP 25 (220 banner), HTTPS /health
Nodes:             fsn1, 192.0.2.10, 2001:db8::10
                   nbg1, 198.51.100.20
# Alert: rule "DNS failover: node taken out" (Alerts) fires when a node is out of DNS.
لماذا لا تغيّر العقدة المعزولة DNS
لا يوجد قفل القائد إلا عبر اتصال بخادم PostgreSQL الأساسي. ومع Target Session Attributes=primary في سلسلة الاتصال (الفصل 04 أعلاه)، لا يصل الجانب المعزول إلى أي خادم أساسي، فلا يحصل على القفل ويبقى خاملًا. ويمنع تصويت الأغلبية بين جميع المراقبين عقدةً واحدة ذات رؤية معطوبة من إزالة عقدة سليمة.

البديل: سكربت تحت قفل etcd

لمزودي DNS الذين لا يتوفر لهم موصل مدمج (مثل deSEC)، أو عندما ينبغي أن يتخذ etcd القرار، يبقى السكربت بديلًا مكافئًا. لا تشغّلوا الوكيل والسكربت للأسماء نفسها في الوقت نفسه. يفحص السكربت /health/ready لكل عقدة عبر عنوان IP العام الخاص بها كل 20 ثانية، ويوفّق سجلات A عبر Cloudflare API ‏(PATCH/POST/DELETE على dns_records). ويعمل في كل موقع ضمن etcdctl lock: لا تتصرف إلا نسخة واحدة، وفقط في الجانب الذي يملك أغلبية etcd. وبالتالي لا يمكن لموقع معزول أن يغيّر DNS بعد ذلك. ولـ Hetzner DNS أو deSEC لا تتغير سوى استدعاءات curl الثلاثة. [39]

nextxch-dns-failover.sh
#!/usr/bin/env bash
# /usr/local/bin/nextxch-dns-failover.sh
# ALTERNATIVE to the built-in agent (admin: Domains > DNS failover), e.g. for DNS
# providers without a built-in connector (deSEC) or when etcd decides anyway.
# Keeps the A records of mail.example.com equal to the set of healthy nodes.
# Runs on every site under an etcd lock, so only one copy acts, and only on the
# side that still has etcd quorum (a cut-off minority site can never change DNS).
#   ExecStart=etcdctl --endpoints=http://127.0.0.1:2379 lock nextxch-dns /usr/local/bin/nextxch-dns-failover.sh
# Needs: curl, jq. Secrets via systemd EnvironmentFile (CF_API_TOKEN, CF_ZONE_ID).
set -euo pipefail

NAME="mail.example.com"
TTL=60
NODES=("192.0.2.10" "198.51.100.20")     # public IPs of the nextxch nodes
FAILS_BEFORE_REMOVE=3                    # 3 x 20 s = 1 minute
STATE=/run/nextxch-failover; mkdir -p "$STATE"
API="https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records"
AUTH=(-H "Authorization: Bearer ${CF_API_TOKEN}" -H "Content-Type: application/json")

healthy() {   # HTTPS readiness through the public IP, with SNI for the real name
  curl -fsS --max-time 5 --resolve "${NAME}:443:$1" "https://${NAME}/health/ready" \
    | jq -e '.status == "ready"' >/dev/null
}

while true; do
  want=()
  for ip in "${NODES[@]}"; do
    f="$STATE/$ip"; n=$(cat "$f" 2>/dev/null || echo 0)
    if healthy "$ip"; then echo 0 > "$f"; want+=("$ip")
    else n=$((n+1)); echo "$n" > "$f"; [ "$n" -lt "$FAILS_BEFORE_REMOVE" ] && want+=("$ip"); fi
  done
  # Never publish an empty set: if everything looks down, the checker is probably the problem.
  [ "${#want[@]}" -eq 0 ] && { sleep 20; continue; }

  have=$(curl -fsS "${AUTH[@]}" "${API}?type=A&name=${NAME}" | jq -r '.result[] | "\(.id) \(.content)"')
  for ip in "${want[@]}"; do   # add missing healthy IPs
    grep -q " ${ip}$" <<<"$have" || curl -fsS "${AUTH[@]}" -X POST "$API" \
      --data "{\"type\":\"A\",\"name\":\"${NAME}\",\"content\":\"${ip}\",\"ttl\":${TTL},\"proxied\":false}" >/dev/null
  done
  while read -r id ip; do      # remove IPs that are no longer healthy
    [ -z "${id:-}" ] && continue
    printf '%s\n' "${want[@]}" | grep -qx "$ip" || curl -fsS "${AUTH[@]}" -X DELETE "${API}/${id}" >/dev/null
  done <<<"$have"
  sleep 20
done
وحدة systemd
# /etc/systemd/system/nextxch-dns-failover.service (install on every site)
[Unit]
Description=nextxch DNS failover (runs only while holding the etcd lock)
After=network-online.target etcd.service wg-quick@wg0.service
Wants=network-online.target

[Service]
EnvironmentFile=/etc/nextxch/dns-failover.env
ExecStart=/usr/bin/etcdctl --endpoints=http://127.0.0.1:2379 lock nextxch-dns /usr/local/bin/nextxch-dns-failover.sh
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target

05 · الإعداد M: ثلاثة مواقع كاملة (Proxmox أو أجهزة افتراضية عادية)

مثل الإعداد S، لكن المواقع الثلاثة جميعها تشغّل nextxch مع نسخة متماثلة من PostgreSQL. وعند تعطل موقع واحد، تبقى قاعدتا بيانات وعقدتا nextxch، ومع synchronous_mode يُحافظ حتى على النسخ المتماثل المتزامن. وتوزّع ثلاثة سجلات MX ذات أولوية متساوية حمل البريد الوارد.

flowchart TB
  S["Remote mail servers"] -->|"MX 10 mx1, mx2, mx3"| N
  U["Users"] -->|"mail.example.com"| N
  subgraph N["WireGuard mesh 10.88.0.0/24"]
    direction LR
    subgraph A["Hetzner"]
      A1["nextxch"] --- A2[("Patroni + PG")] --- A3["etcd + Garage"]
    end
    subgraph B["netcup"]
      B1["nextxch"] --- B2[("Patroni + PG")] --- B3["etcd + Garage"]
    end
    subgraph C["Third provider"]
      C1["nextxch"] --- C2[("Patroni + PG")] --- C3["etcd + Garage"]
    end
  end
  N --> BK["Backups: Storage Box + S3 at another provider"]
بنية الإعداد M
Proxmox: لا عنقود عبر المزودين
وفقًا لـ Proxmox، يحتاج corosync إلى زمن انتقال أقل من 5 ms (مستوى الشبكة المحلية)؛ وبعد نحو 10 ms ومع أكثر من ثلاث عقد يُستبعد أن يعمل. لذلك لا يُنصح بعنقود Proxmox VE واحد يمتد عبر Hetzner وnetcup وSTRATO. والنمط الصحيح: مضيف أو عنقود Proxmox مستقل لكل مزود، مع توافر عالٍ توفره التطبيقات (Patroni وGarage وعنقود nextxch). أما QDevice (corosync-qnetd) فمخصص لعناقيد العقدتين في موقع واحد ولا يخضع لحد زمن الانتقال. ويدير Proxmox Datacenter Manager (الإصدار 1.0 منذ 2025-12-04) عدة عناقيد معًا. [45]

خطوة بخطوة

  1. ثلاثة أجهزة افتراضية (أو ضيوف Proxmox) لدى ثلاثة مزودين، بإصدار Debian نفسه، مع PTR إلى mx1/mx2/mx3 لكل منها.
  2. قائمة جرد Ansible بثلاثة مضيفين، ودليل تشغيل (Playbook) واحد للجميع (WireGuard وetcd وPatroni وGarage وnextxch).
  3. إعداد Patroni من الإعداد S مع مضيف ثالث؛ synchronous_mode: on، ومع ثلاث قواعد بيانات اختياريًا بالنصاب (Quorum). [54]
  4. سلسلة اتصال بثلاثة مضيفين، وتخطيط Garage بثلاث مناطق، وثلاثة سجلات MX، وثلاثة عناوين IP في SPF وفي قائمة عقد DNS failover.
  5. Proxmox Backup Server اختياري لصور الأجهزة الافتراضية: مخزن بيانات محلي أو S3 (الواجهة الخلفية S3 مدعومة رسميًا منذ PBS 4.2)؛ ولا توثّق Proxmox مشاركات الشبكة مثل Storage Box عبر CIFS كنوع لمخزن البيانات. [46]
Ansible: inventory/ha.yml
# inventory/ha.yml: three sites, one VM (or Proxmox guest) per site
all:
  vars:
    ansible_user: deploy
    wg_network: 10.88.0.0/24
    pg_major: 18
  children:
    nextxch_sites:
      hosts:
        hetzner-a: { ansible_host: 192.0.2.10,    wg_ip: 10.88.0.1, garage_zone: hetzner }
        netcup-b:  { ansible_host: 198.51.100.20, wg_ip: 10.88.0.2, garage_zone: netcup }
        third-c:   { ansible_host: 203.0.113.30,  wg_ip: 10.88.0.3, garage_zone: third }
Ansible: site.yml
# site.yml: idempotent base for every site (same playbook for Hetzner, netcup, Strato ...)
- hosts: nextxch_sites
  become: true
  tasks:
    - name: Base packages (Debian 13)
      ansible.builtin.apt:
        name: [wireguard, etcd-server, etcd-client, patroni, pgbackrest, docker.io, docker-compose-v2, jq, curl]
        update_cache: true
    # PostgreSQL 18 itself comes from the PGDG apt repository (apt.postgresql.org),
    # Garage as the static binary or the dxflrs/garage image.

    - name: WireGuard config
      ansible.builtin.template: { src: wg0.conf.j2, dest: /etc/wireguard/wg0.conf, mode: "0600" }
      notify: restart wg

    - name: etcd, Patroni and Garage config
      ansible.builtin.template: { src: "{{ item.src }}", dest: "{{ item.dest }}", mode: "0640" }
      loop:
        - { src: etcd.env.j2,     dest: /etc/default/etcd }
        - { src: patroni.yml.j2,  dest: /etc/patroni/config.yml }
        - { src: garage.toml.j2,  dest: /etc/garage.toml }

    - name: nextxch compose file
      ansible.builtin.template: { src: docker-compose.yml.j2, dest: /opt/nextxch/docker-compose.yml }

    - name: Start nextxch
      ansible.builtin.command: docker compose up -d
      args: { chdir: /opt/nextxch }
      changed_when: false

  handlers:
    - name: restart wg
      ansible.builtin.service: { name: wg-quick@wg0, state: restarted }

06 · الإعداد L: Kubernetes لدى ثلاثة مزودين

لعدد كبير من صناديق البريد والتوسع الأفقي. أمر مهم أولًا: k3s صراحةً لا يدعم etcd المدمج في عنقود موزّع عبر عدة سحابات؛ لذا ينبغي أن تبقى عقد الخادم في موقع واحد. ومن ثم: عنقود Kubernetes واحد لكل مزود (k3s أو Talos لكل منهما)، يشغّل كل منها نشرًا لـ nextxch من مخطط Helm المرفق. وتعمل طبقة البيانات (Patroni + etcd + Garage) على أجهزة افتراضية خاصة بها عبر WireGuard كما في الإعداد M، وتستخدمها العناقيد الثلاثة جميعها. [49][48]

flowchart TB
  DNS["DNS: MX per provider LB, mail.example.com with health checks"]
  subgraph K1["k3s/Talos cluster Hetzner"]
    L1["Load balancer"] --> P1["nextxch pods (HPA)"]
  end
  subgraph K2["k3s/Talos cluster netcup"]
    L2["Load balancer"] --> P2["nextxch pods (HPA)"]
  end
  subgraph K3["k3s/Talos cluster STRATO"]
    L3["Load balancer"] --> P3["nextxch pods (HPA)"]
  end
  DNS --> L1
  DNS --> L2
  DNS --> L3
  subgraph D["Data layer on VMs, WireGuard"]
    PG[("Patroni: primary + 2 replicas")]
    GA["Garage: 3 zones"]
    ET["etcd: 3 members"]
  end
  P1 --> PG
  P2 --> PG
  P3 --> PG
  P1 --> GA
  P2 --> GA
  P3 --> GA
إعداد البنية L

لماذا لا CloudNativePG عبر العناقيد؟

يمكن لـ CloudNativePG تشغيل عناقيد نسخ متماثلة في عناقيد Kubernetes أخرى، لكن الترقية وخفض الرتبة بين العناقيد تصريحيان (الحقل primary، ‏promotionToken)، أي ليسا تلقائيين. ولتجاوز الفشل التلقائي بين المزودين، يُعد Patroni مع etcd عبر ثلاثة مواقع الحل الأكثر مباشرة. [52]

عنقود واحد عبر جميع المزودين؟

يوفر Talos ميزة KubeSpan، وهي شبكة WireGuard كاملة بين المواقع، ويوفر k3s واجهة flannel الخلفية wireguard-native. غير أن مستوى التحكم (etcd) حساس لزمن الاستجابة عبر WAN. وتشغيل عدة عناقيد صغيرة أبسط، وتتعطل كل منها بشكل مستقل. [51][49][50]

خطوة بخطوة

  1. ابنوا طبقة البيانات كما في الإعداد M (ثلاثة أجهزة افتراضية مع Patroni وetcd وGarage).
  2. عنقود k3s أو Talos واحد لكل مزود؛ والاتصال الشبكي بطبقة البيانات عبر WireGuard (أو KubeSpan).
  3. المنافذ الواردة: خدمة من نوع LoadBalancer مع externalTrafficPolicy: Local (كما في المستودع) للحفاظ على عنوان IP للمرسل من أجل SPF/DMARC؛ ومع موازن تحميل يدعم بروتوكول PROXY استخدموا بدلًا من ذلك Cluster__EnableProxyProtocol=true. ويدعم Hetzner Cloud Load Balancer بروتوكول PROXY وأي خدمة TCP. [19]
  4. المنفذ الصادر 25: خروج الحاويات (Pods) عبر عنوان IP ثابت مع سجل PTR وإدخال SPF مطابقين (بوابة خروج، كما هو موضح في deploy/kubernetes/README.md).
  5. قيم Helm لكل مزود، والنشر عبر خط المعالجة واحدًا تلو الآخر (الفصل 07). ويأتي HPA (من 2 إلى 10 pods عند 70% CPU) وPodDisruptionBudget مع المستودع.
Helm: k8s/values-hetzner.yaml
# values-hetzner.yaml (one file per provider cluster; netcup/strato analogue)
# Helm chart: deploy/helm/nextxch in the nextxch repository.
replicaCount: 3
mail:
  hostname: mail.example.com
  defaultDomain: example.com
postgres:
  # Patroni data layer outside Kubernetes, reached through WireGuard / KubeSpan
  connectionString: "Host=10.88.0.1,10.88.0.2,10.88.0.3;Database=nextxch;Username=nextxch;Password=<from secret>;Target Session Attributes=primary"
s3:
  endpointUrl: "http://garage.nextxch-data.svc:3900"   # Garage zone of this provider
  bucketName: "nextxch"

07 · النشر والتحديث من Git

كل ما هو مهم موجود كملف في المستودع: ملف compose أو قيم Helm، وجرد Ansible، وسكربت تجاوز الفشل عبر DNS. ويطرح خط المعالجة دائمًا موقعًا واحدًا فقط، وينتظر حتى يعيد /health/ready الإبلاغ بحالة "ready" قبل المتابعة. وتوجد الأسرار (الترخيص وكلمات المرور ورموز API ومفاتيح SSH) فقط في مخازن أسرار GitHub/GitLab أو في ملف .env على الخادم، وليس في المستودع أبدًا.

flowchart TB
  G["git push to main"] --> V["Validate: compose config / helm lint"]
  V --> A["Roll out site A"]
  A --> HA{"/health/ready = ready?"}
  HA -->|"yes"| B["Roll out site B"]
  HA -->|"no"| X["Abort, B stays on old version"]
  B --> HB{"/health/ready = ready?"}
  HB -->|"yes"| C["Roll out site C"]
  HB -->|"no"| X
  C --> OK["Done"]
خط النشر
GitHub Actions: ملفات Compose للمواقع
# .github/workflows/deploy.yml: rolling update of the compose sites, one site at a time.
name: deploy-nextxch
on:
  push:
    branches: [main]
    paths: ["sites/**", "compose/**"]
  workflow_dispatch:

concurrency: { group: deploy-nextxch, cancel-in-progress: false }

jobs:
  deploy:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 1            # never update two sites at once
      matrix:
        site: [hetzner-a, netcup-b]
    environment: ${{ matrix.site }}
    steps:
      - uses: actions/checkout@v4
      - name: Write SSH key
        run: |
          install -m 700 -d ~/.ssh
          echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
          echo "${{ vars.HOST_KEY }}" >> ~/.ssh/known_hosts
      - name: Upload compose file and roll the container
        env:
          HOST: ${{ vars.SSH_HOST }}
        run: |
          scp compose/docker-compose.yml "deploy@${HOST}:/opt/nextxch/docker-compose.yml"
          scp "sites/${{ matrix.site }}.env.tag" "deploy@${HOST}:/opt/nextxch/tag.env"
          ssh "deploy@${HOST}" 'cd /opt/nextxch && docker compose --env-file .env --env-file tag.env pull && docker compose --env-file .env --env-file tag.env up -d'
      - name: Wait until the node is ready again
        env:
          HOST: ${{ vars.SSH_HOST }}
        run: |
          for i in $(seq 1 30); do
            ssh "deploy@${HOST}" 'curl -fsS http://127.0.0.1/health/ready' && exit 0
            sleep 10
          done
          echo "node did not become ready" >&2; exit 1
GitLab CI
# .gitlab-ci.yml: the same rolling update with GitLab CI
stages: [validate, deploy-a, deploy-b]

variables:
  NEXTXCH_TAG: "latest"     # pin a released version in production

validate:
  stage: validate
  image: docker:27
  script:
    - docker compose -f compose/docker-compose.yml config -q

.deploy: &deploy
  image: alpine:3.22
  before_script:
    - apk add --no-cache openssh-client curl
    - install -m 700 -d ~/.ssh && echo "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519 && chmod 600 ~/.ssh/id_ed25519
    - echo "$HOST_KEY" >> ~/.ssh/known_hosts
  script:
    - scp compose/docker-compose.yml "deploy@$SSH_HOST:/opt/nextxch/"
    - ssh "deploy@$SSH_HOST" "cd /opt/nextxch && NEXTXCH_TAG=$NEXTXCH_TAG docker compose pull && NEXTXCH_TAG=$NEXTXCH_TAG docker compose up -d"
    - for i in $(seq 1 30); do ssh "deploy@$SSH_HOST" 'curl -fsS http://127.0.0.1/health/ready' && exit 0; sleep 10; done; exit 1
  resource_group: production   # serializes deployments
  rules: [ { if: '$CI_COMMIT_BRANCH == "main"' } ]

deploy-hetzner-a:
  <<: *deploy
  stage: deploy-a
  environment: { name: hetzner-a }

deploy-netcup-b:
  <<: *deploy
  stage: deploy-b
  environment: { name: netcup-b }
GitHub Actions: Kubernetes (الإعداد L)
# .github/workflows/deploy-k8s.yml: one Kubernetes cluster per provider, rolled one after another.
name: deploy-nextxch-k8s
on:
  push: { branches: [main], paths: ["k8s/**"] }
  workflow_dispatch:

concurrency: { group: deploy-k8s, cancel-in-progress: false }

jobs:
  helm:
    runs-on: ubuntu-latest
    strategy:
      max-parallel: 1
      matrix:
        cluster: [hetzner, netcup, strato]
    environment: k8s-${{ matrix.cluster }}
    steps:
      - uses: actions/checkout@v4
      - uses: azure/setup-helm@v4
      - name: Kubeconfig
        run: |
          mkdir -p ~/.kube
          echo "${{ secrets.KUBECONFIG_B64 }}" | base64 -d > ~/.kube/config
          chmod 600 ~/.kube/config
      - name: Helm upgrade (waits for readiness probes on /health/ready)
        run: |
          helm upgrade --install nextxch ./deploy/helm/nextxch \
            --namespace nextxch --create-namespace \
            -f k8s/values-common.yaml -f k8s/values-${{ matrix.cluster }}.yaml \
            --set image.tag=${{ vars.NEXTXCH_TAG }} \
            --wait --timeout 10m --atomic
مهمة تجاوز الفشل لا مكان لها في CI
تشغيلات CI المجدولة ليست دقيقة بالثانية ولا مضمونة. لذلك يعمل تجاوز الفشل عبر DNS على المواقع نفسها: كوكيل مدمج في nextxch أو كخدمة systemd مع السكربت (الفصل 04)، ويقتصر دور خط المعالجة على نشره.
إعداد البريد أيضًا من Git
تُحفظ النطاقات وصناديق البريد والقوائم والقواعد وSMTP والإعدادات الأخرى في المستودع بتنسيق YAML أيضًا: يعلّق nextxch-cli config plan على طلب السحب (Pull Request) بالخطة، ويطبّقها config apply --prune --yes بعد الدمج. دليل مع ملفات GitHub Actions وGitLab CI جاهزة: التكوين بوصفه شيفرة (GitOps).

08 · سيناريوهات الأعطال: ما الذي يحدث بالضبط

Hetzner (الموقع A، الأساسي الحالي) يخرج عن الخدمة بالكامل

sequenceDiagram
  participant S as Remote mail server
  participant A as Site A (Hetzner)
  participant B as Site B (netcup)
  participant C as Witness C
  participant D as DNS
  Note over A: Provider outage
  S->>A: SMTP to mx1
  A--xS: no connection
  S->>B: SMTP to mx2 (or retry)
  B--xS: without a primary the session fails, sender retries later
  Note over B,C: etcd majority B + C, leader key of A expires (ttl 30 s)
  B->>B: Patroni promotes PostgreSQL B to primary
  B->>B: nextxch reconnects (Target Session Attributes=primary)
  S->>B: retry
  B-->>S: 250 OK (commit synchronous or asynchronous)
  C->>D: failover job removes IP A from mail.example.com
  Note over D: clients follow after the TTL expires
التسلسل عند تعطل الموقع A
الوقتما الذي يحدثما يلاحظه المستخدمون
0 sخرج A عن الخدمة. يفشل البريد الوارد إلى mx1، ويستخدم المرسلون mx2 (الأفضلية نفسها) أو يعيدون المحاولة لاحقًا. [1]يفقد العملاء على A اتصالهم.
حتى 30 ثانية تقريبًاتنتهي صلاحية مفتاح القائد الخاص بـ A في etcd بعد ttl = 30 s؛ ويملك B وC الأغلبية. [53]تعيد العقدة B الرمز 503 على /health/ready، وتنتظر عمليات الكتابة.
نحو 30 إلى 60 ثانيةيرقّي Patroni الخادم B (loop_wait 10 s). ويعثر nextxch على B على الخادم الأساسي الجديد عبر سلسلة الاتصال متعددة المضيفين (إعادة الفحص خلال 5 إلى 10 s). [63]يستقبل B البريد مجددًا.
نحو 60 إلى 90 sيزيل تجاوز الفشل عبر DNS (الوكيل: 3 جولات فاشلة × 30 ثانية، والسكربت: 3 × 20 ثانية) عنوان IP A من mail.example.com.تذهب الاتصالات الجديدة إلى B بمجرد انتهاء مدة TTL (60 ثانية).
من 1 إلى 5 دقائقتتبعها المحللات ذات الحد الأدنى الخاص بها لقيم TTL والعملاء ذوو ذاكرة DNS المؤقتة. ويستمر Garage مع المنطقتين B وC (نصاب 2 من 3). [61]يعيد العملاء الاتصال؛ وتبقى جلسات Webmail صالحة (جلسات موقّعة عديمة الحالة).

RPO

مع synchronous_mode، لا يؤكد PostgreSQL أي عملية Commit إلا بعد أن يحصل عليها الموقع الثاني؛ فتوجد الرسالة المؤكدة بـ 250 OK حينها في موقعين. والثمن: تنتظر كل عملية Commit رحلة ذهاب وإياب واحدة على الأقل بين الموقعين. ودون synchronous_mode قد تُفقد الثواني الأخيرة قبل الانقطاع. ومع synchronous_mode_strict: false، يعود Patroni إلى الوضع غير المتزامن عندما لا تبقى أي نسخة متماثلة، فيستمر قبول البريد. [56][54]

RTO

قاعدة البيانات نحو 30 إلى 60 ثانية (إعدادات Patroni الافتراضية)، وتبديل DNS نحو دقيقة واحدة بالإضافة إلى TTL، والبريد الوارد دون انتظار عبر MX الثاني أو إعادة المحاولة من المرسل. هذه الأوقات مشتقة من الإعدادات الافتراضية وليست مقيسة: تحققوا منها في اختباركم الخاص (إيقاف موقع بشكل قسري).

netcup (الموقع B، النسخة المتماثلة) يخرج عن الخدمة

يستمر الخادم الأساسي على A في العمل. ودون شريك متزامن يتحول Patroni إلى الوضع غير المتزامن (strict = false)، وتزيل مهمة تجاوز الفشل عنوان IP B، ويتدفق البريد الوارد عبر mx1. ويستمر Garage مع A وC.

الاتصال بين A وB فقط معطّل

الجانب الذي لا يزال يصل إلى الشاهد C يحتفظ بأغلبية etcd ويبقى أساسيًا أو يصبح أساسيًا. أما الجانب الآخر فيفقد أغلبيته، ويخفّض Patroni رتبة الخادم الأساسي هناك (لا يسري failsafe_mode إلا إذا ظل قادرًا على الوصول إلى جميع الأعضاء)، ولا يعمل تجاوز الفشل عبر DNS هناك: فالوكيل المدمج لا يصل إلى أي خادم أساسي ولا يحصل على قفل القائد، والسكربت لا يحصل على قفل etcd. ولا يحدث انقسام الدماغ (Split Brain). [55]

الاستعادة بعد تجاوز الفشل (Failback)

sequenceDiagram
  participant A as Site A
  participant B as Site B (primary)
  participant G as Garage
  participant D as DNS job
  A->>A: server back online, WireGuard up
  A->>B: Patroni: pg_rewind, then replica of B
  G->>A: missing objects are re-replicated
  A->>A: /health/ready = ready
  D->>D: successful checks
  D->>A: IP A back in mail.example.com
  Note over A,B: optional, planned: patronictl switchover back to A
عودة الموقع A

عندما يعود A، يبقى B أساسيًا. ويعيد Patroni الخادم A كنسخة متماثلة باستخدام pg_rewind (use_pg_rewind: true)، ويعيد Garage مزامنة الكائنات المفقودة، وتعيد مهمة تجاوز الفشل إضافة عنوان IP A بمجرد أن يُبلغ /health/ready هناك بالحالة "ready". ولا يلزم نقل الخادم الأساسي مرة أخرى؛ وإن رغبتم في ذلك، فنفّذوه بشكل مخطط باستخدام patronictl switchover في وقت هادئ.

09 · النسخ الاحتياطي والاستعادة وGoBD

يحمي النسخ المتماثل من الأعطال، لا من الأخطاء: صندوق البريد المحذوف عن طريق الخطأ يختفي من جميع المواقع في آن واحد. لذا أضيفوا نسخًا احتياطية وفق قاعدة 3-2-1: ثلاث نسخ، ونوعان من الوسائط، ونسخة واحدة خارج الموقع. [59]

flowchart LR
  PG[("PostgreSQL primary or replica")] -->|"pgBackRest, continuous WAL"| R1["Repo 1: Hetzner Storage Box (SFTP)"]
  PG -->|"pgBackRest"| R2["Repo 2: S3 at another provider"]
  GA["Garage (3 zones)"] -->|"rclone copy hourly"| AR["Object Storage with Object Lock (GoBD)"]
  GA -->|"rclone copy"| R1
  R1 --> T["Quarterly restore test"]
  R2 --> T
تدفق النسخ الاحتياطي
  • PostgreSQL: pgBackRest مع أرشفة WAL مستمرة (الاستعادة إلى نقطة زمنية). المستودع 1 على Hetzner Storage Box عبر SFTP (يدعم pgBackRest بروتوكول SFTP منذ الإصدار 2.46؛ وتوفر Storage Box بروتوكولات SFTP وSCP وrsync وBorgBackup وRestic وRclone وSMB وWebDAV)، والمستودع 2 على S3 لدى مزود آخر. وكلاهما مشفر. [58][15]
  • الكائنات الثنائية (Blobs): تُسمّى كائنات .eml بقيمة SHA-256 ولا تُعدَّل أبدًا. ويكفي rclone copy (وليس sync، فلا يحذف أبدًا) كنسخة كل ساعة.
  • أرشيف GoBD: يدعم Hetzner Object Storage ميزة Object Lock في وضعَي GOVERNANCE وCOMPLIANCE بالإضافة إلى إدارة الإصدارات؛ ويجب تفعيل Object Lock عند إنشاء الحاوية (Bucket). أنشئوا الحاوية مع مدة احتفاظ افتراضية (compliance)، فيُقفل كل كائن منسوخ تلقائيًا. وبدلًا من ذلك يمكن ترك nextxch يكتب مباشرة في هذه الحاوية (واجهة S3 أو Hybrid الخلفية)، لكن توافر كائنات Blob يعتمد حينها على Hetzner. وفي هذه الحالة ينشئ nextxch حاوية مفقودة مع Object Lock عند بدء التشغيل بمجرد تكوين وضع WORM (أو Storage:S3:ObjectLockEnabled)؛ ويعرض "Check S3 bucket" في أرشيف GoBD إمكانية الوصول وإدارة الإصدارات ووضع Object Lock ومدة الاحتفاظ الافتراضية. [18][17]
  • اختبار الاستعادة: استعيدوا pgBackRest مرة كل ربع سنة على الأقل في جهاز افتراضي للاختبار، وافتحوا عينات من نسخة الكائنات الثنائية (Blob).
pgBackRest: /etc/pgbackrest/pgbackrest.conf
# /etc/pgbackrest/pgbackrest.conf on every PostgreSQL site.
# Repo 1: Hetzner Storage Box over SFTP (pgBackRest 2.46+), repo 2: S3 at another provider.
[global]
repo1-type=sftp
repo1-path=/pgbackrest
repo1-sftp-host=uXXXXXX.your-storagebox.de
repo1-sftp-host-port=23
repo1-sftp-host-user=uXXXXXX
repo1-sftp-private-key-file=/var/lib/postgresql/.ssh/id_ed25519
repo1-sftp-host-key-hash-type=sha256
repo1-retention-full=2
repo1-cipher-type=aes-256-cbc
repo1-cipher-pass=<long random secret, stored outside the repo>

repo2-type=s3
repo2-path=/pgbackrest
repo2-s3-endpoint=<s3 endpoint of the second provider>
repo2-s3-bucket=nextxch-pg-backup
repo2-s3-region=<region>
repo2-s3-uri-style=path
repo2-retention-full=4
repo2-cipher-type=aes-256-cbc
repo2-cipher-pass=<second secret>

archive-async=y
process-max=2
start-fast=y

[nextxch]
pg1-path=/var/lib/postgresql/18/main
pg1-port=5432

# Weekly full, daily differential (cron on every node; pgBackRest runs only where it can,
# use --repo and the "backup-standby" option to take backups from a replica).
#   0 2 * * 0  pgbackrest --stanza=nextxch --type=full backup
#   0 2 * * 1-6 pgbackrest --stanza=nextxch --type=diff backup
rclone: نسخ الكائنات (Blobs) وحاوية GoBD
# Blob copy (all .eml objects) from Garage to an independent target, every hour.
# ~/.config/rclone/rclone.conf (keys are placeholders)
[garage]
type = s3
provider = Other
endpoint = http://127.0.0.1:3900
region = garage
access_key_id = <garage key>
secret_access_key = <garage secret>

[archive]
type = s3
provider = Other
endpoint = <object storage endpoint>
access_key_id = <key>
secret_access_key = <secret>

# Content-addressed (SHA-256 names, never modified): "copy" is enough and never deletes.
#   rclone copy garage:nextxch archive:nextxch-archive --immutable --transfers 8 --checksum
# For a GoBD copy create the target bucket WITH Object Lock and a default retention
# (COMPLIANCE, e.g. 10 years) so every copied object is locked automatically.

10 · التوسع: المستخدمون والتخزين وpods

flowchart LR
  S1["Start: 2 sites + witness"] --> S2["More mailboxes: add nextxch nodes per site"]
  S2 --> S3["DB grows: scale PostgreSQL VMs vertically (RAM, NVMe)"]
  S3 --> S4["More storage: add Garage nodes per zone"]
  S4 --> S5["Many mailboxes: Kubernetes per provider, HPA"]
مسار التوسع
المكوّنالتوسعملاحظة
عقد / حاويات (Pods) nextxchأفقيًاعديم الحالة؛ مزيد من العقد لكل موقع أو مزيد من pods (HPA). لا حاجة إلى الجلسات اللاصقة (Sticky Sessions).
PostgreSQL الأساسيرأسيًاتذهب جميع عمليات الكتابة إلى خادم أساسي واحد. ذاكرة RAM أكبر وأقراص أسرع (IOPS) بدلًا من مزيد من العقد؛ وقيمة البداية لـ shared_buffers هي 25% من RAM. لا يستخدم nextxch نسخ القراءة المتماثلة. [57]
مخزن Blobأفقيًامزيد من عقد Garage لكل منطقة مع السعة (garage layout assign -c)، ويُعاد توازن البيانات. وتقلّل إزالة التكرار عبر SHA-256 وضغط Zstd من الحاجة إلى ذلك.
SMTPعبر DNSتوزّع سجلات MX الإضافية ذات الأولوية المتساوية الحمل؛ وتتوزع قائمة الانتظار الصادرة على جميع العقد عبر SKIP LOCKED.
IMAPعبر DNS / موازن الحملتبقى اتصالات IDLE طويلة الأمد على عقدتها؛ ويصل البريد الجديد من العقد الأخرى عبر LISTEN/NOTIFY. وبعد إضافة عقدة، لا يتوزع الحمل إلا عند إعادة اتصال العملاء. [7]
HTTPS (Webmail وActiveSync وJMAP)عبر DNS / موازن الحملجلسات HMAC عديمة الحالة؛ ويمكن لأي طلب أن يصل إلى أي عقدة.

11 · التكلفة: أين يمكن التوفير وأين لا

حفظ

  • خطط vCPU مشتركة لـ nextxch والشاهد؛ والأنوية المخصصة فقط عندما يتطلبها حمل المعالج في قاعدة البيانات.
  • مدة 12 شهرًا لدى netcup (أقل بنسبة 13% من السعر الشهري وفق صفحة الأسعار). [22]
  • تجاوز الفشل عبر DNS بالوكيل المدمج (أو بسكربتكم الخاص) بدلًا من موازن تحميل مدفوع.
  • ضغط Zstd (Storage:Compression) وإلغاء تكرار SHA-256 للكائنات الثنائية (Blobs).

لا تقتصدوا في

  • ذاكرة RAM لعقد PostgreSQL: كل ما لا يُخزَّن مؤقتًا يكلّف عمليات إدخال/إخراج على القرص مع كل عرض لمجلد.
  • عمليات IOPS للقرص لـ PostgreSQL (NVMe)؛ إذ تحدد عمليات كتابة WAL زمن استجابة كل عملية تسليم.
  • الموقع الثالث: من دونه لا يوجد تجاوز فشل تلقائي آمن.
  • نسخ احتياطية مختبرة في وجهة مستقلة.

الأسعار المخزنة (صافية شهريًا، حسب حالة 2 نوفمبر 2026). تُدرج netcup وSTRATO أسعارًا إجمالية شاملة ضريبة القيمة المضافة بنسبة 19%، وقد قُسمت هنا على 1.19. ولدى Hetzner يُضاف 0.50 € لعنوان IPv4 الأساسي. وفي تاريخ الفحص عرضت Hetzner جميع الخطط السحابية بأنها "غير متاحة حاليًا"؛ ولم يتسنَّ تحديد ما إذا كان ذلك بسبب المخزون أم العرض. ولم يمكن قراءة سعر وحدات تخزين Hetzner من الصفحات الرسمية، لذا فهو غير موجود في الحاسبة. الأسعار تتغير: تحققوا لدى المزود قبل الطلب. [12][10][22][23][30][15][16]

المزودالخطةvCPUالذاكرة (RAM)القرص€ صافٍ / شهريًا
Hetzner CloudCX2324 GB40 GB€6.49
Hetzner CloudCX3348 GB80 GB€9.49
Hetzner CloudCX43816 GB160 GB€16.99
Hetzner CloudCX531632 GB320 GB€30.49
netcup VPSVPS 500 G12.524 GB64 GB€6.94
netcup VPSVPS 1000 G12.548 GB128 GB€12.18
netcup VPSVPS 2000 G12.5816 GB256 GB€22.62
netcup VPSVPS 4000 G12.51232 GB512 GB€38.12
netcup VPSVPS 8000 G12.51664 GB1024 GB€56.39
netcup Root ServerRS 1000 G12.548 GB128 GB€18.26
netcup Root ServerRS 2000 G12.5816 GB256 GB€34.20
netcup Root ServerRS 4000 G12.51232 GB512 GB€65.40
netcup Root ServerRS 8000 G12.51664 GB1024 GB€124.72
netcup Root ServerRS 12000 G12.52096 GB1536 GB€182.26
netcup Root ServerRS 16000 G12.524128 GB2048 GB€243.24
STRATO VPSVPS S12 GB60 GB€3.36
STRATO VPSVPS M24 GB120 GB€8.40
STRATO VPSVPS L48 GB240 GB€16.81
STRATO VPSVPS XL816 GB480 GB€32.77
STRATO VPSVPS XXL1224 GB720 GB€47.06
Hetzner Storage BoxBX11--1 TB€3.20
Hetzner Storage BoxBX21--5 TB€10.90
Hetzner Storage BoxBX31--10 TB€20.80
Hetzner Storage BoxBX41--20 TB€40.60
Hetzner Object Storageالسعر الأساسي شاملًا 1 TB تخزين + 1 TB حركة خروج--1 TB€6.49

12 · الحاسبة: الموارد والتكلفة

يعمل بالكامل في المتصفح دون خدمات خارجية. وتُعرض الافتراضات بشكل مفتوح أسفل النتيجة.

الافتراضات (غير مقيسة، يُرجى التحقق منها بأرقامكم الخاصة)
  • حجم البريد = عدد صناديق البريد × متوسط الحجم × (1 + النمو)^عدد السنوات. دون خصم لإزالة التكرار أو الضغط (تقدير متحفظ).
  • حجم PostgreSQL = 3% من حجم البريد (البيانات الوصفية والعلامات وفهرس النص الكامل)، وبحد أدنى 2 GB. وتأتي القيمة الفعلية من SELECT pg_database_size('nextxch').
  • RAM لـ PostgreSQL = 25% من حجم قاعدة البيانات، و2 GB على الأقل، مقرّبة للأعلى إلى 2/4/8/16/32/64/96/128 GB. القرص لـ PostgreSQL = 2 × حجم قاعدة البيانات (البيانات وWAL والهامش الاحتياطي).
  • عملية nextxch: ‏1 GB + 1 GB لكل 1,000 صندوق بريد بدأت (حد Kubernetes في المستودع هو 1 GiB لكل Pod). ‏etcd + Garage + نظام التشغيل: 1 GB. الشاهد (Witness): ‏2 GB RAM. نظام التشغيل: 20 GB من القرص.
  • Garage replication_factor 3: تخزّن كل منطقة (موقع أو شاهد) حجم البريد بالكامل.
  • الإعداد L: ما لا يقل عن 2 pod لكل مزود، وpod واحد لكل 1,500 صندوق بريد، وعمّال بذاكرة 8 GB؛ ويحتوي الجهاز الافتراضي للبيانات على PostgreSQL وetcd ومنطقة Garage.
  • حاجة النسخ الاحتياطي = 3 × حجم قاعدة البيانات (نسختان احتياطيتان كاملتان + WAL) + حجم البريد؛ الهدف: أصغر Hetzner Storage Box مناسب.
  • أسعار الترخيص من كتالوج الأسعار الحالي لهذا الموقع (يسري سعر الشريحة على جميع صناديق البريد، السعر السنوي / 12). يُعرض Cloudflare بالدولار الأمريكي ولا يُضاف إلى المجموع باليورو.

13 · إضافة مزود آخر (مثال STRATO)

  1. قبل الطلب: هل يمكن ضبط PTR؟ STRATO: يُضبط Reverse DNS لخوادم VPS في منطقة العملاء ضمن النطاقات، ويجب أن يوجد سجل A/AAAA أولًا، ويستغرق ذلك حتى 24 ساعة. المنفذ 25: بالنسبة إلى خوادم root توثّق STRATO أن الصادر محظور مع إمكانية رفع الحظر عبر الدعم؛ أما لخوادم VPS فلا توجد قاعدة موثقة. ولا توفر STRATO عنوان IP لتجاوز الفشل. [31][32][30]
  2. أضيفوا المضيف الجديد إلى مخزون Ansible مع عنوان IP خاص به في WireGuard ومنطقة Garage خاصة به، وأضيفوا النظير في جميع المواقع، ثم شغّلوا الـ playbook.
  3. etcd: أضيفوا العضو باستخدام etcdctl member add وشغّلوه مع ETCD_INITIAL_CLUSTER_STATE=existing. ومع أربعة مواقع لقاعدة البيانات، أبقوا عدد أعضاء etcd فرديًا. [48]
  4. يبدأ Patroni كنسخة متماثلة وينسخ البيانات تلقائيًا. Garage: garage layout assign مع المنطقة الجديدة، ثم garage layout apply.
  5. DNS: أضيفوا عنوان IP إلى SPF، وسجل MX جديدًا بالأولوية نفسها، والعقدة إلى قائمة DNS failover (الوكيل أو السكربت)، وسجل TLSA لاسم MX الجديد، واسم EHLO وسجل PTR للعقدة الجديدة، ووسّعوا الشهادة لتشمل الاسم. تلتقط سياسة MTA-STS سجل MX الجديد تلقائيًا (قائمة MX تلقائية)؛ ثم انشروا قيمة TXT الجديدة لـ _mta-sts من مستشار DNS.
  6. أضيفوا الموقع إلى مصفوفة خط التشغيل (Pipeline)، واختبروا انقطاع الموقع الجديد مرة واحدة.

14 · الحدود والثغرات (بصراحة)

اكتُشفت في الشيفرة أثناء كتابة هذا الدليل. أُغلقت أربع ثغرات حتى الآن: MTA-STS مع عدة أسماء MX، واسم EHLO لكل عقدة، وحاوية Object Lock تُنشأ تلقائيًا، ووكيل تجاوز الفشل عبر DNS المدمج. ويتعامل الدليل مع البقية كما هو موضح.

الثغرةالتأثيرالحل البديل
وكيل تجاوز فشل DNS (مغلق)مدمج، ويُفعَّل اختياريًا: فحوصات السلامة من كل عقدة، ولا يغيّر A/AAAA عبر Hetzner DNS أو Cloudflare إلا قائد العنقود.مزودو DNS الآخرون (مثل deSEC): سكربت تحت قفل etcd (الفصل 04).
MTA-STS مع عدة سجلات MX (مُغلق)تسمّي السياسة الخاصة بكل نطاق جميع أسماء MX (DNS MX، أو أسماء الخادم والعقد، أو قائمة خاصة مع أحرف البدل)؛ ويتبع معرّف السياسة القائمة.بعد كل تغيير في MX، انشروا قيمة TXT لـ _mta-sts من مستشار DNS.
اسم EHLO لكل عقدة (مُغلق)Smtp:HeloHostname أو إعداد لكل عقدة في /admin/، مع فحص PTR لكل عقدة؛ ويبقى اسم مضيف الخادم ساريًا على مستوى العنقود.-
وجهة S3 واحدة فقطلا كتابة مزدوجة في مخزنَي S3 لدى مزودين مختلفين؛ وتتطلب ميزات Object Lock مخزن S3 يدعم Object Lock.Garage عبر ثلاثة مواقع بالإضافة إلى نسخ rclone إلى حاوية Object Lock.
حاوية Object Lock (مغلقة)عند ضبط وضع WORM، ينشئ الخادم عند بدء التشغيل الحاوية (Bucket) المفقودة مع Object Lock. ويُبلَّغ عن حاوية موجودة دون Object Lock كتحذير وتنبيه، لأن Object Lock لا يمكن تفعيله إلا عند الإنشاء. [18]تحققوا في أي وقت عبر "Check S3 bucket" في أرشيف GoBD.
لا يوجد نسخ احتياطي مدمجلا توجد في الخادم خدمة نسخ احتياطي لقاعدة البيانات والكتل.pgBackRest وrclone (الفصل 09).
لا توجد نسخ قراءة متماثلةتذهب جميع الاستعلامات إلى الخادم الأساسي؛ ولا ترد النسخ المتماثلة للقراءة في خطة التطوير إلا بصفة "عند الطلب".وسّعوا خادم PostgreSQL الأساسي رأسيًا.
لا يوجد قياس أداء مُقاس لتحديد الحجمأحجام الذاكرة RAM وقاعدة البيانات في الحاسبة افتراضات.استبدلوها بقياساتكم الخاصة.

الأسئلة الشائعة

هل يمكن تشغيل nextxch بنمط Active-Active عبر عدة مزودين؟

نعم. مع Cluster:Enabled=true وPostgreSQL، تتشارك أي عدد من عقد nextxch الحالة عبر LISTEN/NOTIFY وجداول UNLOGGED وأقفال advisory. ويجب أن تصل جميع العقد إلى خادم PostgreSQL الأساسي نفسه وإلى مخزن كتل S3 نفسه؛ وعبر المزودين يتم ذلك عبر شبكة WireGuard.

هل أحتاج إلى موازن حمل أو Anycast؟

لا. يحتاج البريد الوارد فقط إلى عدة سجلات MX بالأفضلية نفسها (RFC 5321 القسم 5.1). أما IMAP والإرسال (Submission) وHTTPS فتحتاج إلى اسم واحد بقيمة TTL قصيرة، تُدار سجلات A الخاصة به بواسطة مهمة فحص صحة. ويمكن استخدام Anycast/BGP لكنه معقد.

كم عدد المواقع التي أحتاج إليها كحد أدنى؟

ثلاثة أصوات: موقعان كاملان إضافة إلى شاهد صغير (etcd، ومنطقة Garage، ومدقق تجاوز الفشل)، أو ثلاثة مواقع كاملة. ومع موقعين فقط لا يستطيع أي من الجانبين أن يحدد بأمان ما إذا كان الآخر قد تعطّل أم أن الرابط فقط هو الذي انقطع (انقسام الدماغ - Split Brain).

هل يُفقد البريد عندما يخرج مزود عن الخدمة؟

يواصل المرسلون إعادة المحاولة لمدة 4 إلى 5 أيام على الأقل (RFC 5321 القسم 4.5.4.1). ومع Patroni synchronous_mode وGarage replication_factor 3، تكون الرسالة المؤكدة بـ 250 OK مخزّنة بالفعل في موقع ثانٍ.

15 · المصادر

جرى التحقق من جميع الحقائق الخارجية في 2 نوفمبر 2026 مقابل المصادر الرسمية التالية. والنقاط التي تعذّر التحقق منها موسومة بذلك في النص.

  1. RFC 5321 (SMTP): 4.1.4 EHLO، 4.5.4.1 إعادة المحاولة، 5.1 MX: https://www.rfc-editor.org/rfc/rfc5321.html
  2. RFC 7208 (SPF): 4.6.4 حد استعلامات DNS، و5.6 ip4/ip6: https://www.rfc-editor.org/rfc/rfc7208.html
  3. RFC 6376 (DKIM): ‏3.1 المحددات: https://www.rfc-editor.org/rfc/rfc6376.html
  4. RFC 8461 (MTA-STS): 3.2، 4.1 أنماط mx: https://www.rfc-editor.org/rfc/rfc8461.html
  5. RFC 7672 (DANE لـ SMTP): 3.1 TLSA 3 1 1: https://www.rfc-editor.org/rfc/rfc7672.html
  6. RFC 8314 (TLS ضمني 465/993/995): https://www.rfc-editor.org/rfc/rfc8314.html
  7. RFC 2177 (IMAP IDLE، إعادة الإصدار كل 29 دقيقة): https://www.rfc-editor.org/rfc/rfc2177.html
  8. Google: إرشادات مرسلي البريد الإلكتروني (DNS الأمامي والعكسي): https://support.google.com/a/answer/81126
  9. Hetzner Docs: Floating IPs: https://docs.hetzner.com/cloud/floating-ips/overview
  10. Hetzner Docs: أسعار IPv4: https://docs.hetzner.com/general/others/ipv4-pricing
  11. Hetzner Docs: عناوين IP لتجاوز الفشل في Robot: https://docs.hetzner.com/robot/dedicated-server/ip/failover
  12. Hetzner Cloud: الخطط المحسّنة التكلفة: https://www.hetzner.com/cloud/cost-optimized/
  13. Hetzner Docs: الأسئلة الشائعة لخوادم السحابة (المنفذان 25/465): https://docs.hetzner.com/cloud/servers/faq/
  14. Hetzner Docs: سجلات PTR: https://docs.hetzner.com/networking/dns/record-types/ptr-record/
  15. Hetzner Storage Box (الأسعار والبروتوكولات): https://www.hetzner.com/storage/storage-box/
  16. Hetzner Object Storage (الأسعار): https://www.hetzner.com/storage/object-storage/
  17. Hetzner Docs: نظرة عامة على Object Storage: https://docs.hetzner.com/storage/object-storage/overview
  18. Hetzner Docs: Object Lock والاحتفاظ: https://docs.hetzner.com/storage/object-storage/howto-protect-objects/protect-object-lock-retention/
  19. وثائق Hetzner: الأسئلة الشائعة حول Load Balancer: https://docs.hetzner.com/cloud/load-balancers/faq
  20. Hetzner Docs: الأسئلة الشائعة حول الشبكات: https://docs.hetzner.com/cloud/networks/faq
  21. وثائق Hetzner: ‏DNS (Console وAPI): https://docs.hetzner.com/networking/dns/overview/
  22. netcup: أسعار VPS G12.5: https://www.netcup.com/de/server/vps
  23. netcup: أسعار Root Server G12.5: https://www.netcup.com/de/server/root-server
  24. مركز مساعدة netcup: عناوين IP وتجاوز الفشل: https://www.netcup.com/en/helpcenter/documentation/server/ip
  25. netcup: ملحقات الخادم (أسعار Failover IP): https://www.netcup.com/de/server/kvm-server-erweiterungen
  26. netcup Helpcenter: جدار الحماية (السياسة "netcup Mail block"): https://www.netcup.com/en/helpcenter/documentation/server/firewall
  27. netcup Helpcenter: الشبكة / rDNS: https://www.netcup.com/en/helpcenter/documentation/server/network-server
  28. netcup: Storage Space (NFS): https://www.netcup.com/en/server/storage-space
  29. netcup Helpcenter: DNS API: https://www.netcup.com/en/helpcenter/documentation/domain/our-api
  30. STRATO: أسعار VPS Linux: https://www.strato.de/server/linux-vserver/
  31. STRATO FAQ: Reverse DNS لخوادم VPS: https://www.strato.de/faq/server/wie-kann-ich-reverse-dns-fuer-meinen-vps-vds-einrichten/
  32. STRATO FAQ: جدار حماية الشبكة للخادم الجذري (المنفذ 25): https://www.strato.de/faq/server/wie-stelle-ich-die-netzwerk-firewall-bei-meinem-windows-root-server-ein/
  33. Cloudflare: سعر إضافة Load Balancing: https://www.cloudflare.com/plans/network-cdn/
  34. Cloudflare Docs: قيود Load Balancing (الفاصل الزمني لفحص السلامة): https://developers.cloudflare.com/load-balancing/reference/limitations/
  35. Cloudflare Docs: سجلات DNS لـ Load Balancing (MX/SRV في Enterprise فقط): https://developers.cloudflare.com/load-balancing/load-balancers/dns-records/
  36. Cloudflare Docs: أوضاع الوكيل (DNS-only): https://developers.cloudflare.com/load-balancing/understand-basics/proxy-modes/
  37. Cloudflare Docs: بروتوكولات Spectrum حسب الخطة: https://developers.cloudflare.com/spectrum/protocols-per-plan/
  38. وثائق Cloudflare: ‏DNS TTL: https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/
  39. Cloudflare API: تعديل سجل DNS: https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/edit/
  40. deSEC: النطاقات والحد الأدنى لـ TTL: https://desec.readthedocs.io/en/latest/dns/domains.html
  41. Akamai/Linode: تجاوز فشل IP (BGP، مركز البيانات نفسه): https://techdocs.akamai.com/cloud-computing/docs/configure-failover-on-a-compute-instance
  42. Akamai: أسعار NodeBalancer: https://www.akamai.com/cloud/pricing/nodebalancers
  43. Vultr: متطلبات BGP: https://docs.vultr.com/configuring-bgp-on-vultr
  44. RIPE NCC: طلب رقم AS: https://www.ripe.net/manage-ips-and-asns/as-numbers/request-an-as-number/
  45. Proxmox VE: Cluster Manager (زمن استجابة corosync، QDevice): https://pve.proxmox.com/wiki/Cluster_Manager
  46. Proxmox Backup Server: مخازن البيانات (بما في ذلك S3): https://pbs.proxmox.com/docs/storage.html
  47. WireGuard: https://www.wireguard.com/
  48. k3s: التوافر العالي مع etcd مدمج: https://docs.k3s.io/datastore/ha-embedded
  49. k3s: عنقود هجين موزع أو متعدد السحابات: https://docs.k3s.io/networking/distributed-multicloud
  50. etcd: الضبط (heartbeat وelection timeout): https://etcd.io/docs/v3.5/tuning/
  51. Talos Linux: KubeSpan: https://docs.siderolabs.com/talos/v1.11/networking/kubespan
  52. CloudNativePG: عناقيد النسخ المتماثلة: https://cloudnative-pg.io/docs/current/replica_cluster
  53. Patroni: الإعداد الديناميكي (ttl، loop_wait، retry_timeout): https://patroni.readthedocs.io/en/latest/dynamic_configuration.html
  54. Patroni: أوضاع النسخ المتماثل (synchronous_mode، strict): https://patroni.readthedocs.io/en/latest/replication_modes.html
  55. Patroni: وضع DCS failsafe: https://patroni.readthedocs.io/en/latest/dcs_failsafe_mode.html
  56. PostgreSQL 18: النسخ المتماثل المتزامن: https://www.postgresql.org/docs/current/warm-standby.html#SYNCHRONOUS-REPLICATION
  57. PostgreSQL 18: shared_buffers: https://www.postgresql.org/docs/current/runtime-config-resource.html
  58. pgBackRest (مستودعات S3 وSFTP): https://pgbackrest.org/
  59. CISA: خيارات النسخ الاحتياطي للبيانات (3-2-1): https://www.cisa.gov/sites/default/files/publications/data_backup_options.pdf
  60. Garage: صفحة المشروع (مراكز بيانات متعددة، 200 ms / 50 Mbps): https://garagehq.deuxfleurs.fr/
  61. Garage: التكوين (replication_factor، النصاب): https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/
  62. Garage: التوافق مع S3 (دون Object Lock، ودون إدارة الإصدارات): https://garagehq.deuxfleurs.fr/documentation/reference-manual/s3-compatibility/
  63. Npgsql: تجاوز الفشل وموازنة الحمل (متعدد المضيفات): https://www.npgsql.org/doc/failover-and-load-balancing.html
  64. Mermaid 12.2.0 (MIT)، مضمّن محليًا في هذا الموقع: https://www.npmjs.com/package/mermaid