پرش به مطلب اصلی

Caddy

مرور کلی​

Caddy یک پروکسی معکوس است که خودش گواهی‌های Let's Encrypt را دریافت و تمدید می‌کند: نیازی به کلاینت ACME جداگانه، cron و بارگذاری مجدد دستی نیست. پنل XLADA و صفحه اشتراک فقط پشت پروکسی با HTTPS کار می‌کنند — میان‌افزار آن‌ها اگر درخواست بدون هدرهای X-Forwarded-For و X-Forwarded-Proto: https برسد، اتصال را قطع می‌کند.

Caddy خودش X-Forwarded-* را تنظیم می‌کند. در پیکربندی زیر با این حال آن‌ها را به‌صورت صریح تعیین می‌کنیم: این‌گونه اگر CDN یا لود بالانسر جلوی Caddy قرار بگیرد رفتار تغییر نمی‌کند و X-Forwarded-Proto به http تبدیل نمی‌شود.

در مثال‌ها دامنه‌های panel.example.com و sub.example.com، آپ‌استریم‌های 127.0.0.1:3000 (پنل) و 127.0.0.1:3010 (صفحه اشتراک) — کانتینرهای xpanel و xpanel-subpage دقیقاً همین پورت‌ها را روی loopback میزبان منتشر می‌کنند.

هشدار

ادامه تنها با یک دامنه ثبت‌شده ممکن است: بدون آن Let's Encrypt گواهی صادر نمی‌کند.

DNS​

آدرس IP سرور را بررسی کنید (بهتر است استاتیک باشد) و رکوردها را نزد ارائه‌دهنده DNS یا ثبت‌کننده ایجاد کنید:

نوعناممقدار
Apanelآدرس IPv4 سرور
Asubآدرس IPv4 سرور
AAAApanelآدرس IPv6 سرور
AAAAsubآدرس IPv6 سرور

در Cloudflare رکوردها در بخش DNS → Records → Create record اضافه می‌شوند: نوع A، در فیلد نام panel یا sub و در فیلد آدرس IP سرور.

  • ابر خاکستری — ترافیک مستقیم به سرور شما می‌رود و Caddy IP واقعی کاربر را می‌بیند. اگر Cloudflare در دسترس نیست یا مزاحمت ایجاد می‌کند این حالت را انتخاب کنید.
  • ابر نارنجی — ترافیک از شبکه Cloudflare عبور می‌کند: IP سرور پنهان است و محافظت در برابر حملات فعال است. در اینجا حتماً محدوده‌های Cloudflare را در trusted_proxies static در Caddyfile مشخص کنید، وگرنه همه کلاینت‌ها به‌صورت یک آدرس Cloudflare دیده می‌شوند و محدودیت‌های HWID، ممیزی و محافظت از ورود در برابر حمله جست‌وجوی فراگیر بی‌معنی می‌شوند.

منتظر به‌روزرسانی رکوردها بمانید: dig +short panel.example.com باید آدرس سرور و در حالت ابر نارنجی آدرس‌های Cloudflare را برگرداند.

گواهی‌ها​

Caddy به‌محض اینکه دامنه در Caddyfile مشخص شود، گواهی را به‌صورت خودکار دریافت می‌کند. برای احراز دامنه، پورت‌های 80 (HTTP-01) و 443 (TLS-ALPN-01) باید باز باشند؛ تمدید نیز خودکار است — حدود ۳۰ روز پیش از انقضا. در بلوک global کافی است email را مشخص کنید: اعلان‌های مربوط به مشکلات گواهی به این آدرس می‌رسند.

Caddyfile کامل​

دایرکتوری و فایل را بسازید:

sudo mkdir -p /opt/caddy && cd /opt/caddy
sudo nano Caddyfile
/opt/caddy/Caddyfile
# ─── پارامترهای global ────────────────────────────────────────────
{
# ایمیل برای اعلان‌های Let's Encrypt
email admin@example.com

# IP واقعی کاربر پشت Cloudflare. محدوده‌ها را Cloudflare منتشر می‌کند؛
# بهتر است فهرست آن‌ها را به‌صورت دوره‌ای با نسخه به‌روز بررسی کنید.
servers {
trusted_proxies static \
173.245.48.0/20 103.21.244.0/22 103.22.200.0/22 103.31.4.0/22 \
141.101.64.0/18 108.162.192.0/18 190.93.240.0/20 188.114.96.0/20 \
197.234.240.0/22 198.41.128.0/17 162.158.0.0/15 104.16.0.0/13 \
104.24.0.0/14 172.64.0.0/13 131.0.72.0/22 2400:cb00::/32 \
2606:4700::/32 2803:f800::/32 2405:b500::/32 2405:8100::/32 \
2a06:98c0::/29 2c0f:f248::/32
client_ip_headers CF-Connecting-IP X-Forwarded-For
}
}

# ─── پنل XLADA ────────────────────────────────────────────────────
panel.example.com {
encode zstd gzip

# Security headers
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}

