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

Traefik

Traefik یک پروکسی معکوس است که خودش گواهی‌های Let's Encrypt را دریافت می‌کند و پیکربندی را از فایل‌ها و برچسب‌های Docker می‌خواند. زمانی مناسب است که باید دسترسی را تفکیک کنید: پنل فقط برای آدرس‌های شما باز باشد و مسیر اشتراک /api/sub برای تمام اینترنت.

بخش ایستای پیکربندی، نقاط ورود، ACME و ارائه‌دهنده‌ها را توصیف می‌کند و بخش پویا — روترها، سرویس‌ها و middleware را. ارائه‌دهنده file فایل‌ها را در لحظه بازخوانی می‌کند، بنابراین تغییرات مسیریابی بدون راه‌اندازی مجدد کانتینر اعمال می‌شوند. ارائه‌دهنده docker تنها در صورتی لازم است که ترجیح می‌دهید مسیرها را با برچسب روی کانتینر پنل تعیین کنید.

در ادامه حالتی آمده است که Traefik مستقیماً جلوی پنل قرار دارد و خودش TLS را پایان می‌دهد.

1. چه چیزهایی لازم است​

  • دامنه panel.example.com که رکورد A آن به IP هاست اشاره می‌کند؛
  • پورت‌های باز 80 و 443 (پورت 80 برای ACME http-01 الزامی است)؛
  • پنل در حال اجرای xpanel در شبکه xpanel-network؛
  • صفحه اشتراک xpanel-subpage (پورت 3010)، اگر از آن استفاده می‌کنید.

دامنه sub.example.com در صورتی لازم است که صفحه اشتراک جدا از پنل ارائه شود. مقادیر دامنه‌ها باید با مقادیری که در .env پنل مشخص شده یکسان باشند: PANEL_DOMAIN، FRONT_END_DOMAIN و SUB_PUBLIC_DOMAIN.

پیش از شروع پیکربندی، DNS را بررسی کنید:

dig +short panel.example.com

دستور باید IP هاست شما را برگرداند. اگر پاسخ خالی بود، منتظر به‌روزرسانی زون نزد ثبت‌کننده بمانید — ACME نمی‌تواند دامنه را تأیید کند.

هشدار

اگر دامنه از طریق Cloudflare پروکسی می‌شود (ابر نارنجی)، http-01 عبور نخواهد کرد. در زمان نخستین صدور گواهی، رکورد را به حالت «فقط DNS» تغییر دهید.

2. پوشه و شبکه​

mkdir -p /opt/xlada/traefik/config /opt/xlada/traefik/letsencrypt /opt/xlada/traefik/logs
cd /opt/xlada/traefik
touch letsencrypt/acme.json
chmod 600 letsencrypt/acme.json

شبکه را یک بار بسازید و پنل را به آن متصل کنید:

docker network create xpanel-network
docker network connect xpanel-network xpanel
docker network connect xpanel-network xpanel-subpage
توجه

Traefik با پنل از طریق نام xpanel:3000 ارتباط برقرار می‌کند و از انتشار پورت 127.0.0.1:3000 استفاده نمی‌کند. فوروارد پورت‌های docker-compose.yml پنل را می‌توانید نگه دارید — برای عیب‌یابی محلی به کار می‌آید.

Traefik در همان شبکه bridge پنل کار می‌کند، بنابراین آن را با نام کانتینر می‌بیند. فقط پورت‌های 80 و 443 منتشر می‌شوند؛ پورت‌های 3000، 3010 و 3001 تنها روی خود هاست در دسترس می‌مانند.

اگر Traefik را به‌صورت پکیج نصب می‌کنید و نه کانتینر، پیکربندی ایستا را در /etc/traefik/traefik.yml و پیکربندی پویا را در /etc/traefik/config/ قرار دهید و آن را با unit سیستم‌دی راه‌اندازی کنید:

/etc/systemd/system/traefik.service
[Unit]
Description=Traefik
After=network-online.target
Wants=network-online.target

[Service]
Restart=on-failure
ExecStart=/usr/local/bin/traefik --configFile=/etc/traefik/traefik.yml

[Install]
WantedBy=multi-user.target
systemctl daemon-reload && systemctl enable --now traefik

