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 سیستمدی راهاندازی کنید:
[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 میسازد.
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، پروکسیهای مورد اعتماد، ارائهدهندهها و
پارامترهای گواهی توصیف شدهاند.
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 باید
دقیقاً با دامنهای که گواهی برای آن صادر میشود یکسان باشد.
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 روتری را انتخاب میکند
که پیشوند منطبق طولانیتری دارد، بنابراین ترتیب تعریف مهم نیست، اما قاعدهها
باید فقط در همین مسیر همپوشانی داشته باشند.
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 روترها و سرویسها را از
برچسبهای کانتینرها میخواند. در ادامه هر دو فایل بهطور کامل آمده است.
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
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 ایستا برای مدیر قابل اتکا است.