SSD Nodes Learn
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-07-24

הגדרת Traefik v3 עם Docker Compose

מדריך להרצת 5 אפליקציות על IP אחד באמצעות Traefik v3. נלמד איך להשתמש ב-Host rule וב-Let's Encrypt ואיך להימנע מבעיית ה-acme.json ב-Docker.

IP אחד, חמש אפליקציות, פורט 443 אחד

ל-VPS שלך יש כתובת IPv4 ציבורית אחת ופורט TCP אחד, 443. אתה רוצה להריץ עליו את Gitea, עותק staging של האפליקציה שלך, דשבורד פנימי, דף סטטוס ומקבל webhooks — חמישה שמות דומיין (hostnames) על שרת אחד. reverse proxy הוא התהליך שמשתלט על פורטים :80 ו-:443, קורא את ה-header של Host בכל בקשה ומעביר אותה ל-container המתאים. Traefik מבצע פעולה זו, והוא משיג ומחדש תעודה עבור כל hostname מבלי שתצטרך להריץ את certbot באופן ידני.

מה שמבדיל את Traefik מבלוק nginx server {} הוא המקור להגדרות שלו. ב-nginx עליך לערוך קובץ ולבצע reload, ומחזור החיים של התעודה נשאר משימה נפרדת — זרימת העבודה שבה אתה מנפיק תעודות Let's Encrypt באמצעות certbot ב-nginx, כאשר טיימר החידוש נמצא מחוץ לשרת ה-web לחלוטין. ה-Docker provider של Traefik עוקב אחר ה-Docker event stream וקורא labels מה-containers שלך: הפעל container עם label של חוק Host() והוא יהיה זמין בניתוב תוך שנייה; עצור אותו והניתוב ייעלם. זהו גם מלכוד. הגדרות שנמצאות בתוך labels קיימות בחמישה מקומות בו-זמנית, ו-label שגוי לא יקפיץ שגיאה — ה-container פשוט לא ינותב, ו-Traefik לא ידווח על כך.

ארבעת השמות

  • Entrypoints הם sockets המאזינים לבקשות. תגדירו שניים: web ב-:80 ו-websecure ב-:443.
  • Routers מבצעים התאמה לבקשה (Host(...)) ומחברים אותה לשירות. תעודות (Certificates) מתבקשות עבור כל router באמצעות tls.certresolver.
  • Services הם ה-backend — קונטיינר והפורט (port) שעליו הוא מאזין בתוך רשת ה-Docker.
  • Middlewares נמצאים בין ה-router לבין ה-service: אימות בסיסי (basic auth), רשימות IP מורשות, שינוי כותרות (header rewrites) והפניות (redirects).

הגדרות סטטיות (entrypoints, providers, ACME) מועברות בשורת הפקודה של Traefik או ב-traefik.yml, ושינוי שלהן מחייב הפעלה מחדש של Traefik. הגדרות דינמיות (routers, services, middlewares) מתקבלות מתוך labels של הקונטיינר ומתעדכנות בזמן אמת (hot-reloaded). בלבול בין השתיים הוא הגורם הנפוץ לכך ש-"הדגל (flag) שלי לא עושה כלום".

קובץ ה-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 ports), לחבר אותה ל-proxy, ולהשאיר את תוויות ה-router לטיפול בשם המארח (hostname) ובתעודה.

חמישה פרטים כאן הם קריטיים.

exposedByDefault=false הופכת את המכולה לבלתי נראית עבור Traefik עד שהיא כוללת את traefik.enable=true. אם תשמיטו זאת, עבור כל מכולה שתפעיל — כולל ה-postgres הזמנית שהרצת לבדיקה — תיווצר עבורה נתיב (route).

providers.docker.network=proxy אומרת ל-Traefik באיזו רשת להשתמש כאשר מכולה מחוברת למספר רשתות. אם תשמיטו זאת, Traefik עלולה לבחור בכתובת ה-IP הלא נכונה של המכולה, מה שיוביל לשגיאת 502 שנראית כתקלה באפליקציה.

loadbalancer.server.port=3000 הוא הפורט בתוך המכולה; Gitea מאזינה בפורט 3000 שם. שים לב שאף מכולת אפליקציה אינה מפרסמת פורט — רק Traefik עושה זאת.

