لنفرض أن لديك تطبيقاً يعمل على السيرفر Server في المنفذ 3000، وتريد أن يصل إليه المستخدم عبر نطاق Domain خاص به وبشهادة HTTPS، فالطريقة التقليدية أن تثبت Nginx وتكتب له ملف إعداد Configuration لكل نطاق، ثم تشغل certbot لتطلب الشهادة، ثم تضيف مهمة مجدولة Cron Job تجددها كل ثلاثة أشهر، ثم تتذكر أن تعيد تحميل Nginx بعد كل تجديد، وكل خطوة من هذه الخطوات قابلة للنسيان، والنتيجة المعروفة موقع يتوقف فجأة لأن شهادته انتهت في يوم لم ينتبه له أحد. لذلك ظهرت أدوات تجمع ال Reverse Proxy وإدارة الشهادات في مكان واحد، والفرق الأول بينها هو طريقة الإعداد، ففي Nginx Proxy Manager تدير النطاقات من واجهة ويب Web UI ويحفظ الإعداد في قاعدة بيانات Database، وفي Traefik تكتب التوجيه وسوماً Labels على كل Container، أما HAProxy فهو موازن حمل Load Balancer قبل كل شيء ويعتمد غالباً على أداة خارجية لإصدار الشهادات.
وهذا الدليل عن Caddy، وهو خادم ويب Web Server وReverse Proxy مفتوح المصدر Open Source مكتوب بلغة Go، يقرأ إعداده من ملف نصي واحد قصير اسمه Caddyfile، وHTTPS فيه مفعل افتراضياً لكل موقع تذكره، فأنت تكتب اسم النطاق وعنوان التطبيق في ثلاثة أسطر، ثم يطلب Caddy الشهادة من Let's Encrypt ويجددها قبل انتهائها ويحول زوار HTTP إلى HTTPS دون أي خطوة منك، ولهذا تختاره فرق كثيرة عندما تريد أقصر طريق إلى موقع آمن.
وسوف نناقش في هذا المقال ما يلي:
- متى يناسبك Caddy ومتى يناسبك غيره، وكيف يعمل HTTPS التلقائي وأين يحفظ الشهادات.
- تثبيت Caddy على Ubuntu 24.04 من ال Image الرسمي مع Docker Compose، وكتابة Caddyfile يوجه نطاقين إلى تطبيقين.
- ترويسات الأمان Security Headers، وكلمة مرور لمسار الإدارة، والضغط Compression، والسجلات Logs، وتوزيع الحمل Load Balancing.
- حماية ال Admin API، وإعادة التحميل دون انقطاع، والإضافات وشهادات Wildcard.
- التحقق من النجاح، والنسخ الاحتياطي، والتحديث ولماذا نتخطى الإصدار 2.11.6، وأشهر المشكلات وحلولها.
متى يناسبك Caddy، ومتى يناسبك غيره؟
اختر Caddy إذا أردت أن يكون الإعداد ملفاً نصياً تحفظه في Git وتراجعه كأي كود Code، وتريد أن تعمل الشهادات دون خطوة إضافية، وهو مناسب أيضاً لمن يشغل مواقع ثابتة Static Sites إلى جانب بضعة تطبيقات، لأنه خادم ملفات File Server كذلك وليس Reverse Proxy فقط. والجدول التالي يلخص الفرق بينه وبين الأدوات الثلاث الأخرى:
| المعيار | Caddy | Nginx Proxy Manager | Traefik | HAProxy |
|---|---|---|---|---|
| طريقة الإعداد | ملف Caddyfile قصير | واجهة ويب | وسوم Docker وملفات YAML | ملف haproxy.cfg |
| شهادات HTTPS | تلقائية افتراضياً لكل نطاق في الملف | من الواجهة لكل مضيف | تلقائية بعد تعريف Certificate Resolver | عبر أداة خارجية غالباً |
| اكتشاف ال Containers تلقائياً Service Discovery | لا يوجد في النسخة القياسية، ومتاح عبر إضافة | لا يوجد | نعم، من الوسوم | لا يوجد |
| الوصول إلى Docker socket | لا يحتاج إليه | لا يحتاج إليه | يحتاج إليه | لا يحتاج إليه |
| توزيع الحمل وفحوص الصحة Health Checks | متوفرة | محدودة | متوفرة | متقدمة، وهي غرضه الأساسي |
وفي المقابل، إذا كان من يدير السيرفر لا يريد أن يفتح ملفاً أصلاً فNginx Proxy Manager أيسر له، وإذا كانت تطبيقاتك تظهر وتختفي كل يوم وتريد أن يعلن كل منها عن نطاقه في ملف Compose الخاص به فTraefik أنسب، وإذا كانت لديك عدة سيرفرات خلفية Backend Servers وحركة كثيفة وخدمات TCP فHAProxy مصمم لهذا بالتحديد.
المتطلبات Requirements
- سيرفر يعمل بنظام Ubuntu 24.04 وعليه Docker Engine وCompose v2، وإذا لم يكن جاهزاً فاتبع دليل تثبيت Docker على Ubuntu، ثم دليل تأمين خادم VPS من أول دخول.
- نطاق تدير سجلات DNS Records الخاصة به، وفي هذا الدليل يشير السجلان
app.example.comوapi.example.comإلى العنوان العام Public IP للسيرفر203.0.113.10، وإذا كانت هذه السجلات جديدة عليك فقد شرحناها في شرح DNS وسجلاته للمبتدئين. - المنفذان Ports
80/tcpو443/tcpمفتوحان من الإنترنت في جدار الحماية Firewall لدى المزود وعلى السيرفر معاً، وإذا أردت HTTP/3 فافتح أيضاً443/udpلأنه مفعل في Caddy افتراضياً. - لا يستمع أي برنامج آخر على المنفذين 80 و443، فإذا لم يطبع الأمر التالي شيئاً فالمنفذان متاحان:
sudo ss -ltnp | grep -E ':(80|443) 'أما الموارد Resources فCaddy برنامج واحد صغير يكفيه أصغر خادم افتراضي خاص VPS لعدد من المواقع والتطبيقات، وإذا كانت بعض المصطلحات هنا جديدة عليك فراجع مصطلحات الاستضافة الذاتية للمبتدئين.
كيف يعمل HTTPS التلقائي Automatic HTTPS؟
كلما وجد Caddy اسم نطاق في عنوان موقع داخل ال Caddyfile فعل له ما يسميه توثيقه HTTPS التلقائي، حيث يطلب لكل اسم شهادة من جهة إصدار Certificate Authority عبر بروتوكول ACME، ويجددها في الخلفية، ويضيف تحويلاً Redirect من HTTP إلى HTTPS على المنفذ 80. وجهة الإصدار الافتراضية الأولى هي Let's Encrypt والثانية ZeroSSL، فإذا فشل الطلب من الأولى جرب الثانية، وهذا يعني أن تعطل جهة إصدار واحدة لا يوقف شهاداتك.
ولكي تثبت أنك تملك النطاق تطلب منك جهة الإصدار أن تمر بعملية تحقق Challenge، وCaddy يفعل طريقتين للتحقق افتراضياً ويختار بينهما عشوائياً في البداية، ثم يتعلم مع الوقت أيهما ينجح أكثر فيفضله:
- التحقق عبر HTTP (HTTP-01 Challenge): تطلب جهة الإصدار ملفاً مؤقتاً من النطاق على المنفذ 80.
- التحقق عبر TLS (TLS-ALPN-01 Challenge): تتحقق جهة الإصدار عبر مصافحة TLS Handshake خاصة على المنفذ 443.
- أما التحقق من ملكية النطاق عبر ال DNS (DNS-01 Challenge) فلا يحتاج إلى أي منفذ مفتوح، ولكنه يتطلب إضافة Plugin لمزود DNS، وسوف نشرحه في قسم الإضافات أدناه.
وبالتالي فلكي ينجح الإصدار يذكر التوثيق هذه الشروط: سجل A أو AAAA يشير إلى سيرفرك، والمنفذان 80 و443 مفتوحان من الخارج ويصلان إلى Caddy، واسم النطاق مكتوب في الإعداد، ومجلد البيانات قابل للكتابة ولا يضيع، والسبب في الشرط الأخير سوف تراه في القسم التالي.
ويجدر الانتباه إلى أن Caddy يلغي طلبات ACME الجارية كلما غيرت الإعداد وأعدت تحميله، كما يذكر التوثيق، لذلك إذا كنت تضيف عدة نطاقات فاجمع تعديلاتك في إعادة تحميل واحدة بدلاً من إعادة التحميل بعد كل سطر، حتى تعطي Caddy الوقت ليكمل إصدار الشهادات في الخلفية.
أين يحفظ Caddy الشهادات؟
يحفظ Caddy الشهادات ومفاتيحها الخاصة Private Keys وحساب ACME في مجلد البيانات Data Directory، وهذا المجلد في ال Image الرسمي هو /data والشهادات تحت /data/caddy/certificates، أما في /config فيحفظ نسخة من آخر إعداد عمل به باسم autosave.json.
/data فسوف يفقد الشهادات كلها عند كل إعادة إنشاء لل Container، ثم يطلبها من جديد لكل النطاقات دفعة واحدة، ومع تكرار ذلك تصطدم بحدود الإصدار في Let's Encrypt Rate Limits، وقد يتوقف إصدار الشهادات لنطاقك مدة تصل إلى أسبوع بحسب الحد الذي تجاوزته.التثبيت Installation
سوف نثبت الوسم Tag caddy:2.11.4 برقمه الكامل، وهو آخر إصدار سليم متاح في ال Image الرسمي على Docker Hub عند كتابة هذا الدليل، وتجد تغييراته في صفحة الإصدار v2.11.4، والسبب في أننا لم نختر الإصدار الأحدث 2.11.6 تجده في قسم التحديث. ولا تستخدم latest ولا 2، والسبب أن الإصدار سوف يتغير حينئذ دون علمك مع أول docker compose pull، وهذا ما حدث بالفعل لكل من كان يستخدم هذين الوسمين في 2 أكتوبر 2026، حيث انتقل إلى 2.11.6 بمشكلاته دون أن يقرر ذلك.
ولاحظ أن الإصدار 2.11.4 يتجاهل ترويسات الطلب التي في أسمائها شرطة سفلية Underscore مثل X_Custom لأسباب أمنية، فإذا كان تطبيقك يعتمد على ترويسة من هذا النوع فغير اسمها في التطبيق، لأن الخيار الذي يسمح بها لم يظهر إلا في الإصدارات التالية.
نبدأ بشبكة Docker مشتركة
نضع Caddy وكل تطبيق يخدمه على شبكة Docker Docker Network واحدة اسمها proxy، وبهذا يصل Caddy إلى كل Container باسمه ولا يحتاج أي تطبيق إلى نشر منفذ على السيرفر. وإذا أردت أن تفهم لماذا نفعل ذلك وما أنواع الشبكات في Docker، ولماذا نضع قاعدة البيانات على شبكة داخلية خاصة بها، فقد شرحنا ذلك بالتفصيل في دليل شبكات Docker وأفضل الممارسات. أنشئ الشبكة مرة واحدة:
docker network create proxyكيف نرتب مجلد Caddy؟
sudo mkdir -p /opt/caddy/conf /opt/caddy/data /opt/caddy/config /opt/caddy/logs
sudo chown -R $USER: /opt/caddy
cd /opt/caddyconf/Caddyfile: ملف الإعداد.data: الشهادات والمفاتيح وحساب ACME، ويقابله/dataداخل ال Container.config: آخر إعداد محفوظ، ويقابله/config.logs: سجلات الوصول Access Logs لكل موقع.
وسوف نستخدم مجلدات على السيرفر وليس Volumes يديرها Docker، والسبب أن النسخ الاحتياطي يصبح حينها نسخ مجلد واحد. وقد يتساءل البعض: لماذا نربط مجلد conf كاملاً وليس الملف وحده؟ والإجابة أن توثيق ال Image ينبه إلى ذلك صراحة، لأن أغلب المحررات Editors مثل vim تحفظ الملف بإنشاء نسخة جديدة منه، فإذا ربطت الملف مباشرة بقي ال Container يرى النسخة القديمة، ولن تعمل إعادة التحميل Reload كما تتوقع حتى تعيد إنشاء ال Container.
كيف تولد كلمة مرور مسار الإدارة؟
سوف نحمي المسار /admin/ في التطبيق بكلمة مرور، وCaddy لا يقبل كلمة المرور نصاً صريحاً، وإنما تحولها إلى هاش Hash بالأمر caddy hash-password الموجود داخل ال Image نفسه:
docker run --rm -it caddy:2.11.4 caddy hash-passwordيطلب الأمر كلمة المرور مرتين ثم يطبع هاش bcrypt يبدأ ب $2a$14$، فانسخه كما هو لأنك سوف تضعه في ال Caddyfile في الخطوة التالية.
نكتب ملف الإعداد Caddyfile
الملف /opt/caddy/conf/Caddyfile يوجه النطاق app.example.com إلى ال Container whoami على المنفذ 80، والنطاق api.example.com إلى ال Container api على المنفذ 3000، وهذا محتواه كاملاً:
{
email [email protected]
}
(security) {
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "DENY"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}
}
app.example.com {
import security
encode zstd gzip
log {
output file /var/log/caddy/app.example.com.log
}
basic_auth /admin/* {
admin $2a$14$WMIMdolrjA3dw5cd3Es81.ZiXtipyQTZ3KuVrCB00Tjx1KMs7j6X6
}
reverse_proxy whoami:80
}
api.example.com {
import security
encode zstd gzip
log {
output file /var/log/caddy/api.example.com.log
}
reverse_proxy api:3000
}في الإعداد أعلاه لاحظ التالي:
- الكتلة الأولى بين قوسين دون اسم هي الخيارات العامة Global Options، والخيار
emailيربط حساب ACME ببريدك، ويوصي به التوثيق حتى تصلك رسائل جهة الإصدار إذا ظهرت مشكلة في شهاداتك. - الكتلة
(security)مقتطف Snippet لا يعمل وحده، وإنما تدرجه في أي موقع بالأمرimport security، فتكتب ترويسات الأمان مرة واحدة وتعدلها في مكان واحد. - كل كتلة تبدأ باسم نطاق هي موقع Site، ويكفي أن تذكر الاسم ليطلب Caddy شهادته ويضيف التحويل من HTTP.
- التوجيه
reverse_proxy whoami:80يمرر كل طلب إلى ال Container باسمه عبر الشبكةproxy، لأن Docker يحل أسماء ال Containers على الشبكات التي ينشئها المستخدم، ويضيف Caddy الترويساتX-Forwarded-ForوX-Forwarded-ProtoوX-Forwarded-Hostفيعرف التطبيق عنوان الزائر وأنه جاء عبر HTTPS، ويتجاهل Caddy القيم التي يرسلها الزائر نفسه في هذه الترويسات حتى لا ينتحلها.
أما بقية الأسطر، أي الترويسات وكلمة المرور والضغط والسجلات، فسوف نشرحها في أقسامها أدناه، وترتيبها داخل الموقع لا يهم لأن Caddy يرتب التوجيهات Directives ترتيباً ثابتاً، فهو يطبق basic_auth مثلاً قبل reverse_proxy أينما كتبته.
نشغل Caddy بملف Compose
الملف /opt/caddy/compose.yaml:
services:
caddy:
image: caddy:2.11.4
container_name: caddy
restart: unless-stopped
cap_add:
- NET_ADMIN
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy:ro
- ./data:/data
- ./config:/config
- ./logs:/var/log/caddy
networks:
- proxy
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:2019/config/"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
start_interval: 2s
networks:
proxy:
external: trueفي الإعداد أعلاه لاحظ التالي:
- يشغل ال Image افتراضياً الأمر
caddy run --config /etc/caddy/Caddyfile --adapter caddyfile، لذلك لا نحتاج إلىcommand. - المنفذ
443/udpضروري ل HTTP/3، والصلاحية CapabilityNET_ADMINتسمح ل Caddy برفع حجم مخازن UDP Buffers فيتحسن أداء HTTP/3، وهي اختيارية بحسب توثيق ال Image، فإذا لم تحتج إلى HTTP/3 فاحذف السطرين معاً. - لا نربط
/var/run/docker.sock، لأن Caddy يقرأ التوجيه من ال Caddyfile وليس من Docker، وبذلك يبقى باب حساس مغلقاً يحتاج Traefik إلى فتحه. - يفحص
healthcheckال Admin API داخل ال Container على127.0.0.1:2019، ولا ننشر هذا المنفذ على السيرفر، والسبب في قسم ال Admin API. - نربط
confللقراءة فقط:ro، فلا شيء داخل ال Container يستطيع تعديل إعداده.
نجرب بتطبيق صغير
سوف نختبر بالتطبيق الصغير whoami، وهو يعيد تفاصيل الطلب كما وصلته فترى الترويسات التي أضافها Caddy. وهذا هو الملف /opt/whoami/compose.yaml، ولاحظ أنه لا ينشر أي منفذ:
services:
whoami:
image: traefik/whoami:v1.12.0
container_name: whoami
restart: unless-stopped
networks:
- proxy
networks:
proxy:
external: truesudo mkdir -p /opt/whoami
sudo chown $USER: /opt/whoami
cd /opt/whoami
docker compose up -dوبالطريقة نفسها أضف تطبيقك الحقيقي إلى الشبكة proxy باسم api ليستمع على المنفذ 3000 داخل ال Container، وإذا اختلف اسم ال Container أو منفذه فعدل سطر reverse_proxy ليطابقه.
افحص الإعداد قبل التشغيل
قبل التشغيل الأول افحص الإعداد بالأمر caddy validate، حيث يقرأ الإعداد ويجهز كل وحداته Modules كأنه سيعمل ثم يخرج دون أن يستمع على أي منفذ:
cd /opt/caddy
docker compose run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfileوالمخرج سوف يكون كما يلي:
Valid configurationبعد ذلك شغل الخدمة وانتظر حتى تصبح حالتها healthy:
docker compose up -d
docker compose ps
docker compose logs --tail 30 caddyNAME IMAGE STATUS
caddy caddy:2.11.4 Up 10 seconds (healthy)ويطلب Caddy الشهادات في الخلفية بعد الإقلاع مباشرة، فلا يتأخر بدء الخدمة بسببها، وسوف ترى في السجل لكل نطاق رسالة obtaining certificate ثم certificate obtained successfully.
acme_ca https://acme-staging-v02.api.letsencrypt.org/directory، فيستخدم Caddy بيئة الاختبار Staging في Let's Encrypt وحدودها أوسع بكثير، ولكن شهاداتها غير موثوقة في المتصفح Browser. وبعد نجاح التجربة احذف السطر وأعد التحميل فيطلب Caddy شهادات حقيقية، ولا يلزمك حذف شيء من data لأن Caddy يحفظ شهادات كل جهة إصدار في مجلد مستقل.وإذا أردت التثبيت من مستودع apt الرسمي
إذا أردت تشغيل Caddy كخدمة systemd عادية دون Docker، فالمشروع يوفر مستودع apt رسمياً للإصدارات المستقرة، وهذه أوامر إضافته كما في صفحة التثبيت:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo chmod o+r /usr/share/keyrings/caddy-stable-archive-keyring.gpg
sudo chmod o+r /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddyوتشغل الحزمة الخدمة caddy تلقائياً بالمستخدم caddy، والإعداد في /etc/caddy/Caddyfile، والبيانات في /var/lib/caddy/.local/share/caddy، وإعادة التحميل بالأمر sudo systemctl reload caddy، كما يشرح دليل التشغيل. ويثبت المستودع أحدث إصدار مستقر، وقد يسبق ال Image الرسمي بأيام، فعند كتابة هذا الدليل كان المستودع يقدم الإصدار 2.11.7 بينما لم يصل Docker Hub بعد إلا إلى 2.11.6. ولكن انتبه إلى أن التطبيقات هنا تعمل في Containers وCaddy خارجها، فلا يصل إليها بأسمائها، وإنما ينشر كل تطبيق منفذه على 127.0.0.1 وتكتب reverse_proxy 127.0.0.1:8080.
ترويسات الأمان في مقتطف واحد
يستخدم المقتطف security التوجيه header ليضيف أربع ترويسات إلى كل استجابة ويحذف واحدة، وتجد شرح كل ترويسة وبقية الترويسات التي تحتاجها في دليل ترويسات الأمان بالتفصيل: HSTS وCSP:
Strict-Transport-Security: يطلب من المتصفح ألا يستخدم HTTP مع النطاق مدة سنة، وهذا ما يسمى HSTS.X-Content-Type-Options: nosniff: يمنع المتصفح من تخمين نوع الملف.X-Frame-Options: DENY: يمنع عرض الصفحة داخل إطار iframe في موقع آخر.Referrer-Policy: يقلل ما يرسله المتصفح من روابط صفحاتك إلى المواقع الأخرى.-Server: العلامة-تعني حذف الترويسة، فلا يعلن السيرفر اسمه في كل استجابة.
includeSubDomains يشمل كل نطاق فرعي Subdomain تحت النطاق، لذلك ابدأ بقيمة صغيرة مثل max-age=300 ثم ارفعها إلى سنة بعد التأكد من أن الشهادات تصدر وتتجدد لكل الأسماء، وإذا كان أحد التطبيقات يحتاج إلى العرض داخل إطار فاكتب له مقتطفاً آخر دون X-Frame-Options.حماية مسار الإدارة بكلمة مرور
يطلب التوجيه basic_auth اسم مستخدم وكلمة مرور بمصادقة HTTP الأساسية Basic Authentication، والمطابق Matcher /admin/* يقصر الحماية على هذا المسار وتبقى بقية التطبيق مفتوحة، فإذا أردت حماية الموقع كله فاحذف المسار واكتب basic_auth { مباشرة.
ويمكنك أن تضيف أكثر من مستخدم كل واحد في سطر داخل الكتلة، ولاحظ أن اسم التوجيه تغير في الإصدار 2.8 من basicauth إلى basic_auth، فإذا وجدت الاسم القديم في أمثلة على الإنترنت فاعلم أنها كتبت قبل ذلك الإصدار.
كيف يضغط Caddy الاستجابات ويكتب السجلات؟
يضغط التوجيه encode zstd gzip الاستجابات بخوارزمية Zstandard للمتصفحات التي تدعمها وب gzip لغيرها، ولا يضغط Caddy الاستجابة التي يقل حجمها عن 512 بايت لأن الفائدة فيها ضئيلة.
ويكتب التوجيه log سطراً بصيغة JSON لكل طلب يصل إلى الموقع في ملف خاص بالموقع داخل /opt/caddy/logs، وعند 100 ميجابايت ينتقل Caddy بنفسه إلى ملف جديد Log Rotation ويحتفظ بعشرة ملفات قديمة مدة أقصاها 90 يوماً، فلا يمتلئ القرص. ويخفي افتراضياً قيم الترويسات الحساسة مثل Cookie وAuthorization فتظهر في السجل بالقيمة REDACTED، وبالتالي لا تتسرب كلمة مرور مسار الإدارة إلى ملفات السجلات.
tail -n 1 /opt/caddy/logs/app.example.com.logأما سجل Caddy نفسه، أي رسائل الإقلاع والشهادات والأخطاء، فيكتبه على المخرج القياسي Standard Output وتقرؤه بالأمر docker compose logs caddy.
كيف توزع الحمل على أكثر من نسخة من التطبيق؟
إذا شغلت من التطبيق نسختين فاذكرهما معاً في reverse_proxy ويوزع Caddy الطلبات عليهما، وهذا موقع api.example.com بعد تعديله ليخدمه ال Containers api1 وapi2:
api.example.com {
import security
encode zstd gzip
log {
output file /var/log/caddy/api.example.com.log
}
reverse_proxy api1:3000 api2:3000 {
lb_policy round_robin
health_uri /health
health_interval 10s
health_timeout 2s
}
}في الإعداد أعلاه لاحظ التالي:
- يحدد
lb_policyطريقة التوزيع، والقيمة الافتراضيةrandom، واخترنا هناround_robinأي التناوب، ومن الخيارات الأخرىleast_connللنسخة الأقل اتصالات، وip_hashوcookieلإبقاء الزائر على النسخة نفسها. - يفعل
health_uriالفحص النشط Active Health Check، حيث يطلب Caddy المسار/healthمن كل نسخة كل 10 ثوان، والنسخة التي لا تعيد الرمز 200 خلال ثانيتين يستبعدها حتى تتعافى. - وإذا لم يكن في تطبيقك مسار للفحص فاستخدم الفحص السلبي Passive Health Check بإضافة
fail_duration 30s، وعندها يستبعد Caddy النسخة التي فشل فيها طلب حقيقي مدة 30 ثانية.
وإذا احتجت إلى توزيع الحمل على عدة سيرفرات مع فحوص صحة دقيقة ووضع TCP فراجع دليل HAProxy لأنه مصمم لهذا.
أبق ال Admin API داخل السيرفر
يدير Caddy إعداده عبر Admin API يستمع افتراضياً على localhost:2019، ومن خلاله يطبق الأمر caddy reload الإعداد الجديد، وتستطيع قراءة الإعداد الحالي بصيغة JSON من داخل ال Container:
docker compose exec caddy wget -qO- http://127.0.0.1:2019/config/ولكن هذا ال API لا يطلب أي مصادقة Authentication في إعداده الافتراضي، فمن يصل إليه يستطيع أن يستبدل الإعداد كله أو يوجه نطاقاتك إلى أي مكان يريده، لذلك لا تضف 2019:2019 إلى ports، ولا تغير عنوانه إلى 0.0.0.0:2019 بالخيار admin. وفي ملف Compose أعلاه يستمع ال API على 127.0.0.1 داخل ال Container وحده، فلا يصل إليه أحد من السيرفر ولا من الإنترنت.
وقد يتساءل البعض: إذا كان ال API بهذه الخطورة فلماذا لا نعطله تماماً بالخيار العام admin off؟ والإجابة أن الأمر caddy reload يتوقف عن العمل عندها لأنه يرسل الإعداد الجديد عبر هذا ال API نفسه، فلا يبقى أمامك لتطبيق أي تعديل إلا إيقاف Caddy وتشغيله، ويفشل كذلك فحص الصحة في ملف Compose، لذلك نبقيه مفعلاً ومغلقاً.
التحقق من الإعداد وإعادة التحميل دون انقطاع
بعد كل تعديل على ال Caddyfile رتبه أولاً بالأمر caddy fmt، والخيار --diff يعرض الفروق بين ملفك والصيغة المعتمدة دون أن يغير شيئاً:
cd /opt/caddy
docker compose exec caddy caddy fmt --diff /etc/caddy/Caddyfileوالمجلد مربوط للقراءة فقط، فإذا أردت كتابة الصيغة المرتبة في الملف فاكتبها من Container مؤقت:
docker run --rm -v /opt/caddy/conf:/etc/caddy caddy:2.11.4 caddy fmt --overwrite /etc/caddy/Caddyfileثم افحص الإعداد وطبقه بالأمر caddy reload داخل ال Container العامل، والخيار -w /etc/caddy يحدد مجلد العمل Working Directory فيجد الأمر الملف Caddyfile فيه:
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
docker compose exec -w /etc/caddy caddy caddy reloadويطبق Caddy الإعداد الجديد دون أن يقطع الاتصالات المفتوحة، وهذا ما يوصي به توثيق ال Image بدلاً من إعادة تشغيل ال Container. والآن لنكسر الإعداد عمداً لنرى ما يحدث: إذا كتبت reverse_proxx بدلاً من reverse_proxy في موقع app.example.com ثم أعدت التحميل، فسوف يرفض Caddy الإعداد الجديد ويستمر في العمل بالقديم، ويطبع سبب الرفض مع رقم السطر:
Error: adapting config using caddyfile: Caddyfile:24: unrecognized directive: reverse_proxxولكن تذكر أن الملف الخاطئ يبقى على القرص، ولن يقلع به Caddy عند أول إعادة تشغيل للسيرفر، لذلك أصلح الخطأ فوراً ولا تتركه لوقت لاحق.
متى تحتاج إلى الإضافات Plugins وشهادات Wildcard؟
النسخة القياسية من Caddy لا تضم وحدات لمزودي DNS، وأنت تحتاج إلى إحداها في حالتين: الأولى شهادة Wildcard مثل *.example.com لأن Let's Encrypt لا تصدرها إلا بالتحقق عبر ال DNS (DNS-01 Challenge)، والثانية شهادة لخدمة داخلية لا يصل إليها الإنترنت. وفي الحالتين تبني نسخة من Caddy تضم وحدة مزودك عبر xcaddy الموجود في ال Image ذي الوسم builder.
وهذا الملف /opt/caddy/Dockerfile يضيف وحدة Cloudflare كمثال:
FROM caddy:2.11.4-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/[email protected]
FROM caddy:2.11.4
COPY --from=builder /usr/bin/caddy /usr/bin/caddyبعد ذلك استبدل في compose.yaml السطر image: caddy:2.11.4 بالسطر build: .، ومرر توكن ال API الخاص بمزود DNS عبر متغير بيئة Environment Variable في .env، ثم اكتب في موقع ال Wildcard:
*.example.com {
tls {
dns cloudflare {env.CF_API_TOKEN}
}
reverse_proxy whoami:80
}وتجد وحدات المزودين الأخرى في صفحة التنزيل، ومن الإضافات المعروفة أيضاً caddy-docker-proxy التي تقرأ التوجيه من وسوم Docker كما يفعل Traefik، وهي التي تشغلها Coolify عندما تختار Caddy. ولكن تذكر أن كل إضافة هي كود Code من طرف آخر يعمل داخل البرنامج المعرض للإنترنت مباشرة، ويحتاج إلى إعادة بناء مع كل تحديث، لذلك لا تضف إلا ما تحتاج إليه فعلاً.
التحقق من النجاح Verification
- يعرض الأمر
docker compose psفي/opt/caddyال Containercaddyبحالةhealthy. - يعيد طلب HTTP تحويلاً دائماً إلى HTTPS بالرمز
308:
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://app.example.com/308 https://app.example.com/- يعيد طلب HTTPS الرمز
200دون الخيار-k، أي أن الشهادة موثوقة، ومعه ترويسات الأمان:
curl -s -D - -o /dev/null https://app.example.com/HTTP/2 200
alt-svc: h3=":443"; ma=2592000
referrer-policy: strict-origin-when-cross-origin
strict-transport-security: max-age=31536000; includeSubDomains
via: 1.1 Caddy
x-content-type-options: nosniff
x-frame-options: DENY
...- يعرض الأمر التالي جهة الإصدار وتاريخ انتهاء الشهادة:
echo | openssl s_client -connect app.example.com:443 -servername app.example.com 2>/dev/null | openssl x509 -noout -issuer -dates- يعيد المسار
https://app.example.com/admin/الرمز401دون كلمة المرور، والرمز200مع الخيار-u admin. - في جسم الاستجابة من whoami يظهر السطر
X-Forwarded-Proto: https، أي أن التطبيق يعرف أن الزائر جاء عبر HTTPS. - في
/opt/caddy/logs/app.example.com.logسطر لكل طلب، وhttp://203.0.113.10:2019لا يرد من خارج السيرفر.
النسخ الاحتياطي Backup والاستعادة Restore
كل ما يحتاج إليه Caddy موجود في /opt/caddy: ال Caddyfile في conf، والشهادات وحساب ACME في data، وآخر إعداد في config، وملف Compose، لذلك يكفي أن تنسخ المجلد دون أن توقف Caddy:
sudo tar czf /root/caddy-$(date +%F).tar.gz -C /opt caddyوللاستعادة على سيرفر جديد ثبت Docker، وأنشئ الشبكة، وفك النسخة، ثم شغل الخدمة:
docker network create proxy
sudo tar xzf /root/caddy-2026-10-02.tar.gz -C /opt
sudo chown -R $USER: /opt/caddy
cd /opt/caddy
docker compose up -dومع مجلد data يستخدم Caddy الشهادات الحالية مباشرة ولا يطلب شهادات جديدة لكل النطاقات دفعة واحدة، وبعد ذلك انقل التطبيقات مع ملفات Compose الخاصة بها وغير سجلات DNS إلى العنوان الجديد، ويمكنك أن تستثني مجلد logs من النسخة إذا كان كبيراً ولا تحتاج إليه.
data أبداً.التحديث Upgrade إلى إصدار أحدث
- راجع صفحة الإصدارات واقرأ قسم التغييرات التي تكسر التوافق Breaking Changes في كل إصدار بين إصدارك والإصدار الجديد، ثم تأكد أن الوسم الجديد ظهر في وسوم ال Image الرسمي، لأنه يتأخر عادة أياماً عن صفحة الإصدارات.
- أنشئ نسخة احتياطية كما في القسم السابق.
- غير الوسم في
compose.yamlإلى الإصدار الجديد برقمه الكامل، ثم افحص إعدادك به في Container مؤقت قبل أن تلمس ال Container العامل:
cd /opt/caddy
docker compose pull
docker compose run --rm --no-deps caddy caddy validate --config /etc/caddy/Caddyfile- إذا نجح الفحص فأعد إنشاء ال Container وتابع السجل:
docker compose up -d
docker compose logs --tail 30 caddyوأثناء إعادة إنشاء ال Container تنقطع الاتصالات المفتوحة بضع ثوان، لذلك اختر وقتاً قليل الحركة، وإذا ظهرت مشكلة فأعد الوسم القديم ونفذ docker compose up -d، وإذا كنت تستخدم إضافات فغير الوسمين في Dockerfile ونفذ docker compose build --pull قبل ذلك.
ولنأخذ مثالاً حياً على أهمية قراءة صفحة الإصدارات قبل التحديث، فقد صدر الإصدار v2.11.6 في 1 أكتوبر 2026 ومعه إصلاحات أمنية، أحدها يخص المواقع التي تجمع forward_auth وreverse_proxy، ولكنه غير أيضاً بعض السلوك الافتراضي، فهو يرفض الطلبات التي يزيد حجم ترويساتها على 16 كيلوبايت بالرمز 431، ويقطع الاتصال الذي تتوقف فيه قراءة جسم الطلب أو كتابة الاستجابة أكثر من دقيقة، ويحذف ترويسات الطلب التي في أسمائها نقطة، ويشترط Go 1.26 لبناء الإضافات. وبعد يومين فقط صدر الإصدار v2.11.7 في 3 أكتوبر 2026 ليصلح مشكلات سببتها هذه المهلات الزمنية Timeouts الجديدة في 2.11.6، أهمها أن Caddy قد ينهار Panic عند العمل ك Reverse Proxy عبر HTTP/2 إذا كان ما زال يقرأ جسم الطلب، وأن البث المستمر Streaming مثل SSE الذي يفتحه الكلاينت بطلب POST كان ينقطع بعد 60 ثانية بالضبط، ويوصي فريق Caddy كل من انتقل إلى 2.11.6 بالترقية.
لذلك تخط الإصدار 2.11.6 تماماً، والسبب أن ال Reverse Proxy الذي ينهار تحت HTTP/2 أو يقطع البث بعد دقيقة أسوأ من البقاء على إصدار سليم أقدم بشهور. وعند كتابة هذا الدليل لم يكن الوسم caddy:2.11.7 قد ظهر بعد في وسوم ال Image الرسمي، فبقينا على 2.11.4، وعندما يظهر انتقل إليه مباشرة بالخطوات أعلاه، وإذا كان تطبيقك يعتمد على Cookies كبيرة أو اتصالات طويلة صامتة فاقرأ عن الخيارين max_header_size وtimeouts في ملاحظات الإصدار 2.11.6 قبل أن تنتقل، لأن هذه التغييرات باقية في 2.11.7.
مشكلات شائعة وحلولها Troubleshooting
لا تصدر الشهادة
ابحث في سجل Caddy عن سبب الرفض:
docker compose logs caddy | grep -i -E 'obtain|challenge|acme' | tail -n 10وغالباً سوف تجد رسالة could not get certificate from issuer ومعها السبب الذي ذكرته جهة الإصدار، فإذا كان السبب Timeout during connect أو Connection refused فالمنفذان 80 و443 لا يصلان إلى Caddy من الإنترنت، فراجع جدار الحماية لدى المزود وعلى السيرفر. وإذا كان السبب NXDOMAIN أو عنواناً غير عنوان سيرفرك فسجل DNS لم ينتشر بعد أو يشير إلى مكان خاطئ، فتحقق منه بالأمر dig +short app.example.com، وتحقق كذلك من سجل AAAA بالأمر dig +short AAAA app.example.com، لأن سجلاً قديماً يشير إلى عنوان IPv6 لا يصل إلى Caddy قد يفشل التحقق.
too many certificates أو too many failed authorizations
معنى الرسالتين أنك تجاوزت حدود Let's Encrypt، وهذا يحدث غالباً عندما يضيع مجلد data مرة بعد مرة أو عندما تكرر محاولة فاشلة. ويعيد Caddy المحاولة بنفسه بفواصل تطول حتى يوم كامل بين المحاولة والأخرى ولمدة تصل إلى 30 يوماً، وأثناء إعادة المحاولة مع Let's Encrypt يتحول إلى بيئة Staging، ويجرب ZeroSSL إذا فشلت الأولى. لذلك لا تعد تشغيل ال Container على أمل أن ينجح الطلب، وإنما أصلح السبب أولاً، وجرب على بيئة Staging بالخيار acme_ca كما وصفنا في قسم التثبيت، وتأكد أن data مربوط بمجلد دائم.
لا يبدأ Caddy لأن المنفذ مستخدم
إذا ظهرت عند docker compose up -d رسالة مثل Bind for 0.0.0.0:80 failed: port is already allocated فهناك Container آخر ينشر المنفذ 80، وغالباً هو Nginx Proxy Manager أو Traefik، أما الرسالة address already in use فمعناها أن برنامجاً على السيرفر نفسه يستمع على المنفذ، مثل Nginx أو Apache أو Caddy مثبت من حزمة apt. واعرف صاحب المنفذ بالأمرين:
sudo ss -ltnp | grep -E ':(80|443) '
docker ps --filter publish=80 --filter publish=443والحل أن تختار Reverse Proxy واحداً للسيرفر وتنقل إليه كل النطاقات ثم توقف الآخر، وإذا كان البرنامج خدمة نظام لا تحتاج إليها فأوقفها وعطلها، مثلاً sudo systemctl disable --now nginx.
502 Bad Gateway
هذا يعني أن الطلب وصل إلى Caddy ولكن Caddy لم يصل إلى التطبيق، والسبب يظهر في سجل Caddy بمستوى error:
dial tcp: lookup api: i/o timeoutأوno such host: لا يجد Caddy الاسم، فإما أن ال Container ليس على الشبكةproxyوإما أن اسمه مختلف، فأضف الشبكة إلى خدمته معexternal: trueكما في مثال whoami، وتحقق من الاسم بالأمرdocker network inspect proxy.connect: connection refused: يجد Caddy ال Container ولكن المنفذ خاطئ، أو التطبيق يستمع على127.0.0.1داخل ال Container وليس على0.0.0.0، واكتب فيreverse_proxyالمنفذ داخل ال Container وليس منفذاً منشوراً على السيرفر.- إذا ظهرت
no upstreams availableمع توزيع الحمل فالفحص النشط استبعد كل النسخ، فتأكد أن المسار فيhealth_uriيعيد 200 فعلاً.
permission denied في البيانات أو السجلات
داخل ال Image الرسمي يعمل Caddy بالمستخدم root، فلا تظهر هذه المشكلة عادة، وإنما تظهر إذا أضفت user: إلى ملف Compose لتشغيله بمستخدم آخر، أو استعدت النسخة بأداة لا تحفظ الملكية، وعندها يجب أن يملك ذلك المستخدم المجلدات data وconfig وlogs وأن يقرأ conf. ويتحقق Caddy من أن التخزين قابل للكتابة قبل أي طلب ACME، لذلك تظهر الرسالة في السجل بعد الإقلاع مباشرة، وفي تثبيت apt تأكد أن المستخدم caddy يقرأ ال Caddyfile ويكتب في مجلد السجلات الذي تحدده.
الخلاصة
وصلنا لنهاية الموضوع، وأهم ما فيه:
- يكفي أن تكتب اسم النطاق في ال Caddyfile ليطلب Caddy شهادته ويجددها ويحول HTTP إلى HTTPS، فلا تحتاج إلى certbot ولا إلى مهام مجدولة قد تنساها.
- اربط
/dataبمجلد دائم وانسخه مشفراً خارج السيرفر، لأن ضياعه يعني طلب كل الشهادات من جديد والاصطدام بحدود Let's Encrypt. - اربط مجلد
confكاملاً وليس الملف وحده، وافحص الإعداد بcaddy validateثم طبقه بcaddy reloadدون انقطاع. - أبق ال Admin API على
127.0.0.1داخل ال Container ولا تنشر المنفذ 2019 أبداً. - ثبت رقم الإصدار كاملاً، وتخط 2.11.6، وانتقل إلى 2.11.7 عندما يظهر في ال Image الرسمي بعد قراءة ملاحظات الإصدار.
سجل التحديثات (Changelog)
- أكتوبر 2026: كتابة الدليل.