اجرای 5 اپلیکیشن با Traefik v3 و Docker Compose
آموزش راهاندازی 5 اپلیکیشن روی یک IP با Traefik v3. یادگیری تنظیمات Host rule و رفع خطای acme.json برای دریافت خودکار گواهینامه Let's Encrypt.
یک IP، پنج اپلیکیشن، یک پورت 443
VPS شما تنها یک آدرس IPv4 عمومی و یک پورت TCP 443 دارد. شما میخواهید Gitea، یک نسخه staging از اپلیکیشن خود، یک داشبورد داخلی، یک صفحه وضعیت و یک webhook receiver را روی آن اجرا کنید؛ یعنی پنج hostname روی یک سرور. یک reverse proxy فرآیندی است که پورتهای :80 و :443 را مدیریت میکند، هدر Host را در هر درخواست میخواند و آن را به container مربوطه میسپارد. Traefik این کار را انجام میدهد و برای هر hostname گواهینامه (certificate) دریافت و تمدید میکند، بدون اینکه نیاز باشد شما certbot را به صورت دستی اجرا کنید.
تفاوت Traefik با یک nginx server {} block در منبع پیکربندی آن است. در nginx شما یک فایل را ویرایش و reload میکنید و چرخه حیات certificate یک کار مجزا باقی میماند؛ این همان گردش کاری است که هنگام issue Let's Encrypt certificates with certbot on nginx دنبال میکنید، جایی که timer تمدید کاملاً خارج از وبسرور قرار دارد. Docker provider در Traefik جریان رویدادهای Docker را زیر نظر دارد و labels را از containerهای شما میخواند: یک container را با یک label قانون Host() اجرا کنید تا در کمتر از یک ثانیه قابل مسیریابی (routable) شود؛ آن را متوقف کنید تا مسیر (route) حذف شود. این موضوع یک چالش نیز هست. پیکربندی که در labels قرار دارد، همزمان در پنج نقطه وجود دارد و یک label اشتباه باعث خطای سیستمی نمیشود؛ فقط container مسیریابی نمیشود و Traefik هیچ پیامی نمیدهد.
چهار اسم اصلی
- Entrypoints سوکتهای شنود (listening sockets) هستند. شما دو مورد تعریف خواهید کرد:
webروی:80وwebsecureروی:443. - Routers یک درخواست (
Host(...)) را مطابقت داده و آن را به یک سرویس متصل میکنند. گواهینامهها (Certificates) برای هر router و از طریقtls.certresolverدرخواست میشوند. - Services بخش backend هستند؛ یعنی یک container و پورت آن که داخل شبکه Docker به آن گوش میدهد.
- Middlewares بین router و service قرار میگیرند: شامل مواردی مانند basic auth، لیستهای مجاز IP، بازنویسی headerها و redirectها.
تنظیمات استاتیک (entrypoints, providers, ACME) از طریق command line مربوط به Traefik یا در traefik.yml ارسال میشوند و تغییر آنها مستلزم restart کردن Traefik است. تنظیمات دینامیک (routers, services, middlewares) از طریق container labels دریافت میشوند و به صورت hot-reload بارگذاری میگردند. اشتباه گرفتن این دو مورد، دلیل معمول خطای "my flag does nothing" است.
فایل compose
یک شبکه مشترک Docker به نام proxy هسته اصلی است. Traefik تنها زمانی به یک کانتینر دسترسی دارد که هر دو در آن شبکه حضور داشته باشند.
name: edge
networks:
proxy:
name: proxy
services:
traefik:
image: traefik:v3.5
restart: unless-stopped
command:
- --providers.docker=true
- --providers.docker.exposedByDefault=false
- --providers.docker.network=proxy
- --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443
- --entryPoints.web.http.redirections.entryPoint.to=websecure
- --entryPoints.web.http.redirections.entryPoint.scheme=https
- --certificatesresolvers.le.acme.email=you@example.com
- --certificatesresolvers.le.acme.storage=/letsencrypt/acme.json
- --certificatesresolvers.le.acme.tlschallenge=true
# while you iterate, point at staging so a mistake costs nothing:
# - --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
- --api.dashboard=true
- --log.level=INFO
- --accesslog=true
ports:
- "80:80"
- "443:443"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./letsencrypt:/letsencrypt
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.dashboard.rule=Host(`traefik.example.com`)
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=le
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.middlewares=dashboard-auth
- traefik.http.middlewares.dashboard-auth.basicauth.users=admin:$$apr1$$REPLACE$$THIS
gitea:
image: gitea/gitea:1 # major-only pin keeps this demo copy-pasteable; pin an exact release in production
restart: unless-stopped
volumes:
- ./gitea:/data
networks:
- proxy
labels:
- traefik.enable=true
- traefik.http.routers.gitea.rule=Host(`git.example.com`)
- traefik.http.routers.gitea.entrypoints=websecure
- traefik.http.routers.gitea.tls.certresolver=le
- traefik.http.services.gitea.loadbalancer.server.port=3000ابتدا docker compose up -d و سپس docker compose logs -f traefik. هر اپلیکیشن اضافی، کپیای از بلوک gitea است که نام router، Host() و پورت داخلی مخصوص به خود را دارد. یک نصب Nextcloud در Docker با TLS و بکآپ نیز به همین صورت اضافه میشود؛ پورتهای منتشر شده (published ports) را حذف کنید، آن را به proxy متصل کنید و اجازه دهید برچسبهای (labels) router، نام میزبان (hostname) و گواهینامه (certificate) را مدیریت کنند.
پنج نکته در اینجا اهمیت دارند.
exposedByDefault=false باعث میشود کانتینر برای Traefik نامرئی بماند تا زمانی که دارای traefik.enable=true باشد. اگر این مورد را حذف کنید، برای هر کانتینری که اجرا میکنید — از جمله کانتینرهای موقتی مثل postgres که برای بررسی یک مورد اجرا شدهاند — یک مسیر (route) ساخته میشود.
providers.docker.network=proxy به Traefik میگوید وقتی یک کانتینر به چندین شبکه متصل است، از کدام شبکه استفاده کند. اگر این مورد را نادیده بگیرید، Traefik ممکن است IP اشتباه کانتینر را انتخاب کند؛ این اتفاق منجر به خطای 502 میشود که شبیه به خطای اپلیکیشن به نظر میرسد.
loadbalancer.server.port=3000 پورت داخل کانتینر است؛ برای مثال Gitea در آنجا روی پورت 3000 گوش میدهد. دقت کنید که هیچ کانتینر اپلیکیشنی پورت را منتشر (publish) نمیکند — این کار فقط توسط Traefik انجام میشود.
بازنشانی (redirect) در entrypoint مربوط به web، درخواستهای Plaintext را به یک 308 به HTTPS تبدیل میکند. پورت 80 همچنان باز میماند: ACME HTTP challenge به آن نیاز دارد و همچنین کاربرانی که فقط نام میزبان را تایپ میکنند.
تکرار شدن $$ در هش basic-auth، مربوط به فرآیند escaping در Compose است و نه یک غلط تایپی. آن را با استفاده از htpasswd -nbB admin 'your-password' (بسته apache2-utils) تولید کنید، سپس هر $ را دو برابر کنید.
گواهینامه و تله acme.json
tlschallenge=true از پروتکل TLS-ALPN-01 استفاده میکند: Let's Encrypt روی پورت 443 به سرور شما متصل میشود و Traefik پاسخ چالش را در داخل TLS handshake ارسال میکند. جایگزین دیگر HTTP-01 روی پورت 80 است؛ برای این کار خط tlschallenge را در لیست command: در Traefik با این دو خط جایگزین کنید:
- --certificatesresolvers.le.acme.httpchallenge=true
- --certificatesresolvers.le.acme.httpchallenge.entrypoint=webهر دو روش کار میکنند. در هر دو حالت، DNS عمومی برای آن hostname باید از قبل به VPS شما اشاره کند؛ مرجع گواهینامه (CA) نام را Resolve کرده و از بیرون متصل میشود. ابتدا رکورد A (و AAAA) را بسازید، با استفاده از dig +short git.example.com صحت آن را تایید کنید و سپس Traefik را اجرا کنید.
حالا با تلهای آشنا شوید که باعث هدر رفتن زمان کاربران میشود. Traefik کلید حساب ACME و تمام گواهینامههای صادر شده را در یک فایل acme.json نگه میدارد. اگر این فایل برای گروه یا سایر کاربران قابل خواندن (readable) باشد، Traefik خط زیر را چاپ کرده و متوقف میشود:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600راه حل صحیح، روش بالا است: directory را به صورت bind-mount متصل کنید و اجازه دهید خود Traefik فایل را با mode مناسب ایجاد کند. اگر acme.json را با استفاده از touch ساختهاید، umask آن را روی 644 قرار داده است. آن را در host اصلاح کنید:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikاز آن directory با استفاده از volumeهای اپلیکیشن خود پشتیبان (backup) تهیه کنید. از دست دادن آن قابل جبران است (گواهینامهها دوباره صادر میشوند)، اما صدور مجدد 5 hostname به صورت همزمان، شما را با محدودیتهای نرخ (rate limits) مواجه میکند.
هنگام تست و اصلاح، از staging CA استفاده کنید. خط caserver را از حالت کامنت خارج کنید، تمام مسیرها را راه اندازی کنید، سپس آن را کامنت کرده و acme.json را حذف کنید تا گواهینامههای اصلی (production) مجدداً درخواست شوند. Let's Encrypt در حالت production اجازه صدور 5 گواهینامه تکراری را در هفته برای یک مجموعه یکسان از hostnameها میدهد و در صورت تکرار اعتبارسنجیهای ناموفق برای یک نام مشخص، محدودیت اعمال میکند. حالت staging گواهینامههای غیرقابل اعتماد صادر میکند (مرورگر شما هشدار میدهد و همین هشدار نشاندهنده موفقیت عملیات است) و محدودیتهای بسیار منعطفتری دارد.
داشبورد یک سطح کنترلی است، نه یک نسخه دموی آزمایشی
بسیاری از راهنماهای سریع، --api.insecure=true را تنظیم میکنند که داشبورد را روی پورت 8080 و بدون احراز هویت ارائه میدهد. در سیستمی با IP عمومی، این کار باعث میشود توپولوژی مسیریابی، نامهای میزبان (hostnames)، نامهای middleware و پورتهای backend به هر کسی که سیستم را اسکن میکند، نمایش داده شود.
برچسبهای (labels) سرویس traefik در بالا، راه حل جایگزین هستند: داشبورد مانند هر اپلیکیشن دیگری، روی یک hostname واقعی، از طریق TLS و پشت سر basicauth مسیریابی میشود. service=api@internal همان چیزی است که روتر را به API داخلی Traefik متصل میکند. با زنجیره کردن یک لیست مجاز IP (IP allow-list) که از چپ به راست اعمال میشود، امنیت را بیشتر کنید. اگر آدرس دفتر شما پویا (dynamic) است، محدوده را روی زیرشبکهای (subnet) تنظیم کنید که توسط یک WireGuard VPN که خودتان روی همان VPS میزبانی میکنید اختصاص داده شده است و فقط از طریق تونل به داشبورد دسترسی داشته باشید:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authThe Docker socket is root
/var/run/docker.sock یک API است که میتواند کانتینری ایجاد کند که / را از host mount کند. دسترسی به آن معادل سطح root در ماشین است و Traefik برای خواندن labelها به آن نیاز دارد.
:ro را در mount نگه دارید، اما از مزیت آن آگاه باشید: این کار باعث میشود socket file به صورت read-only باشد. این کار مانع از ارسال درخواستهای POST به Docker API از طریق آن نمیشود. راهکار اصلی این است که هرگز socket را مستقیماً به Traefik ندهید و یک filtering proxy در میان آنها قرار دهید:
dockerproxy:
image: tecnativa/docker-socket-proxy # pin the current tag
restart: unless-stopped
environment:
CONTAINERS: 1
NETWORKS: 1
POST: 0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- proxyحجم (volume) مربوط به socket را از Traefik حذف کنید و provider را به سمت proxy هدایت کنید:
--providers.docker.endpoint=tcp://dockerproxy:2375در این حالت Traefik دسترسی read به containerها و networkها را حفظ میکند، اما توانایی ایجاد هر چیزی را از دست میدهد.
Firewall, ports, and the rule everybody gets wrong
دو پورت باز، بهعلاوه SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableپورتهای منتشر شده در Docker قوانین ufw را دور میزنند. Docker قوانین iptables مخصوص خود را درج میکند که پیش از زنجیرههای ufw بررسی میشوند؛ بنابراین اگر کانتینری با ports: ["3000:3000"] اجرا شود، با وجود وجود یک قانون deny در ufw، از طریق اینترنت قابل دسترسی خواهد بود. این یک نقص ساختاری است، نه پیکربندی فایروال: پورتها را فقط از طریق Traefik منتشر کنید و به سایر کانتینرها فقط networks: [proxy] اختصاص دهید. اگر چیزی واقعاً باید به host دسترسی داشته باشد، آن را به loopback متصل کنید — "127.0.0.1:3000:3000".
Troubleshooting: errors you will actually see
404 page not found، ارائه شده توسط Traefik. هیچ router ای مطابقت داده نشد. به ترتیب احتمال: container فاقد traefik.enable=true است (در حالی که exposedByDefault=false تنظیم شده است)؛ rule مربوط به Host() با نام تایپ شده مطابقت ندارد؛ نام router در یک label با نام router در label دیگر متفاوت است (routers.gitea.rule و routers.gitea.entrypoints باید یک عبارت باشند)؛ یا hostname را به جای backticks در quotes قرار دادهاید. Traefik v3 در داخل matcherها به backticks نیاز دارد.
502 Bad Gateway. یک router مطابقت داده شد اما backend در دسترس نبود. تقریباً همیشه container در شبکه proxy قرار ندارد — docker inspect -f '{{json .NetworkSettings.Networks}}' gitea را بررسی کنید. دلیل دیگر، loadbalancer.server.port اشتباه است: شما یک port منتشر شده (published port) ارائه دادهاید، یا اپلیکیشن در جای دیگری گوش میدهد. لاگ، نام تلاش انجام شده را ذکر میکند: dial tcp 172.18.0.5:8080: connect: connection refused.
مرورگر هشدار میدهد و گواهینامه برای TRAEFIK DEFAULT CERT صادر میشود. گواهینامهای برای آن hostname وجود ندارد و Traefik جایگزین self-signed خود را ارائه داده است. خطوط ACME را بخوانید:
unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.comDNS هنوز به این سرور اشاره نمیکند. رکورد را اصلاح کنید، منتظر اتمام TTL بمانید، و Traefik را restart کنید.
Invalid response from http://git.example.com/.well-known/acme-challenge/... در HTTP challenge: پورت 80 از بیرون به Traefik نمیرسد — معمولاً یک فایروال در سطح provider در مقابل VPS است، نه ufw.
گواهینامهها هرگز صادر نمیشوند و DNS شما در Cloudflare با ابر نارنجی (orange cloud) روشن است. Cloudflare پروتکل TLS را در لبه (edge) خود پایان میدهد و TLS-ALPN-01 نمیتواند از طریق آن تکمیل شود. هنگام صدور گواهینامه، رکورد را روی DNS-only تنظیم کنید، یا به DNS-01 challenge با یک API token سوئیچ کنید. DNS-01 تنها challengهای است که گواهینامههای wildcard صادر میکند.
Redirect loop. چیزی در مقابل Traefik از قبل TLS را پایان میدهد و متن ساده (plaintext) را به :80 میفرستد؛ redirect مربوط به entrypoint آن را دوباره به HTTPS برمیگرداند. یکی از این دو redirect را حذف کنید.
Keeping it running
واحد Docker باید در هنگام بوت فعال باشد (systemctl is-enabled docker)، و restart: unless-stopped استک را پس از بازنشانی سیستم (reboot) برمیگرداند. برای مدیریت دقیقتر، یک unit کوچک در systemd که docker compose -f /srv/edge/compose.yml up -d را با RemainAfterExit=yes اجرا میکند، به شما کنترل ترتیب و systemctl status edge را میدهد.
تگ Traefik را ثابت نگه دارید (traefik:v3.5، هرگز از latest استفاده نکنید). ارتقا از نسخه v2 به v3، سینتکس قوانین و نام providerها را تغییر داده است؛ یک latest خودکار، تنظیماتی را که دیگر نمیشناسد، مجدداً بارگذاری میکند. ارتقا را آگاهانه انجام دهید: یادداشتهای مهاجرت را بخوانید، تگ را تغییر دهید، docker compose up -d traefik را اجرا کنید و لاگها را بررسی کنید. اگر هنوز از تگ v2 استفاده میکنید، راهنمای مهاجرت از Traefik v2 به v3 تمام تغییرات نام، حالت سازگاری (compatibility mode) و روش بازگشت به حالت قبل (rollback) که گواهیهای شما را حفظ میکند، توضیح داده است.
از ./letsencrypt و volume دادههای هر اپلیکیشن بکآپ تهیه کنید. Traefik هیچ وضعیت (state) دیگری ندارد که نتوان از طریق compose file آن را بازسازی کرد.
آنچه در مقیاس بالا دچار مشکل میشود
اولین محدودیت، میزان پردازش (throughput) نیست، بلکه محدودیت یک سرور واحد است: یک instance از Traefik روی یک VPS، یک نقطه شکست (single point of failure) برای 5 اپلیکیشن محسوب میشود. همچنین acme.json از ذخیرهسازی berbasis فایل (flat-file) استفاده میکند؛ بنابراین اگر دو instance از Traefik همزمان در حال نوشتن روی آن باشند، فایل خراب خواهد شد. مقیاسپذیری به معنای انتقال ذخیرهسازی گواهینامهها از فایل، یا پایان دادن به TLS در مکانی دیگر است.
مورد دوم، اتصالات طولانیمدت (long-lived connections) است. رویدادهای سمت سرور (Server-sent events)، آپلودهای حجیم و کلاینتهای کند با محدودیتهای زمانی (timeouts) در entrypoint مواجه میشوند؛ --entryPoints.websecure.transport.respondingTimeouts.readTimeout و همسالان آن یعنی writeTimeout و idleTimeout، تنظیماتی هستند که باید برای مدیریت این وضعیت تغییر کنند. WebSockets بدون نیاز به پیکربندی اضافی، عبور دادهها را انجام میدهند.
مورد سوم، دیسک است. --accesslog=true دادهها را در stdout مینویسد و driver مدل json-file در Docker، این دادهها را تا ابد نگه میدارد مگر اینکه محدود شود. مقدار logging.options.max-size را در سرویس Traefik تنظیم کنید، یا log دسترسی (access log) را در یک فایل بنویسید و آن را rotate کنید.
هیچکدام از این موارد نیازمند یک orchestrator نیستند. تنها نیاز به سروری دارید که تحت کنترل شما باشد، دارای یک IP واقعی باشد و پورتهای 80 و 443 آن برای کل اینترنت باز باشد؛ یک VPS کوچک، تمام پیشنیازهای مورد نیاز است.
FAQ
آیا اگر از Traefik استفاده کنم، همچنان به certbot نیاز دارم؟
خیر. ACME resolver در Traefik برای هر hostname که مسیریابی میکند، درخواست گواهی (certificate) میدهد و آن را تمدید میکند و تمام آنها را در acme.json ذخیره میکند. اگر nginx یا سرور دیگری مستقیماً TLS را مدیریت کند، Certbot ابزار مناسبی است؛ اجرای هر دو روی یک hostname یکسان، باعث مصرف بیدلیل محدودیتهای نرخ (rate limits) Let's Encrypt میشود.
چرا کانتینر من از طریق Traefik خطای 404 برمیگرداند؟
خطای 404 که توسط Traefik ارسال میشود به این معناست که هیچ router ای با درخواست مطابقت نداشته است. بررسی کنید که کانتینر دارای traefik.enable=true باشد (پس از تنظیم exposedByDefault=false اجباری است)، مقدار Host() با نام تایپ شده مطابقت داشته باشد و نام router در تمام labelهای مربوط به آن اپلیکیشن یکسان باشد. در Traefik v3، داخل matcher باید از backtick استفاده شود، نه از quote.
تفاوت بین خطای 404 و 502 در اینجا چیست؟
خطای 404 یعنی مسیریابی (routing) هرگز انجام نشده است؛ خطای 502 یعنی یک router مطابقت داشته اما backend اتصال را رد کرده است. دلایل معمول 502 عبارتند از: کانتینری که به شبکه proxy متصل نیست، و یک loadbalancer.server.port که به جای پورت داخلی اپلیکیشن، به یک پورت منتشر شده (published port) اشاره میکند. فایل access log، آدرس دقیق مورد استفاده توسط Traefik را نشان میدهد.
آیا mount کردن Docker socket به صورت read-only کافی است؟
فلگ :ro فایل socket را read-only میکند، اما API پشت آن را نه؛ درخواستهای POST همچنان از طریق آن ارسال میشوند و دسترسی به Docker API معادل دسترسی root در host است. روش امنتر، استفاده از کانتینر docker-socket-proxy نشان داده شده در بالا است که فقط دسترسیهای خواندنی (read) کانتینر و شبکه را برای Traefik فراهم میکند و دسترسیهای نوشتنی (write) را کاملاً مسدود میکند.
آیا Traefik میتواند گواهی wildcard صادر کند؟
فقط از طریق challenge نوع DNS-01 و با استفاده از API token برای ارائهدهنده DNS شما. روشهای TLS-ALPN-01 و HTTP-01 هر کدام فقط یک hostname را تایید میکنند و نمیتوانند wildcard ایجاد کنند. همچنین اگر یک CDN مانند Cloudflare در مقابل VPS شما TLS را مدیریت کند و دو روش دیگر با شکست مواجه شوند، DNS-01 تنها راه حل است.