# استاتیک بیلد: هش در نام فایل امکان کش طولانی‌مدت آن را می‌دهد
@assets path /assets/*
header @assets Cache-Control "public, max-age=31536000, immutable"

reverse_proxy 127.0.0.1:3000 {
# پنل بدون این دو هدر اتصال را قطع می‌کند.
# X-Forwarded-For را بازنویسی می‌کنیم تا کلاینت IP خود را جعل نکند.
header_up X-Forwarded-For {client_ip}
header_up X-Forwarded-Proto https
header_up X-Real-IP {client_ip}
header_up Host {host}

# WebSocket: Caddy خودش Upgrade و Connection را مدیریت می‌کند،
# هدرهای جداگانه لازم نیستند. flush_interval -1
# بافرینگ را غیرفعال می‌کند — این برای SSE و استریم آمار مهم است.
flush_interval -1
}
}

# ─── صفحه اشتراک ───────────────────────────────────────────────
sub.example.com {
encode zstd gzip

header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "strict-origin-when-cross-origin"
-Server
}

# استاتیک بیلد: هش در نام فایل امکان کش طولانی‌مدت آن را می‌دهد
@assets path /assets/*
header @assets Cache-Control "public, max-age=31536000, immutable"

reverse_proxy 127.0.0.1:3010 {
header_up X-Forwarded-For {client_ip}
header_up X-Forwarded-Proto https
header_up X-Real-IP {client_ip}
header_up Host {host}
flush_interval -1
}
}

# ─── درخواست‌ها بر اساس IP و به دامنه‌های ناشناخته ───────────────────────────
:443 {
tls internal
respond 204
}

دایرکتیوهای کلیدی:

دایرکتیوکاربرد
trusted_proxies staticاعتماد به هدرهای Cloudflare از این زیرشبکه‌ها
client_ip_headersگرفتن IP واقعی از CF-Connecting-IP
header_up X-Forwarded-Forانتقال IP واقعی کاربر به پنل
header_up X-Forwarded-Proto httpsپنل فقط https را می‌پذیرد
flush_interval -1ارسال استریمی بدون بافرینگ
@assets و headerکش طولانی برای استاتیک بیلد از /assets/
:443 با tls internalاسکنرهایی را که با IP در می‌زنند قطع می‌کند
هشدار

روی دامنه sub.example.com احراز هویت HTTP اضافی اضافه نکنید: صفحه اشتراک را اپلیکیشن‌های کلاینت درخواست می‌کنند که نمی‌توانند نام کاربری و رمز عبور وارد کنند. از ورود به پنل با احراز هویت خود پنل و TOTP محافظت کنید.

اجرا در Docker​

/opt/caddy/docker-compose.yml
services:
caddy:
image: caddy:2.9
container_name: caddy
restart: unless-stopped
# حالت host لازم است تا Caddy بتواند 127.0.0.1:3000 و 127.0.0.1:3010 را ببیند
network_mode: host
environment:
- TZ=UTC
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./data:/data # گواهی‌ها و کلیدهای ACME
- ./config:/config # وضعیت داخلی Caddy
cd /opt/caddy
docker compose up -d
docker compose logs -f -t

حالت Host عمداً انتخاب شده است: فقط به این ترتیب کانتینر Caddy پورت‌های loopback پنل و صفحه اشتراک را می‌بیند و گواهی‌ها روی میزبان باقی می‌مانند. پورت‌های 80 و 443 را خود Caddy گوش می‌دهد و 3000، 3010 و 3001 به بیرون باز نمی‌شوند.

بررسی و بارگذاری مجدد​

# بررسی سینتکس بدون راه‌اندازی مجدد
caddy validate --config /opt/caddy/Caddyfile

# مرتب‌سازی فایل به شکل متعارف
caddy fmt --overwrite /opt/caddy/Caddyfile

# بارگذاری مجدد نرم: اتصال‌ها قطع نمی‌شوند
systemctl reload caddy

# در Docker همان دستورها داخل کانتینر اجرا می‌شوند
docker compose exec caddy caddy validate --config /etc/caddy/Caddyfile
docker compose exec caddy caddy reload --config /etc/caddy/Caddyfile

در لاگ‌ها به چه چیزهایی توجه کنیم​

journalctl -u caddy -f # نصب روی میزبان
docker compose logs -f caddy # اجرا در Docker
  • certificate obtained successfully — گواهی صادر شد؛
  • serving initial configuration — پیکربندی پذیرفته شد؛
  • tls.obtain یا acme — DNS، پورت‌های 80 و 443 و محدودیت‌های Let's Encrypt را بررسی کنید؛
  • dial tcp 127.0.0.1:3000: connect: connection refused — پنل اجرا نشده است.

مطمئن شوید که هر دو سایت پاسخ می‌دهند:

curl -I https://panel.example.com
curl -I https://sub.example.com

پاسخ باید با HTTP/2 200 شروع شود. بررسی IP واقعی کاربر در ممیزی پنل راحت است: اگر آنجا آدرس‌های Cloudflare باشد، یعنی trusted_proxies فعال نشده است.

گام‌های بعدی​