3. docker-compose برای Traefik​

کانتینر به 80 و 443 گوش می‌دهد، سوکت Docker را فقط‌خواندنی می‌خواند، گواهی‌ها را در پوشه letsencrypt نگه می‌دارد و لاگ‌ها را در logs می‌نویسد. پوشه logs را از قبل بسازید، وگرنه Docker آن را به نام root می‌سازد.

/opt/xlada/traefik/docker-compose.yml
services:
traefik:
image: traefik:v3.5
container_name: traefik
restart: unless-stopped
security_opt:
- no-new-privileges:true
networks:
- xpanel-network
ports:
- '0.0.0.0:80:80'
- '0.0.0.0:443:443'
environment:
- TZ=UTC
volumes:
# برای خواندن برچسب‌های کانتینرها لازم است
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./traefik.yml:/traefik.yml:ro
- ./config:/config:ro
- ./letsencrypt:/letsencrypt
- ./logs:/var/log/traefik

networks:
xpanel-network:
name: xpanel-network
external: true

4. پیکربندی ایستا​

فایل traefik.yml یک بار هنگام شروع کانتینر خوانده می‌شود. اگر نقاط ورود یا ACME را تغییر می‌دهید — Traefik را دوباره راه‌اندازی کنید. در اینجا خود نقطه ورود web با ریدایرکت به websecure، پروکسی‌های مورد اعتماد، ارائه‌دهنده‌ها و پارامترهای گواهی توصیف شده‌اند.

/opt/xlada/traefik/traefik.yml
api:
dashboard: true
# فقط به loopback داخل کانتینر گوش می‌دهیم: داشبورد به بیرون نگاه نمی‌کند
insecure: false

entryPoints:
web:
address: ':80'
http:
redirections:
entryPoint:
to: websecure
scheme: https
websecure:
address: ':443'
# به هدرهای X-Forwarded-* فقط از این منابع اعتماد می‌کنیم.
# بدون این فهرست، کلاینت می‌تواند X-Forwarded-For را جعل کند و ipAllowList را دور بزند.
forwardedHeaders:
trustedIPs:
- '172.16.0.0/12'
- '173.245.48.0/20'
- '103.21.244.0/22'
- '104.16.0.0/13'
- '172.64.0.0/13'

serversTransport:
# ترافیک داخلی تا پنل از HTTP می‌گذرد، بررسی گواهی لازم نیست
insecureSkipVerify: true

providers:
docker:
exposedByDefault: false
network: xpanel-network
file:
directory: /config
watch: true

certificatesResolvers:
letsencrypt:
acme:
email: you@example.com
storage: /letsencrypt/acme.json
httpChallenge:
entryPoint: web

log:
level: INFO
filePath: /var/log/traefik/traefik.log

accessLog:
filePath: /var/log/traefik/access.log

5. پیکربندی پویا​

این فایل روترهای پنل و صفحه اشتراک را توصیف می‌کند. همچنین هدرهای محافظتی، فهرست مجاز IP و آدرس سرویس‌های upstream در همین‌جا تعیین شده‌اند. قاعده Host باید دقیقاً با دامنه‌ای که گواهی برای آن صادر می‌شود یکسان باشد.

/opt/xlada/traefik/config/xlada.yml
http:
routers:
# صفحه اشتراک — ترافیک عمومی بدون محدودیت
xlada-sub:
rule: 'Host(`sub.example.com`)'
entryPoints:
- websecure
service: subpage
tls:
certResolver: letsencrypt

# فایل‌های ایستا بیلد: کش طولانی بر اساس هش در نام فایل
xlada-assets:
rule: 'Host(`panel.example.com`) && PathPrefix(`/assets/`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
- xlada-static-cache
service: panel
tls:
certResolver: letsencrypt
priority: 100

# پنل: فقط IP شما، به‌همراه هدرهای محافظتی
xlada-panel:
rule: 'Host(`panel.example.com`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
- xlada-ip-allow
service: panel
tls:
certResolver: letsencrypt

middlewares:
xlada-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: strict-origin-when-cross-origin
# ترافیک روی websecure می‌آید، بنابراین Traefik خودش
# X-Forwarded-Proto: https را تنظیم می‌کند — پنل چنین درخواستی را می‌پذیرد