ההפניה (redirect) ב-entrypoint של web הופכת בקשות plaintext להודעת 308 ל-HTTPS. פורט 80 נשאר פתוח בכל מקרה: הוא נחוץ עבור ACME HTTP challenge, וכך גם עבור משתמשים שמקלידים שם מארח ללא פרוטוקול.

הכפילות של $$ בתוך ה-hash של basic-auth היא פעולת escaping של Compose, ולא טעות הקלדה. צור אותה באמצעות htpasswd -nbB admin 'your-password' (חבילת apache2-utils), ולאחר מכן הכפל כל $.

התעודה, ומלכודת ה-acme.json

tlschallenge=true בוחר ב-TLS-ALPN-01: Let's Encrypt מתחברת למחשב שלך בפורט 443 ו-Traefik עונה על ה-challenge בתוך ה-TLS handshake. האפשרות השנייה היא HTTP-01, בפורט 80 — החלף את שורת ה-tlschallenge ברשימת ה-command: של Traefik בשתי השורות הבאות:

      - --certificatesresolvers.le.acme.httpchallenge=true
      - --certificatesresolvers.le.acme.httpchallenge.entrypoint=web

שתי האפשרויות עובדות. שתיהן דורשות שה-DNS הציבורי עבור ה-hostname יצביע כבר ל-VPS שלך — רשות התעודות (CA) פותרת את השם ומתחברת מבחוץ. צור תחילה את רשומת ה-A (וגם AAAA), וודא זאת באמצעות dig +short git.example.com, ולאחר מכן הפעל את Traefik.

כעת, המלכודת שגורמת לאנשים לבזבז ערב שלם. Traefik שומרת את מפתח חשבון ה-ACME ואת כל התעודות שהונפקו בקובץ acme.json אחד. אם הקובץ ניתן לקריאה על ידי ה-group או ה-world, Traefik תדפיס שורה דומה לזו ותפסיק לפעול:

error: unable to get ACME account: permissions 644 for /letsencrypt/acme.json are too open, please use 600

הפתרון הנכון הוא הפתרון שצוין לעיל: בצע bind-mount ל-directory ואפשר ל-Traefik ליצור את הקובץ בעצמה עם ה-mode המתאים. אם יצרת את acme.json באמצעות touch, ה-umask שלך הגדיר אותו כ-644. תקן זאת על גבי ה-host:

chmod 600 ./letsencrypt/acme.json
docker compose restart traefik

בצע גיבוי ל-directory הזה יחד עם ה-app volumes שלך. אובדן הקובץ אינו קטלני — ניתן להנפיק את התעודות מחדש — אך הנפקת חמישה hostnames בבת אחת תגרום לך להגיע למגבלות הקצב (rate limits).

השתמש ב-staging CA בזמן תהליך הפיתוח. בטל את ההערה (uncomment) מהשורה caserver, וודא שכל ה-routes עובדים, ולאחר מכן החזר את ההערה ויםחוק את acme.json כדי לבקש תעודות production חדשות. Let's Encrypt בייצור מאפשרת חמש תעודות כפולות בשבוע עבור סט זהה של hostnames, ומגבילה אימותים שנכשלו שוב ושוב עבור אותו שם. ה-staging מנפיק תעודות שאינן מהימנות — הדפדפן יציג אזהרה, והאזהרה הזו היא הסימן שהתהליך הצליח — עם מגבלות רחבות בהרבה.

ה-dashboard הוא ממשק בקרה, לא דמו

רוב המדריכים המהירים מגדירים את --api.insecure=true, המגיש את ה-dashboard בפורט 8080 ללא אימות. במחשב עם IP ציבורי, זה חושף את טופולוגיית הניתוב, שמות המhosts, שמות ה-middleware ופורטים של ה-backend לכל סורק.

התוויות בשירות traefik לעיל הן החלופה: ה-dashboard מנותב כמו כל אפליקציה אחרת, תחת hostname אמיתי, באמצעות TLS, מאחורי basicauth. service=api@internal הוא הרכיב המחבר את הנתב ל-API המובנה של Traefik. ניתן להדק את האבטחה עוד יותר על ידי שרשור רשימת כתובות IP מורשות (allow-list), המוחלת משמאל לימין. אם כתובת המשרד שלך היא דינמית, הגדר את הטווח לסאבנט (subnet) שמוקצה על ידי WireGuard VPN שאתה מארח בעצמך על אותו VPS וגש ל-dashboard רק דרך המנהרה (tunnel):

