nextxch بتوافر عالٍ عبر عدة مزودين
عندما يخرج مزود بأكمله عن الخدمة، ينبغي أن يستمر تدفق البريد: دون anycast مكلف، ودون موازنات حمل مؤسسية، وقابلًا للنشر من Git. يعرض هذا الدليل ثلاثة إعدادات على خوادم منخفضة التكلفة لدى Hetzner وnetcup وSTRATO، ويشرح ثانية بثانية ما يحدث أثناء الانقطاع، ويحسب الموارد والتكلفة. كل ادعاء يخص مزودًا مرتبط بمصدر مع تاريخ التحقق، وكل ادعاء يخص nextxch يذكر الملف المصدري في الشيفرة.
01 · ما يقدمه nextxch للتوافر العالي (تم التحقق في الشيفرة)
nextxch قادر على العمل بنمط Active-Active: تقبل كل عقدة SMTP وIMAP وJMAP وActiveSync وHTTPS، ولا توجد عقدة خاملة ولا جلسة لاصقة. ويحتفظ PostgreSQL بالحالة المشتركة، بينما تُخزَّن الرسائل نفسها ككائنات .eml معنونة بـ SHA-256 في مخزن الكائنات (Blob Store).
| القدرة | الطريقة | المصدر في الشيفرة |
|---|---|---|
| الشبكة الخلفية للعنقود دون Redis | PostgreSQL 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 |
| مخزن Blob | FileSystem أو 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 من TrustedProxyCidrs | src/Nextxch.Core/Abstractions/IClusterCoordinator.cs |
| نشر المخرجات (Artefacts) | docker-compose.yml، وملفات Kubernetes manifests (3 نسخ متماثلة، HPA من 2 إلى 10، PDB، probes)، وHelm chart، وdeb/rpm، وWindows MSI | deploy/kubernetes/, deploy/helm/nextxch/, deploy/packages/ |
| الترخيص | تُحتسب صناديق البريد النشطة، لا العقد ولا الـ pods؛ وتُشارَك حالة الترخيص عبر cluster_kv | src/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).
/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]
; 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)
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]# 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
# 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، خوادم مخصصة) | مراكز بيانات Hetzner | 4.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] |
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
لماذا Garage كمخزن للكائنات الثنائية؟
إن حاوية S3 لدى مزود واحد ستختفي باختفاء ذلك المزود، ولا يستطيع nextxch حاليًا الكتابة إلا إلى هدف S3 واحد. Garage مخزن متوافق مع S3 (AGPL-3.0) مصمم لعدة مراكز بيانات عبر الإنترنت (متطلبات المشروع: زمن انتقال 200 ms كحد أقصى، و50 Mbit/s على الأقل). ومع replication_factor = 3 توجد نسخة واحدة في كل منطقة؛ وتستمر القراءة والكتابة ما دامت منطقة واحدة فقط معطلة (نصاب الكتابة 2). ومع منطقتين فقط، سيؤدي فقدان إحداهما إلى جعل المخزن للقراءة فقط، ولهذا يحتفظ الشاهد (Witness) أيضًا بنسخة كاملة. [60][61]
خطوة بخطوة
- طلب الخوادم: A = Hetzner Cloud (المنفذان 25/465 محظوران افتراضيًا؛ اطلبوا رفع الحظر بعد شهر واحد وبعد سداد أول فاتورة، والمنفذ 587 مفتوح)، وB = netcup VPS (أزيلوا سياسة جدار الحماية "netcup Mail block" في SCP)، وC = STRATO VPS كشاهد (لا يحتاج الشاهد إلى المنفذ 25). [13][26]
- ضبط PTR: عنوان IP A إلى mx1.example.com (Hetzner Console)، وعنوان IP B إلى mx2.example.com (netcup SCP). [14][27]
- التثبيت الأساسي عبر cloud-init (Debian 13): مستخدم النشر وDocker وWireGuard، ودون تسجيل دخول بكلمة مرور.
- شبكة WireGuard متشابكة 10.88.0.0/24 بين A وB وC؛ افتحوا منفذ UDP 51820 لعناوين IP للنظراء فقط. [47]
- etcd على الثلاثة جميعها (مهلة الانتخاب 10 × RTT على الأقل)، ثم Patroni + PostgreSQL 18 على A وB. [50][53]
- Garage على الثلاثة جميعها، وطبّقوا تخطيطًا بثلاث مناطق، وأنشئوا الحاوية (Bucket) والمفتاح. [61]
- nextxch عبر Docker Compose على A وB (Cluster:Enabled=true، وسلسلة اتصال متعددة المضيفين، وS3 يشير إلى Garage المحلي). وفي /admin/ اضبطوا اسم المضيف mail.example.com وأصدروا شهادة ACME لـ mail وmx1 وmx2.
- DNS كما في الفصل 02، وتحققوا من قائمة MX الخاصة بـ MTA-STS في /admin/، وفعّلوا تجاوز الفشل عبر DNS في /admin/ ضمن النطاقات (يتصرف قائد العنقود فقط) أو بدلًا من ذلك السكربت تحت قفل etcd.
- النسخ الاحتياطية كما في الفصل 09، واختبروا الاستعادة. ثم تدرّبوا على انقطاع: أوقفوا الموقع A إيقافًا قسريًا واتبعوا الفصل 08.
#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 ]
# /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.
# /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.).
# /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
# /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
# 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 بحلقة مفاتيح الخادم ولا يُعرض مرة أخرى أبدًا.
# 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.
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]
#!/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
# /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"]
خطوة بخطوة
- ثلاثة أجهزة افتراضية (أو ضيوف Proxmox) لدى ثلاثة مزودين، بإصدار Debian نفسه، مع PTR إلى mx1/mx2/mx3 لكل منها.
- قائمة جرد Ansible بثلاثة مضيفين، ودليل تشغيل (Playbook) واحد للجميع (WireGuard وetcd وPatroni وGarage وnextxch).
- إعداد Patroni من الإعداد S مع مضيف ثالث؛ synchronous_mode: on، ومع ثلاث قواعد بيانات اختياريًا بالنصاب (Quorum). [54]
- سلسلة اتصال بثلاثة مضيفين، وتخطيط Garage بثلاث مناطق، وثلاثة سجلات MX، وثلاثة عناوين IP في SPF وفي قائمة عقد DNS failover.
- Proxmox Backup Server اختياري لصور الأجهزة الافتراضية: مخزن بيانات محلي أو S3 (الواجهة الخلفية S3 مدعومة رسميًا منذ PBS 4.2)؛ ولا توثّق Proxmox مشاركات الشبكة مثل Storage Box عبر CIFS كنوع لمخزن البيانات. [46]
# 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 }
# 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
لماذا لا CloudNativePG عبر العناقيد؟
يمكن لـ CloudNativePG تشغيل عناقيد نسخ متماثلة في عناقيد Kubernetes أخرى، لكن الترقية وخفض الرتبة بين العناقيد تصريحيان (الحقل primary، promotionToken)، أي ليسا تلقائيين. ولتجاوز الفشل التلقائي بين المزودين، يُعد Patroni مع etcd عبر ثلاثة مواقع الحل الأكثر مباشرة. [52]
خطوة بخطوة
- ابنوا طبقة البيانات كما في الإعداد M (ثلاثة أجهزة افتراضية مع Patroni وetcd وGarage).
- عنقود k3s أو Talos واحد لكل مزود؛ والاتصال الشبكي بطبقة البيانات عبر WireGuard (أو KubeSpan).
- المنافذ الواردة: خدمة من نوع LoadBalancer مع
externalTrafficPolicy: Local(كما في المستودع) للحفاظ على عنوان IP للمرسل من أجل SPF/DMARC؛ ومع موازن تحميل يدعم بروتوكول PROXY استخدموا بدلًا من ذلكCluster__EnableProxyProtocol=true. ويدعم Hetzner Cloud Load Balancer بروتوكول PROXY وأي خدمة TCP. [19] - المنفذ الصادر 25: خروج الحاويات (Pods) عبر عنوان IP ثابت مع سجل PTR وإدخال SPF مطابقين (بوابة خروج، كما هو موضح في deploy/kubernetes/README.md).
- قيم Helm لكل مزود، والنشر عبر خط المعالجة واحدًا تلو الآخر (الفصل 07). ويأتي HPA (من 2 إلى 10 pods عند 70% CPU) وPodDisruptionBudget مع المستودع.
# 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/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.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/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
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
| الوقت | ما الذي يحدث | ما يلاحظه المستخدمون |
|---|---|---|
| 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، يبقى 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).
# /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
# 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 Cloud | CX23 | 2 | 4 GB | 40 GB | €6.49 |
| Hetzner Cloud | CX33 | 4 | 8 GB | 80 GB | €9.49 |
| Hetzner Cloud | CX43 | 8 | 16 GB | 160 GB | €16.99 |
| Hetzner Cloud | CX53 | 16 | 32 GB | 320 GB | €30.49 |
| netcup VPS | VPS 500 G12.5 | 2 | 4 GB | 64 GB | €6.94 |
| netcup VPS | VPS 1000 G12.5 | 4 | 8 GB | 128 GB | €12.18 |
| netcup VPS | VPS 2000 G12.5 | 8 | 16 GB | 256 GB | €22.62 |
| netcup VPS | VPS 4000 G12.5 | 12 | 32 GB | 512 GB | €38.12 |
| netcup VPS | VPS 8000 G12.5 | 16 | 64 GB | 1024 GB | €56.39 |
| netcup Root Server | RS 1000 G12.5 | 4 | 8 GB | 128 GB | €18.26 |
| netcup Root Server | RS 2000 G12.5 | 8 | 16 GB | 256 GB | €34.20 |
| netcup Root Server | RS 4000 G12.5 | 12 | 32 GB | 512 GB | €65.40 |
| netcup Root Server | RS 8000 G12.5 | 16 | 64 GB | 1024 GB | €124.72 |
| netcup Root Server | RS 12000 G12.5 | 20 | 96 GB | 1536 GB | €182.26 |
| netcup Root Server | RS 16000 G12.5 | 24 | 128 GB | 2048 GB | €243.24 |
| STRATO VPS | VPS S | 1 | 2 GB | 60 GB | €3.36 |
| STRATO VPS | VPS M | 2 | 4 GB | 120 GB | €8.40 |
| STRATO VPS | VPS L | 4 | 8 GB | 240 GB | €16.81 |
| STRATO VPS | VPS XL | 8 | 16 GB | 480 GB | €32.77 |
| STRATO VPS | VPS XXL | 12 | 24 GB | 720 GB | €47.06 |
| Hetzner Storage Box | BX11 | - | - | 1 TB | €3.20 |
| Hetzner Storage Box | BX21 | - | - | 5 TB | €10.90 |
| Hetzner Storage Box | BX31 | - | - | 10 TB | €20.80 |
| Hetzner Storage Box | BX41 | - | - | 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)
- قبل الطلب: هل يمكن ضبط PTR؟ STRATO: يُضبط Reverse DNS لخوادم VPS في منطقة العملاء ضمن النطاقات، ويجب أن يوجد سجل A/AAAA أولًا، ويستغرق ذلك حتى 24 ساعة. المنفذ 25: بالنسبة إلى خوادم root توثّق STRATO أن الصادر محظور مع إمكانية رفع الحظر عبر الدعم؛ أما لخوادم VPS فلا توجد قاعدة موثقة. ولا توفر STRATO عنوان IP لتجاوز الفشل. [31][32][30]
- أضيفوا المضيف الجديد إلى مخزون Ansible مع عنوان IP خاص به في WireGuard ومنطقة Garage خاصة به، وأضيفوا النظير في جميع المواقع، ثم شغّلوا الـ playbook.
- etcd: أضيفوا العضو باستخدام
etcdctl member addوشغّلوه مع ETCD_INITIAL_CLUSTER_STATE=existing. ومع أربعة مواقع لقاعدة البيانات، أبقوا عدد أعضاء etcd فرديًا. [48] - يبدأ Patroni كنسخة متماثلة وينسخ البيانات تلقائيًا. Garage:
garage layout assignمع المنطقة الجديدة، ثمgarage layout apply. - DNS: أضيفوا عنوان IP إلى SPF، وسجل MX جديدًا بالأولوية نفسها، والعقدة إلى قائمة DNS failover (الوكيل أو السكربت)، وسجل TLSA لاسم MX الجديد، واسم EHLO وسجل PTR للعقدة الجديدة، ووسّعوا الشهادة لتشمل الاسم. تلتقط سياسة MTA-STS سجل MX الجديد تلقائيًا (قائمة MX تلقائية)؛ ثم انشروا قيمة TXT الجديدة لـ _mta-sts من مستشار DNS.
- أضيفوا الموقع إلى مصفوفة خط التشغيل (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 مقابل المصادر الرسمية التالية. والنقاط التي تعذّر التحقق منها موسومة بذلك في النص.
- RFC 5321 (SMTP): 4.1.4 EHLO، 4.5.4.1 إعادة المحاولة، 5.1 MX: https://www.rfc-editor.org/rfc/rfc5321.html
- RFC 7208 (SPF): 4.6.4 حد استعلامات DNS، و5.6 ip4/ip6: https://www.rfc-editor.org/rfc/rfc7208.html
- RFC 6376 (DKIM): 3.1 المحددات: https://www.rfc-editor.org/rfc/rfc6376.html
- RFC 8461 (MTA-STS): 3.2، 4.1 أنماط mx: https://www.rfc-editor.org/rfc/rfc8461.html
- RFC 7672 (DANE لـ SMTP): 3.1 TLSA 3 1 1: https://www.rfc-editor.org/rfc/rfc7672.html
- RFC 8314 (TLS ضمني 465/993/995): https://www.rfc-editor.org/rfc/rfc8314.html
- RFC 2177 (IMAP IDLE، إعادة الإصدار كل 29 دقيقة): https://www.rfc-editor.org/rfc/rfc2177.html
- Google: إرشادات مرسلي البريد الإلكتروني (DNS الأمامي والعكسي): https://support.google.com/a/answer/81126
- Hetzner Docs: Floating IPs: https://docs.hetzner.com/cloud/floating-ips/overview
- Hetzner Docs: أسعار IPv4: https://docs.hetzner.com/general/others/ipv4-pricing
- Hetzner Docs: عناوين IP لتجاوز الفشل في Robot: https://docs.hetzner.com/robot/dedicated-server/ip/failover
- Hetzner Cloud: الخطط المحسّنة التكلفة: https://www.hetzner.com/cloud/cost-optimized/
- Hetzner Docs: الأسئلة الشائعة لخوادم السحابة (المنفذان 25/465): https://docs.hetzner.com/cloud/servers/faq/
- Hetzner Docs: سجلات PTR: https://docs.hetzner.com/networking/dns/record-types/ptr-record/
- Hetzner Storage Box (الأسعار والبروتوكولات): https://www.hetzner.com/storage/storage-box/
- Hetzner Object Storage (الأسعار): https://www.hetzner.com/storage/object-storage/
- Hetzner Docs: نظرة عامة على Object Storage: https://docs.hetzner.com/storage/object-storage/overview
- Hetzner Docs: Object Lock والاحتفاظ: https://docs.hetzner.com/storage/object-storage/howto-protect-objects/protect-object-lock-retention/
- وثائق Hetzner: الأسئلة الشائعة حول Load Balancer: https://docs.hetzner.com/cloud/load-balancers/faq
- Hetzner Docs: الأسئلة الشائعة حول الشبكات: https://docs.hetzner.com/cloud/networks/faq
- وثائق Hetzner: DNS (Console وAPI): https://docs.hetzner.com/networking/dns/overview/
- netcup: أسعار VPS G12.5: https://www.netcup.com/de/server/vps
- netcup: أسعار Root Server G12.5: https://www.netcup.com/de/server/root-server
- مركز مساعدة netcup: عناوين IP وتجاوز الفشل: https://www.netcup.com/en/helpcenter/documentation/server/ip
- netcup: ملحقات الخادم (أسعار Failover IP): https://www.netcup.com/de/server/kvm-server-erweiterungen
- netcup Helpcenter: جدار الحماية (السياسة "netcup Mail block"): https://www.netcup.com/en/helpcenter/documentation/server/firewall
- netcup Helpcenter: الشبكة / rDNS: https://www.netcup.com/en/helpcenter/documentation/server/network-server
- netcup: Storage Space (NFS): https://www.netcup.com/en/server/storage-space
- netcup Helpcenter: DNS API: https://www.netcup.com/en/helpcenter/documentation/domain/our-api
- STRATO: أسعار VPS Linux: https://www.strato.de/server/linux-vserver/
- STRATO FAQ: Reverse DNS لخوادم VPS: https://www.strato.de/faq/server/wie-kann-ich-reverse-dns-fuer-meinen-vps-vds-einrichten/
- STRATO FAQ: جدار حماية الشبكة للخادم الجذري (المنفذ 25): https://www.strato.de/faq/server/wie-stelle-ich-die-netzwerk-firewall-bei-meinem-windows-root-server-ein/
- Cloudflare: سعر إضافة Load Balancing: https://www.cloudflare.com/plans/network-cdn/
- Cloudflare Docs: قيود Load Balancing (الفاصل الزمني لفحص السلامة): https://developers.cloudflare.com/load-balancing/reference/limitations/
- Cloudflare Docs: سجلات DNS لـ Load Balancing (MX/SRV في Enterprise فقط): https://developers.cloudflare.com/load-balancing/load-balancers/dns-records/
- Cloudflare Docs: أوضاع الوكيل (DNS-only): https://developers.cloudflare.com/load-balancing/understand-basics/proxy-modes/
- Cloudflare Docs: بروتوكولات Spectrum حسب الخطة: https://developers.cloudflare.com/spectrum/protocols-per-plan/
- وثائق Cloudflare: DNS TTL: https://developers.cloudflare.com/dns/manage-dns-records/reference/ttl/
- Cloudflare API: تعديل سجل DNS: https://developers.cloudflare.com/api/resources/dns/subresources/records/methods/edit/
- deSEC: النطاقات والحد الأدنى لـ TTL: https://desec.readthedocs.io/en/latest/dns/domains.html
- Akamai/Linode: تجاوز فشل IP (BGP، مركز البيانات نفسه): https://techdocs.akamai.com/cloud-computing/docs/configure-failover-on-a-compute-instance
- Akamai: أسعار NodeBalancer: https://www.akamai.com/cloud/pricing/nodebalancers
- Vultr: متطلبات BGP: https://docs.vultr.com/configuring-bgp-on-vultr
- RIPE NCC: طلب رقم AS: https://www.ripe.net/manage-ips-and-asns/as-numbers/request-an-as-number/
- Proxmox VE: Cluster Manager (زمن استجابة corosync، QDevice): https://pve.proxmox.com/wiki/Cluster_Manager
- Proxmox Backup Server: مخازن البيانات (بما في ذلك S3): https://pbs.proxmox.com/docs/storage.html
- WireGuard: https://www.wireguard.com/
- k3s: التوافر العالي مع etcd مدمج: https://docs.k3s.io/datastore/ha-embedded
- k3s: عنقود هجين موزع أو متعدد السحابات: https://docs.k3s.io/networking/distributed-multicloud
- etcd: الضبط (heartbeat وelection timeout): https://etcd.io/docs/v3.5/tuning/
- Talos Linux: KubeSpan: https://docs.siderolabs.com/talos/v1.11/networking/kubespan
- CloudNativePG: عناقيد النسخ المتماثلة: https://cloudnative-pg.io/docs/current/replica_cluster
- Patroni: الإعداد الديناميكي (ttl، loop_wait، retry_timeout): https://patroni.readthedocs.io/en/latest/dynamic_configuration.html
- Patroni: أوضاع النسخ المتماثل (synchronous_mode، strict): https://patroni.readthedocs.io/en/latest/replication_modes.html
- Patroni: وضع DCS failsafe: https://patroni.readthedocs.io/en/latest/dcs_failsafe_mode.html
- PostgreSQL 18: النسخ المتماثل المتزامن: https://www.postgresql.org/docs/current/warm-standby.html#SYNCHRONOUS-REPLICATION
- PostgreSQL 18: shared_buffers: https://www.postgresql.org/docs/current/runtime-config-resource.html
- pgBackRest (مستودعات S3 وSFTP): https://pgbackrest.org/
- CISA: خيارات النسخ الاحتياطي للبيانات (3-2-1): https://www.cisa.gov/sites/default/files/publications/data_backup_options.pdf
- Garage: صفحة المشروع (مراكز بيانات متعددة، 200 ms / 50 Mbps): https://garagehq.deuxfleurs.fr/
- Garage: التكوين (replication_factor، النصاب): https://garagehq.deuxfleurs.fr/documentation/reference-manual/configuration/
- Garage: التوافق مع S3 (دون Object Lock، ودون إدارة الإصدارات): https://garagehq.deuxfleurs.fr/documentation/reference-manual/s3-compatibility/
- Npgsql: تجاوز الفشل وموازنة الحمل (متعدد المضيفات): https://www.npgsql.org/doc/failover-and-load-balancing.html
- Mermaid 12.2.0 (MIT)، مضمّن محليًا في هذا الموقع: https://www.npmjs.com/package/mermaid