xlada-static-cache:
headers:
customResponseHeaders:
Cache-Control: 'public, max-age=31536000, immutable'

xlada-ip-allow:
ipAllowList:
sourceRange:
- '203.0.113.10/32'
- '198.51.100.0/24'
ipStrategy:
# depth = 1 یعنی: در زنجیره X-Forwarded-For آدرسی را
# یک موقعیت دورتر از پروکسی مورد اعتماد برمی‌داریم و آن را آدرس کلاینت می‌دانیم
depth: 1

services:
panel:
loadBalancer:
servers:
- url: 'http://xpanel:3000'
subpage:
loadBalancer:
servers:
- url: 'http://xpanel-subpage:3010'

6. اجرا​

cd /opt/xlada/traefik
docker compose up -d
docker compose logs -f

گواهی در letsencrypt/acme.json ظاهر می‌شود. فایل باید بسته باقی بماند: chmod 600. اگر در لاگ‌ها خطای ACME دیده می‌شود، بررسی کنید که پورت 80 از بیرون در دسترس باشد و رکورد DNS هنوز از طریق CDN پروکسی نشود.

7. مسیر عمومی اشتراک​

در پیکربندی بالا، پنل به‌طور کامل با فهرست IP بسته شده است و صفحه اشتراک روی یک دامنه جداگانه باز است. اگر صفحه اشتراک مستقر نشده است، باید تنها مسیر /api/sub/ روی دامنه پنل عمومی بماند: این آدرس در هدر profile-web-page-url قرار می‌گیرد و کلاینت‌ها اشتراک را از آن دانلود می‌کنند.

برای این کار روی یک دامنه دو روتر تعریف می‌کنند: یکی برای /api/sub/ بدون محدودیت و دومی برای بقیه موارد — همراه با فهرست IP. Traefik روتری را انتخاب می‌کند که پیشوند منطبق طولانی‌تری دارد، بنابراین ترتیب تعریف مهم نیست، اما قاعده‌ها باید فقط در همین مسیر هم‌پوشانی داشته باشند.

/opt/xlada/traefik/config/xlada.yml
http:
routers:
# صفحه اشتراک روی دامنه جداگانه — ترافیک عمومی
xlada-sub:
rule: 'Host(`sub.example.com`)'
entryPoints:
- websecure
service: subpage
tls:
certResolver: letsencrypt

# مسیر عمومی اشتراک روی دامنه پنل
xlada-sub-path:
rule: 'Host(`panel.example.com`) && PathPrefix(`/api/sub/`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
service: panel
tls:
certResolver: letsencrypt
priority: 110

# فایل‌های ایستا بیلد: کش طولانی بر اساس هش در نام فایل
xlada-assets:
rule: 'Host(`panel.example.com`) && PathPrefix(`/assets/`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
- xlada-static-cache
service: panel
tls:
certResolver: letsencrypt
priority: 100

# پنل: فقط IP شما، به‌همراه هدرهای محافظتی
xlada-panel:
rule: 'Host(`panel.example.com`)'
entryPoints:
- websecure
middlewares:
- xlada-headers
- xlada-ip-allow
service: panel
tls:
certResolver: letsencrypt

middlewares:
xlada-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: strict-origin-when-cross-origin

xlada-static-cache:
headers:
customResponseHeaders:
Cache-Control: 'public, max-age=31536000, immutable'

xlada-ip-allow:
ipAllowList:
sourceRange:
- '203.0.113.10/32'
- '198.51.100.0/24'
ipStrategy:
depth: 1

services:
panel:
loadBalancer:
servers:
- url: 'http://xpanel:3000'
subpage:
loadBalancer:
servers:
- url: 'http://xpanel-subpage:3010'

پس از ویرایش، هر دو سناریو را بررسی کنید: درخواست به /api/sub/AbCdEf1234567890 باید پاسخی بدون 403 برگرداند و ریشه پنل باید از آدرس بیگانه غیرقابل دسترس بماند. به یاد داشته باشید که مقدار SUB_PUBLIC_DOMAIN در .env پنل باید با طرح انتخاب‌شده یکسان باشد: یا sub.example.com یا panel.example.com/api/sub.