- traefik.http.middlewares.office.ipallowlist.sourcerange=10.0.0.7/32
- traefik.http.routers.dashboard.middlewares=office,dashboard-auth

The Docker socket is root

/var/run/docker.sock הוא API שיכול ליצור container שמmount ל-/ מה-host. גישה אליו שקולה לגישת root במכונה, ו-Traefik זקוק לה כדי לקרוא labels.

השאירו את :ro ב-mount, אך היו מודעים לעלות: פעולה זו הופכת את ה-socket file לקריאה בלבד (read-only). היא אינה מונעת בקשות POST ל-Docker API דרכו. הפתרון האמיתי הוא לא לתת ל-Traefik את ה-socket, ולהציב 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

הסירו את ה-socket volume מ-Traefik וכוונו את ה-provider אל ה-proxy:

--providers.docker.endpoint=tcp://dockerproxy:2375

Traefik שומר על גישת קריאה ל-containers ול-networks, ומאבד את היכולת ליצור כל דבר חדש.

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 משלו, אשר נבדקים לפני ה-chains של ufw. לכן, container שהופעל עם ports: ["3000:3000"] יהיה נגיש מהאינטרנט גם אם קיים חוק deny ב-ufw. ההגנה היא מבנית ולא תלויה בהגדרות ה-firewall: פרסמו פורטים מ-Traefik בלבד, ותנו לכל container אחר את 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 באחת ה-labels שונה מהשם ב-label אחר (routers.gitea.rule ו-routers.gitea.entrypoints חייבים להיות אותה מילה); או שהשתמשת במירכאות במקום ב-backticks עבור ה-hostname. ב-Traefik v3 יש להשתמש ב-backticks בתוך matchers.

502 Bad Gateway. ה-router תואם אך ה-backend אינו נגיש. כמעט תמיד ה-container אינו נמצא ב-network של 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 הגיש תעודת placeholder חתומה עצמית. קרא את שורות ה-ACME:

unable to obtain ACME certificate for domains "git.example.com" ...
acme: error: 400 ... DNS problem: NXDOMAIN looking up A for git.example.com

ה-DNS עדיין אינו מפנה אל המחשב. תקן את הרשומה, המתן לסיום ה-TTL, והפעל מחדש את Traefik.

Invalid response from http://git.example.com/.well-known/acme-challenge/... ב-HTTP challenge: פורט 80 אינו מגיע ל-Traefik מבחוץ — בדרך כלל מדובר ב-firewall ברמת הספק לפני ה-VPS, ולא ב-ufw.

התעודות אינן מונפקות, וה-DNS שלך נמצא ב-Cloudflare עם ה-orange cloud פעיל. Cloudflare מבצעת TLS termination ב-edge שלה, ולכן TLS-ALPN-01 אינו יכול להשלים דרכה. הגדר את הרשומה כ-DNS-only בזמן ההנפקה, או עבור ל-DNS-01 challenge עם API token. DNS-01 הוא גם ה-challenge היחיד המנפיק wildcards.

Redirect loop. רכיב לפני Traefik מבצע TLS termination ומעביר plaintext ל-port :80; ה-entrypoint redirect שולח אותו חזרה ל-HTTPS. הסר אחד משני ה-redirects.

Keeping it running

ה-unit של Docker חייב להיות מוגדר להפעלה בעלייה (systemctl is-enabled docker), ו-restart: unless-stopped מחזיר את ה-stack לאחר הפעלה מחדש. לשליטה מפורשת, unit קטן של systemd המריץ את docker compose -f /srv/edge/compose.yml up -d עם RemainAfterExit=yes יספק לך systemctl status edge ושליטה בסדר הפעולה.

קבעו את ה-tag של Traefik (traefik:v3.5, לעולם לא latest). שדרוג מ-v2 ל-v3 שינה את תחביר הכללים ואת שמות ה-providers, ו-latest לא מתוכנן יטען מחדש קונפיגורציה שהוא כבר לא מבין. בצעו שדרוג מתוכנן: קראו את הערות המעבר, עדכנו את ה-tag, בצעו docker compose up -d traefik, ועקבו אחר ה-log. אם אתם עדיין משתמשים ב-tag של v2, מדריך המעבר מ-Traefik v2 ל-v3 מפרט כל שינוי שם, את מצב התאימות (compatibility mode), ואפשרות rollback ששומרת על התעודות שלכם.

