اجرای 5 برنامه با Traefik v3 در یک Docker Compose
با استفاده از Traefik v3 روی Docker Compose، پنج سرویس را با یک IP مدیریت کنید. نحوه تنظیم Host rule، دریافت خودکار TLS و رفع خطای دسترسی فایل acme.json را بیاموزید.
یک IP، پنج برنامه، یک پورت 443
سرور مجازی (VPS) شما دارای یک آدرس IPv4 عمومی و تنها یک پورت TCP 443 است. شما میخواهید Gitea، یک نسخه staging از برنامه خود، یک داشبورد داخلی، یک صفحه وضعیت و یک دریافتکننده webhook را روی آن اجرا کنید؛ پنج نام دامنه، یک سرور. یک reverse proxy فرآیندی است که پورتهای 80 و 443 را در اختیار میگیرد، هدر Host را در هر درخواست میخواند و آن را به container صحیح هدایت میکند. Traefik این کار را انجام میدهد و برای هر نام دامنه، بدون اینکه نیاز باشد شخصاً certbot را اجرا کنید، گواهی دریافت و تمدید میکند. Nginx و Caddy نیز میتوانند همین پنج نام دامنه را بهخوبی مدیریت کنند؛ بنابراین اگر هنوز در انتخاب مردد هستید، ارزش دارد که پیش از اتصال همه چیز به یکی از آنها، سه پروکسی را از نظر مدیریت گواهی و هزینه پیکربندی برای هر برنامه مقایسه کنید.
آنچه Traefik را از یک بلاک server {} در nginx متمایز میکند، منبع پیکربندی آن است. در nginx شما یک فایل را ویرایش و reload میکنید و چرخه حیات گواهی یک وظیفه جداگانه باقی میماند؛ همان گردش کاری که هنگام صدور گواهیهای Let's Encrypt با certbot روی nginx دنبال میکنید، جایی که زمانسنج تمدید کاملاً خارج از وبسرور قرار دارد. ارائهدهنده Docker در Traefik، جریان رویدادهای Docker را زیر نظر میگیرد و برچسبها (labels) را از روی containerهای شما میخواند: containerای را با یک برچسب قانون Host() شروع کنید تا در کمتر از یک ثانیه قابل مسیریابی شود؛ آن را متوقف کنید تا مسیر ناپدید شود. این همان دام است. پیکربندی که در برچسبها قرار دارد در پنج مکان مختلف پخش شده است و یک برچسب اشتباه، بیصدا میماند؛ container بهسادگی مسیریابی نمیشود و Traefik هیچ پیامی اعلام نمیکند.
چهار مفهوم اصلی
- Entrypoints همان سوکتهای در حال گوش دادن (listening sockets) هستند. شما دو مورد را تعریف خواهید کرد:
webروی:80وwebsecureروی:443. - Routers درخواستها (
Host(...)) را تطبیق داده و آنها را به یک سرویس متصل میکنند. گواهیها از طریقtls.certresolverبرای هر router درخواست میشوند. - Services همان بکاند، کانتینر و پورتی هستند که داخل شبکه Docker روی آن گوش میدهد.
- Middlewares بین router و service قرار میگیرند: احراز هویت پایه (basic auth)، لیستهای مجاز IP، بازنویسی هدرها و تغییر مسیرها (redirects).
این چهار مفهوم، نامهایی هستند که Traefik برای کارهایی انتخاب کرده که در غیر این صورت باید بهصورت دستی انجام میدادید: router همان server_name است، service همان هدف proxy_pass است و middlewares همان دستورالعملهای هدر و احراز هویتی هستند که هنگام ساخت خطبهخط بلوک سرور reverse proxy در nginx خودتان تنظیم میکنید.
پیکربندی ایستا (entrypoints، providers، ACME) از طریق خط فرمان Traefik یا در traefik.yml ارسال میشود و تغییر آن مستلزم راهاندازی مجدد Traefik است. پیکربندی پویا (routers، services، middlewares) از طریق برچسبهای کانتینر (container labels) دریافت شده و بهصورت زنده (hot-reloaded) بارگذاری میشود. اشتباه گرفتن این دو، دلیل اصلی این است که میگویید "فلگ من هیچ کاری انجام نمیدهد".
فایل compose
یک شبکه Docker مشترک به نام proxy ستون فقرات این ساختار است. Traefik تنها در صورتی به یک container دسترسی دارد که هر دو در آن شبکه باشند.
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 آن را حذف کنید، آن را به proxy متصل کنید و اجازه دهید labelهای router، نام دامنه و گواهی را مدیریت کنند.
پنج نکته در اینجا اهمیت ویژهای دارند.
exposedByDefault=false باعث میشود یک container تا زمانی که برچسب traefik.enable=true را نداشته باشد، برای Traefik نامرئی بماند. اگر آن را حذف کنید، برای هر container که اجرا میکنید—از جمله postgres موقتی که برای بررسی چیزی اجرا کردهاید—یک مسیر (route) ایجاد میشود.
providers.docker.network=proxy به Traefik میگوید که وقتی یک container به چندین شبکه متصل است، از کدام شبکه استفاده کند. اگر آن را نادیده بگیرید، ممکن است Traefik آیپی اشتباهی را انتخاب کند که منجر به خطای 502 میشود؛ خطایی که در ظاهر شبیه به نقص برنامه به نظر میرسد.
loadbalancer.server.port=3000 پورتی است که داخل container قرار دارد؛ برای مثال Gitea در آنجا روی پورت 3000 گوش میدهد. توجه کنید که هیچ container برنامهای پورت خود را publish نمیکند و فقط Traefik این کار را انجام میدهد.
تغییر مسیر (redirect) در نقطه ورود web، درخواستهای متنی ساده (plaintext) را به یک کد 308 به سمت HTTPS هدایت میکند. پورت 80 در هر صورت باز میماند: چالش ACME HTTP به آن نیاز دارد و همچنین کاربرانی که نام دامنه را بدون پروتکل وارد میکنند.
تکرار $$ در هش basic-auth، به دلیل escape کردن در 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 نگه میدارد. اگر این فایل برای گروه یا سایر کاربران قابل خواندن باشد، Traefik خطایی مشابه این چاپ کرده و متوقف میشود:
error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600راه حل تمیز همان است که در بالا ذکر شد: دایرکتوری را bind-mount کنید و اجازه دهید Traefik خودش فایل را با مجوز (mode) صحیح ایجاد کند. اگر acme.json را با touch ایجاد کردهاید، umask شما آن را 644 قرار داده است. آن را روی میزبان اصلاح کنید:
chmod 600 ./letsencrypt/acme.json
docker compose restart traefikاز آن دایرکتوری به همراه volumeهای برنامه خود نسخه پشتیبان تهیه کنید. از دست دادن آن قابل جبران است و گواهیها دوباره صادر میشوند، اما صدور مجدد پنج نام میزبان بهطور همزمان، شما را درگیر محدودیتهای نرخ (rate limits) میکند.
هنگام تست و توسعه از CA مرحلهبندی (staging) استفاده کنید. خط caserver را از حالت کامنت خارج کنید، مطمئن شوید تمام مسیرها کار میکنند، سپس آن را دوباره کامنت کرده و acme.json را حذف کنید تا گواهیهای production بهصورت تازه درخواست شوند. Let's Encrypt در محیط production اجازه صدور پنج گواهی تکراری در هفته برای مجموعهای یکسان از نامهای میزبان را میدهد و اعتبارسنجیهای ناموفق مکرر برای یک نام را محدود میکند. محیط staging گواهیهای غیرقابلاعتماد صادر میکند، مرورگر شما هشدار میدهد و همین هشدار نشانه موفقیتآمیز بودن عملیات است، با این تفاوت که محدودیتهای بسیار منعطفتری دارد.
داشبورد یک سطح کنترل است، نه یک دمو
بیشتر راهنماهای شروع سریع، --api.insecure=true را تنظیم میکنند که داشبورد را روی پورت 8080 بدون احراز هویت ارائه میدهد. روی سروری با IP عمومی، این کار توپولوژی مسیریابی، نامهای میزبان، نامهای میانافزار و پورتهای backend شما را در اختیار هر کسی که آن را اسکن کند، قرار میدهد.
برچسبهای موجود در سرویس traefik در بالا، جایگزین این وضعیت هستند: داشبورد مانند هر برنامه دیگری، روی یک نام میزبان واقعی، از طریق TLS و پشت basicauth مسیریابی میشود. service=api@internal همان چیزی است که مسیریاب را به API داخلی Traefik متصل میکند. با زنجیرهسازی یک لیست مجاز IP که از چپ به راست اعمال میشود، امنیت آن را بیشتر کنید. اگر آدرس دفتر شما پویا است، محدوده را روی زیرشبکهای تنظیم کنید که توسط یک WireGuard VPN که روی همان VPS میزبانی میکنید ارائه میشود و فقط از طریق تونل به داشبورد دسترسی پیدا کنید:
- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-authیک رمز عبور مشترک basicauth زمانی که پنج برنامه به حسابهای کاربری مجزا نیاز دارند، دیگر قابل دفاع نیست. همان جایگاه میانافزار میتواند یک forwardauth را بپذیرد که تصمیمگیری را به Authentik، یک سرور احراز هویت یکپارچه (SSO) خودمیزبان واگذار میکند؛ بنابراین داشبورد و تمام مسیرهای کنار آن، پشت یک ورود واحد قرار میگیرند که میتوانید آن را از یک نقطه مرکزی ابطال کنید.
دسترسی root سوکت Docker
/var/run/docker.sock یک API است که میتواند containerای ایجاد کند که / را از میزبان mount میکند. دسترسی به آن معادل دسترسی root روی ماشین است و Traefik برای خواندن labelها به آن نیاز دارد.
:ro را روی mount حفظ کنید، اما شفاف باشید که این کار چه مزیتی دارد: این کار فقط فایل سوکت را به حالت read-only در میآورد. این کار مانع از ارسال درخواستهای POST به API داکر از طریق آن نمیشود. راهکار واقعی این است که هرگز سوکت را مستقیماً به Traefik ندهید و یک 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:
- proxyvolume سوکت را از Traefik حذف کنید و provider را به سمت proxy هدایت کنید:
--providers.docker.endpoint=tcp://dockerproxy:2375Traefik دسترسی خواندن به containerها و شبکه را حفظ میکند و قابلیت ایجاد هرگونه منبع جدید را از دست میدهد.
فایروال، پورتها و قانونی که همه در آن اشتباه میکنند
دو پورت باز، به علاوه SSH:
sudo ufw allow OpenSSH
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enableپورتهای منتشر شده توسط Docker، فایروال ufw را دور میزنند. داکر قوانین iptables اختصاصی خود را تزریق میکند که پیش از زنجیرههای ufw ارزیابی میشوند؛ بنابراین کانتینری که با ports: ["3000:3000"] اجرا شده باشد، حتی با وجود قانون deny در ufw، از اینترنت قابل دسترس است. دفاع در برابر این مسئله ساختاری است، نه پیکربندی فایروال: پورتها را فقط از طریق Traefik منتشر کنید و به سایر کانتینرها فقط networks: [proxy] اختصاص دهید و هیچ پورت دیگری باز نکنید. اگر سرویسی واقعاً باید به میزبان دسترسی داشته باشد، آن را به loopback متصل کنید: "127.0.0.1:3000:3000".
عیبیابی: خطاهایی که واقعاً با آنها مواجه میشوید
خطای 404 page not found، که توسط Traefik ارائه میشود. هیچ router با درخواستی مطابقت نداشته است. به ترتیب احتمال وقوع: کانتینر فاقد traefik.enable=true است (در حالی که exposedByDefault=false تنظیم شده است)؛ قانون Host() با نامی که وارد کردهاید مطابقت ندارد؛ نام router در یکی از labelها با نام router در دیگری متفاوت است (routers.gitea.rule و routers.gitea.entrypoints باید دقیقاً یک کلمه باشند)؛ یا نام میزبان (hostname) را به جای backtick، داخل کوتیشن قرار دادهاید. نسخه Traefik v3 برای matchers به backtick نیاز دارد.
خطای 502 Bad Gateway. یک router مطابقت داشته اما backend در دسترس نبوده است. تقریباً همیشه به این دلیل است که کانتینر در شبکه proxy قرار ندارد؛ docker inspect -f '{{json .NetworkSettings.Networks}}' gitea را بررسی کنید. احتمال دیگر، اشتباه بودن loadbalancer.server.port است: شما یک پورت published به آن دادهاید، یا برنامه روی پورت دیگری گوش میدهد. لاگها تلاش انجامشده را نام میبرند: dial tcp 172.18.0.5:8080: connect: connection refused.
مرورگر هشدار میدهد و گواهی برای TRAEFIK DEFAULT CERT صادر شده است. هیچ گواهی برای آن نام میزبان وجود ندارد و 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: پورت 80 از بیرون به Traefik نمیرسد؛ معمولاً به دلیل فایروال سطح ارائهدهنده (provider-level) در مقابل VPS است، نه ufw.
گواهیها هرگز صادر نمیشوند و DNS شما روی Cloudflare با ابر نارنجی فعال است. Cloudflare عملیات TLS را در لبه شبکه خود خاتمه میدهد و چالش TLS-ALPN-01 نمیتواند از طریق آن تکمیل شود. هنگام صدور گواهی، رکورد را روی حالت DNS-only قرار دهید یا از چالش DNS-01 با استفاده از API token استفاده کنید. DNS-01 همچنین تنها چالشی است که گواهیهای wildcard صادر میکند.
حلقه تغییر مسیر (Redirect loop). چیزی در مقابل Traefik وجود دارد که TLS را خاتمه داده و ترافیک را به صورت plaintext به پورت 80 میفرستد؛ تغییر مسیر (redirect) در entrypoint، درخواست را دوباره به HTTPS میفرستد. یکی از این دو redirect را حذف کنید.
تداوم اجرا
واحد Docker باید برای شروع خودکار در زمان بوت فعال باشد (systemctl is-enabled docker)، و restart: unless-stopped پشته (stack) را پس از راهاندازی مجدد سیستم بازمیگرداند. برای مدیریت دقیقتر، یک واحد systemd کوچک که docker compose -f /srv/edge/compose.yml up -d را با RemainAfterExit=yes اجرا میکند، به شما systemctl status edge و کنترل ترتیب اجرا را میدهد.
تگ Traefik را ثابت نگه دارید (traefik:v3.5، هرگز از latest استفاده نکنید). ارتقا از نسخه v2 به v3 نحو (syntax) قوانین و نام ارائهدهندگان (providers) را تغییر داده است و یک latest خودکار، با خوشحالی پیکربندیای را بارگذاری میکند که دیگر آن را نمیفهمد. ارتقا را آگاهانه انجام دهید: یادداشتهای مهاجرت را بخوانید، تگ را تغییر دهید، docker compose up -d traefik را اجرا کنید و لاگها را زیر نظر بگیرید. اگر هنوز از تگ v2 استفاده میکنید، راهنمای مهاجرت Traefik از v2 به v3 تمام تغییر نامها، حالت سازگاری و روش بازگشت به نسخه قبل (rollback) که گواهیهای شما را حفظ میکند، توضیح میدهد.
از ./letsencrypt و حجم داده (data volume) هر برنامه نسخه پشتیبان تهیه کنید. Traefik هیچ وضعیت (state) دیگری که نتوانید از فایل compose بازسازی کنید، نگه نمیدارد.
چه چیزی در مقیاس بزرگ دچار اختلال میشود
اولین محدودیت، توان عملیاتی نیست، بلکه محدودیت یک سرور واحد است: یک Traefik روی یک VPS، نقطه شکست واحد برای پنج برنامه است و از آنجا که acme.json یک ذخیرهساز فایلمحور (flat-file) است، نوشتن همزمان دو نمونه Traefik روی آن باعث خرابی دادهها میشود. مقیاسپذیری افقی به معنای انتقال ذخیرهسازی گواهیها به خارج از فایل، یا خاتمه دادن TLS در جای دیگری است.
دومین مورد، اتصالات طولانیمدت است. رویدادهای ارسالی از سمت سرور (SSE)، آپلودهای حجیم و کلاینتهای کند با تایماوتهای پاسخدهی نقطه ورود (entrypoint) برخورد میکنند؛ --entryPoints.websecure.transport.respondingTimeouts.readTimeout و همتایان آن یعنی writeTimeout و idleTimeout، ابزارهای تنظیم این موارد هستند. WebSockets بدون نیاز به پیکربندی اضافی عبور میکنند.
سومین مورد، دیسک است. --accesslog=true لاگها را در stdout مینویسد و درایور json-file در Docker، آنها را تا زمانی که محدود نشوند برای همیشه نگه میدارد. برای سرویس Traefik گزینه logging.options.max-size را تنظیم کنید، یا لاگ دسترسی را در یک فایل بنویسید و آن را چرخش (rotate) دهید.
هیچکدام از این موارد به ارکستراتور نیاز ندارند. این تنظیمات به سروری نیاز دارند که تحت کنترل شما باشد، دارای IP واقعی بوده و پورتهای 80 و 443 آن برای دسترسی جهانی باز باشند؛ یک VPS کوچک تنها وابستگی مورد نیاز است.
FAQ
آیا در صورت استفاده از Traefik همچنان به certbot نیاز دارم؟
خیر. ACME resolver در Traefik برای هر نام دامنهای که مسیریابی میکند، گواهی درخواست و تمدید کرده و همه آنها را در acme.json ذخیره میکند. Certbot زمانی ابزار مناسبی است که Nginx یا سرور دیگری خود وظیفه TLS termination را بر عهده داشته باشد؛ اجرای همزمان هر دو برای نامهای دامنه یکسان، تنها باعث مصرف بیمورد سهمیه (rate limit) در Let's Encrypt میشود.
چرا کانتینر من از طریق Traefik خطای 404 برمیگرداند؟
خطای 404 که توسط Traefik ارائه میشود به این معناست که هیچ روتر (router) با درخواست مطابقت نداشته است. بررسی کنید که کانتینر دارای traefik.enable=true باشد (هنگامی که exposedByDefault=false تنظیم شده، این مورد اجباری است)، مقدار Host() با نامی که وارد کردهاید مطابقت داشته باشد و نام روتر در تمام labelهای مربوط به آن برنامه یکسان باشد. همچنین در Traefik v3، عبارتهای matcher باید داخل backtick قرار بگیرند، نه داخل کوتیشن.
تفاوت بین خطای 404 و 502 در اینجا چیست؟
خطای 404 به این معناست که مسیریابی هرگز انجام نشده است؛ خطای 502 به این معناست که روتر مطابقت داشته اما 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 است که در بالا نشان داده شد؛ این روش فقط دسترسی خواندن کانتینر و شبکه را به Traefik میدهد و دسترسیهای نوشتن را کاملاً مسدود میکند.
آیا Traefik میتواند گواهی wildcard صادر کند؟
فقط از طریق چالش DNS-01 و با استفاده از یک API token از ارائهدهنده DNS شما. چالشهای TLS-ALPN-01 و HTTP-01 هر کدام تنها یک نام دامنه را اعتبارسنجی میکنند و نمیتوانند گواهی wildcard تولید کنند. همچنین زمانی که یک CDN مانند Cloudflare وظیفه TLS termination را در مقابل VPS شما بر عهده دارد و دو چالش دیگر هرگز تکمیل نمیشوند، DNS-01 راهکار نهایی است.