8. برچسب‌های کانتینر پنل​

همین مسیریابی را می‌توان به‌جای فایل با برچسب‌ها توصیف کرد. در این حالت در xlada.yml فقط middlewareها می‌مانند و Traefik روترها و سرویس‌ها را از برچسب‌های کانتینرها می‌خواند. در ادامه هر دو فایل به‌طور کامل آمده است.

/opt/xlada/traefik/config/xlada.yml
http:
middlewares:
xlada-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
contentTypeNosniff: true
frameDeny: true
referrerPolicy: strict-origin-when-cross-origin

xlada-static-cache:
headers:
customResponseHeaders:
Cache-Control: 'public, max-age=31536000, immutable'

xlada-ip-allow:
ipAllowList:
sourceRange:
- '203.0.113.10/32'
- '198.51.100.0/24'
ipStrategy:
depth: 1
/opt/xlada/docker-compose.yml
services:
xpanel-db:
image: postgres:18.4
container_name: xpanel-db
restart: unless-stopped
env_file: .env
environment:
- POSTGRES_USER=${POSTGRES_USER}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=${POSTGRES_DB}
- TZ=UTC
volumes:
- xpanel-db-data:/var/lib/postgresql
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}']
interval: 5s
timeout: 5s
retries: 10
networks:
- xpanel-network

xpanel-redis:
image: valkey/valkey:9-alpine
container_name: xpanel-redis
restart: unless-stopped
command: >
valkey-server --save "" --appendonly no
--maxmemory-policy noeviction --loglevel warning
healthcheck:
test: ['CMD', 'valkey-cli', 'ping']
interval: 5s
timeout: 3s
retries: 10
networks:
- xpanel-network

xpanel:
image: ghcr.io/xray-panel/backend:latest
container_name: xpanel
restart: unless-stopped
env_file: .env
environment:
- REDIS_HOST=xpanel-redis
- REDIS_PORT=6379
- REDIS_DB=0
depends_on:
xpanel-db:
condition: service_healthy
xpanel-redis:
condition: service_healthy
networks:
- xpanel-network
labels:
- 'traefik.enable=true'
- 'traefik.docker.network=xpanel-network'

# مسیر عمومی اشتراک
- 'traefik.http.routers.xlada-sub.rule=Host(`panel.example.com`) && PathPrefix(`/api/sub/`)'
- 'traefik.http.routers.xlada-sub.entrypoints=websecure'
- 'traefik.http.routers.xlada-sub.tls.certresolver=letsencrypt'
- 'traefik.http.routers.xlada-sub.middlewares=xlada-headers@file'
- 'traefik.http.routers.xlada-sub.service=xlada'
- 'traefik.http.routers.xlada-sub.priority=110'

# فایل‌های ایستا بیلد با کش طولانی
- 'traefik.http.routers.xlada-assets.rule=Host(`panel.example.com`) && PathPrefix(`/assets/`)'
- 'traefik.http.routers.xlada-assets.entrypoints=websecure'
- 'traefik.http.routers.xlada-assets.tls.certresolver=letsencrypt'
- 'traefik.http.routers.xlada-assets.middlewares=xlada-headers@file,xlada-static-cache@file'
- 'traefik.http.routers.xlada-assets.service=xlada'
- 'traefik.http.routers.xlada-assets.priority=100'

# پنل: فقط IP شما
- 'traefik.http.routers.xlada-panel.rule=Host(`panel.example.com`)'
- 'traefik.http.routers.xlada-panel.entrypoints=websecure'
- 'traefik.http.routers.xlada-panel.tls.certresolver=letsencrypt'
- 'traefik.http.routers.xlada-panel.middlewares=xlada-headers@file,xlada-ip-allow@file'
- 'traefik.http.routers.xlada-panel.service=xlada'

- 'traefik.http.services.xlada.loadbalancer.server.port=3000'