גבו את ./letsencrypt ואת ה-data volume של כל אפליקציה. Traefik אינו מחזיק שום state אחר שלא ניתן לשחזר מקובץ ה-compose.

מה נשבר בעת גדילה (at scale)

המחסום הראשון אינו קצב העיבוד (throughput), אלא המכשיר הבודד: מופע Traefik אחד ב-VPS אחד מהווה נקודת כשל יחידה (single point of failure) עבור חמש אפליקציות, ו-acme.json הוא אחסון מבוסס קובץ — שני מופעי Traefik שכותבים אליו יגרמו לשחיתות הנתונים. גדילה אופצית (scaling out) מחייבת העברת אחסון התעודות מתוך קובץ, או סיום (termination) של TLS במקום אחר.

המחסום השני הוא חיבורים ארוכי טווח. Server-sent events, העלאות גדולות ולקוחות איטיים נתקלים ב-timeouts של ה-entrypoint; --entryPoints.websecure.transport.respondingTimeouts.readTimeout והאחים שלו writeTimeout ו-idleTimeout הם הכפתורים לכוונון. WebSockets עוברים ללא צורך בהגדרות נוספות.

המחסום השלישי הוא הדיסק. --accesslog=true כותב ל-stdout, ומנהל ה-json-file של Docker שומר זאת לנצח אלא אם מוגדרת מגבלה. הגדר את logging.options.max-size בשירות ה-Traefik, או כתוב את ה-access log לקובץ ובצע לו rotation.

אף אחד מהדברים הללו אינו דורש orchestrator. הוא דורש שרת שאתה שולט בו, עם IP אמיתי ופורטים 80 ו-443 פתוחים לעולם — VPS קטן אחד הוא כל רשימת התלויות.

FAQ

האם אני עדיין זקוק ל-certbot אם אני מריץ Traefik?

לא. ה-ACME resolver של Traefik מבקש ומחדש את התעודה עבור כל hostname שהוא מנתב, ושומר את הכל ב-acme.json. Certbot נשאר הכלי המתאים כאשר nginx או שרת אחר מבצע TLS termination בעצמו; הרצה של שניהם עבור אותם hostnames תגרום רק לחריגה מהמגבלות (rate limits) של Let's Encrypt.

מדוע ה-container שלי מחזיר 404 דרך Traefik?

שגיאת 404 שמוגשת על ידי Traefik פירושה שאף router לא התאים לבקשה. ודא שה-container מכיל את traefik.enable=true (חובה ברגע ש-exposedByDefault=false מוגדר), שערך ה-Host() תואם לשם שהקלדת, וששם ה-router זהה בכל ה-label עבור האפליקציה הזו. ב-Traefik v3 יש להשתמש ב-backticks בתוך ה-matcher, ולא ב-quotes.

מה ההבדל בין 404 ל-502 במקרה זה?

404 פירושו שלא התבצע ניתוב; 502 פירושו ש-router התאים לבקשה אך ה-backend סירב לחיבור. הגורמים הנפוצים ל-502 הם container שאינו מחובר לרשת ה-proxy, או loadbalancer.server.port שמצביע על port שפורסם במקום על ה-port שהאפליקציה מאזינה עליו בתוך ה-container. ה-access log מפרט את הכתובת המדויקת שאליה Traefik ניסה להתחבר.

האם mounting של ה-Docker socket במצב read-only מספיק?

ה-flag של :ro הופך את קובץ ה-socket למצב read-only, אך לא את ה-API שמאחוריו — בקשות POST עדיין עוברות דרכו, וגישה ל-Docker API שקולה לגישת root על ה-host. ההגדרה המאובטחת יותר היא ה-docker-socket-proxy container המוצג לעיל, אשר חושף ל-Traefik רק הרשאות קריאה ל-container ולרשת וחוסם כתיבה לחלוטין.

האם Traefik יכול להנפיק wildcard certificate?

רק באמצעות DNS-01 challenge, עם API token עבור ספק ה-DNS שלך. שיטות TLS-ALPN-01 ו-HTTP-01 מאמתות hostname בודד בלבד ולא יכולות להנפיק wildcard. DNS-01 היא גם הפתרון כאשר CDN כגון Cloudflare מבצע TLS termination לפני ה-VPS שלך ושתי השיטות האחרות נכשלות.