xpanel-subpage:
image: ghcr.io/xray-panel/subscription-page:latest
container_name: xpanel-subpage
restart: unless-stopped
env_file: .env.subpage
depends_on:
- xpanel
networks:
- xpanel-network
labels:
- 'traefik.enable=true'
- 'traefik.docker.network=xpanel-network'
- 'traefik.http.routers.xlada-subdomain.rule=Host(`sub.example.com`)'
- 'traefik.http.routers.xlada-subdomain.entrypoints=websecure'
- 'traefik.http.routers.xlada-subdomain.tls.certresolver=letsencrypt'
- 'traefik.http.routers.xlada-subdomain.middlewares=xlada-headers@file'
- 'traefik.http.routers.xlada-subdomain.service=xlada-subpage'
- 'traefik.http.services.xlada-subpage.loadbalancer.server.port=3010'

networks:
xpanel-network:
name: xpanel-network
external: true

volumes:
xpanel-db-data:
name: xpanel-db-data

هم‌زمان از فایل و برچسب برای یک روتر استفاده نکنید — باعث تضاد تعریف‌ها می‌شود. در این حالت روترها و سرویس‌ها فقط با برچسب‌ها توصیف شده‌اند و فایل xlada.yml تنها شامل middlewareها است.

9. IP واقعی کاربر​

IP درست برای محدودیت‌های HWID، حسابرسی و محافظت از ورود در برابر حمله جست‌وجو لازم است. Traefik مقدار X-Forwarded-For را قرار می‌دهد، اما به هدر ورودی فقط از آدرس‌های موجود در forwardedHeaders.trustedIPs اعتماد می‌کند.

زنجیره را با یک مثال مشخص بررسی کنید. کلاینت A از طریق پروکسی B به Traefik T می‌رود:

X-Forwarded-For: A, B
  • depth: 1 — آدرس یک موقعیت دورتر از پروکسی مورد اعتماد را برمی‌داریم (در مثال این A است)، یعنی IP واقعی کاربر؛
  • excludedIPs — ابتدا آدرس‌های فهرست‌شده را از زنجیره حذف می‌کند؛ این روش زمانی به کار می‌رود که Cloudflare جلوی Traefik قرار دارد: در فهرست محدوده‌های Cloudflare ذکر می‌شوند و Traefik کلاینت را می‌بیند، نه آدرس CDN را.

اگر پنل همیشه یک آدرس یکسان را می‌بیند، بررسی کنید:

  • محدوده شبکه Docker و آدرس‌های Cloudflare به trustedIPs اضافه شده باشند؛
  • در ipStrategy مقدار درست depth یا فهرست excludedIPs مشخص شده باشد؛
  • هدر توسط پروکسی میانی بازنویسی نشود.

اگر Cloudflare جلوی Traefik قرار دارد، علاوه بر این اعتماد به CF-Connecting-IP را همان‌گونه که در صفحه Cloudflare Tunnel توصیف شده تنظیم کنید.

10. بررسی و عیب‌یابی​

# بررسی گواهی و ریدایرکت
curl -sI http://panel.example.com | head -3
curl -sI https://panel.example.com | head -3

# مسیر عمومی اشتراک باید بدون محدودیت IP پاسخ دهد
curl -s -o /dev/null -w '%{http_code}\n' https://sub.example.com/

# ببینیم Traefik چه IP‌ای می‌بیند
tail -n 20 /opt/xlada/traefik/logs/access.log

docker compose logs traefik | tail -50

داشبورد Traefik فعال است، اما به بیرون منتشر نشده است. می‌توانید این‌گونه به آن دسترسی پیدا کنید. دیمن socat پورت loopback کانتینر را به هاست منتقل می‌کند و سپس تونل معمولی SSH کار می‌کند:

# نمایش پورت 8080 کانتینر traefik روی localhost هاست
socat TCP-LISTEN:8080,fork,reuseaddr \
TCP:$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' traefik):8080

# روی ماشین خودتان
ssh -L 8080:127.0.0.1:8080 user@panel.example.com

پس از آن http://127.0.0.1:8080/dashboard/ را در مرورگر باز کنید. اگر داشبورد را همیشه لازم دارید، امن‌تر است آن را با یک روتر جداگانه همراه با ipAllowList و basicAuth ببندید تا اینکه بدون محافظت منتشر کنید.

نکته

به‌جای فهرست سخت‌گیرانه IP می‌توانید پنل را از طریق Cloudflare Zero Trust یا VPN ببندید. فهرست آدرس‌ها تنها با IP ایستا برای مدیر قابل اتکا است.

قدم بعدی​