SSD Nodes Learn 🎉 VPS החל מ־$5.50/חודש
מדריכים Matt Connorמאת Matt Connor · עודכן 2026-08-13

ניתוב Docker דרך VPN: פתרון לבעיית הפורטים שנעלמים

מדוע פורטים מפסיקים לעבוד בשימוש ב-network_mode: service עם Gluetun? גלו כיצד לנתב תעבורה נכון דרך VPN ב-Docker וקבלו קובץ docker-compose תקין שמונע שגיאות רשת.

מדוע פורטים נעלמים בעת ניתוב מכולות Docker דרך VPN

כדי לנתב מכולות Docker דרך VPN, יש להגדיר מכולה אחת שתשמש כמנהרה (tunnel), ולאחר מכן לחבר את שאר המכולות למרחב השמות (network namespace) שלה באמצעות network_mode: "service:gluetun". החיבור הזה הוא החלק המפתיע משתמשים רבים. למכולה המחוברת אין יותר רשת משלה, ולכן הפורטים שפורסמו (published ports) ושם שירות ה-Docker שלה מפסיקים להתקיים. במקום זאת, יש לפרסם את הפורטים במכולת ה-VPN, ושאר המכולות יגיעו ליישום דרך שם המכולה של ה-VPN.

אם תשאיר בלוק ports: במכולה המחוברת, Docker יסרב ליצור אותה לחלוטין:

Error response from daemon: conflicting options: port publishing and the container type network mode

הכלי המשמש לכך הוא Gluetun, מכולה שמתחברת לספק VPN (רשת פרטית וירטואלית) מסחרי באמצעות WireGuard או OpenVPN וכוללת חומת אש משלה. גרסה v3.41.3 היא הגרסה העדכנית נכון לאוגוסט 2026. הדוגמאות משתמשות ב-Mullvad עם WireGuard, לכן עליך להצטייד בחשבון ובמפתח מהספק שלך. אם אתה מעדיף לסיים את המנהרה על חומרה שבבעלותך, הרצת שרת WireGuard משלך על גבי VPS תקים את הצד השני של החיבור, ו-wg-easy ב-Docker תעטוף זאת בממשק אינטרנטי.

מה network_mode: "service:gluetun" עושה בפועל

כל קונטיינר Docker מקבל בדרך כלל מרחב שמות (namespace) רשת משלו: ממשקים, טבלת ניתוב, חוקי firewall ו־sockets להאזנה. מצב service: מדלג על שלב זה ומפעיל את הקונטיינר בתוך מרחב השמות של gluetun. מרחב שמות אחד משמעו כתובת IP אחת, וזה משנה שישה דברים:

  • ליישום אין כתובת משלו. הכתובת שלו היא הכתובת של gluetun.
  • היישום אינו מחובר לאף רשת Docker, לכן שם השירות שלו לעולם לא נרשם ולעולם לא מתבצע לו resolution. קונטיינרים אחרים חייבים להשתמש ב-gluetun.
  • קונטיינרים בתוך מרחב השמות מגיעים זה לזה דרך localhost.
  • שני קונטיינרים באותו מרחב שמות אינם יכולים להאזין לאותו פורט. התיעוד של gluetun חד-משמעי בנושא זה: אין לכך פתרון עוקף.
  • הרשאות (capabilities) שייכות לקונטיינר, לא למרחב שמות. gluetun מחזיק ב-NET_ADMIN וב-/dev/net/tun מכיוון שהוא יוצר את ממשק ה-tunnel. הקונטיינר המצורף אינו יורש אותן.
  • Compose דוחה כל קובץ שבו שירות אחד מגדיר גם את network_mode וגם את networks. חברו את gluetun לרשתות שלכם, והיישום יצטרף אליו.

הפעלה מחדש של gluetun מנתקת את כל מה שמחובר אליו. זו התנהגות מתועדת, וזו הסיבה ש-gluetun מפעיל מחדש את תהליך ה-VPN בתוך הקונטיינר במקום לצאת כאשר החיבור נכשל. לאחר שאתם מפעילים מחדש או יוצרים מחדש את gluetun בעצמכם, הפעילו מחדש את הקונטיינרים המחוברים אליו.

קובץ ה-compose התקין

services:
  gluetun:
    image: qmcgaw/gluetun:v3
    container_name: gluetun
    cap_add:
      - NET_ADMIN
    devices:
      - /dev/net/tun:/dev/net/tun
    environment:
      - VPN_SERVICE_PROVIDER=mullvad
      - VPN_TYPE=wireguard
      - SERVER_CITIES=Amsterdam
      - TZ=Europe/Amsterdam
    env_file:
      - ./gluetun.env
    volumes:
      - ./gluetun:/gluetun
    ports:
      - 127.0.0.1:8080:8080/tcp
    restart: unless-stopped

  qbittorrent:
    image: lscr.io/linuxserver/qbittorrent:latest
    container_name: qbittorrent
    network_mode: "service:gluetun"
    environment:
      - PUID=1000
      - PGID=1000
      - TZ=Europe/Amsterdam
      - WEBUI_PORT=8080
    volumes:
      - ./qbittorrent:/config
      - ./downloads:/downloads
    depends_on:
      gluetun:
        condition: service_healthy
    restart: unless-stopped

התג :v3 הוא הגרסה היציבה העדכנית ביותר בסדרת v3. התג :latest מצביע על ה-commit האחרון בענף ה-master, המהווה את גרסת הפיתוח; לכן, הצמידו את :v3 למכונה שאינכם מעוניינים לנפות בה שגיאות ביום שלישי.

הערך WEBUI_PORT=8080 חייב להתאים לפורט שפורסם, כיוון ש-qBittorrent מאזין בתוך ה-namespace של gluetun, וכלל הפרסום מעביר תעבורה מהמארח לפורט 8080 שם. שינוי של מספר אחד ללא השני יגרום לכך שהפורט לא יגיב. הערך 127.0.0.1:8080:8080 שומר את ממשק הניהול על כתובת ה-loopback של המארח. שימוש ב-8080:8080 בלבד מפרסם את השירות בכל הממשקים ויוצר כלל firewall עצמאי, וזו הדרך שבה פורטים שפורסמו ב-Docker עוקפים את ufw.

העלו את השירותים, ולאחר מכן בצעו בדיקה לפי הסדר הבא:

docker compose up -d
docker compose ps
docker compose logs gluetun | tail -30

הפקודה docker compose ps אמורה להציג את gluetun במצב healthy ואת qbittorrent במצב running. לאחר מכן, אשרו את כתובת היציאה מתוך ה-namespace; זו הבדיקה הקובעת את תקינות שאר ההגדרות:

docker run --rm --network=container:gluetun alpine:3.22 sh -c "apk add wget && wget -qO- https://ipinfo.io"

השדה ip ב-JSON זה אמור להציג את הכתובת של ספק ה-VPN שלכם. אם מוצגת הכתובת של השרת עצמו, היישום אינו נמצא בתוך ה-tunnel, ושום דבר מהמתואר להלן לא יפעל כמצופה.

שמירה על מפתחות מחוץ לקובץ ה-compose

gluetun.env מחזיק את פרטי ההזדהות, והוא נשאר מחוץ ל-git:

WIREGUARD_PRIVATE_KEY=wOEI9rqqbDwnN8/Bpp22sVz48T71vJ4fYmFWujulwUU=
WIREGUARD_ADDRESSES=10.64.222.21/32

שני הערכים מגיעים מקובץ תצורה של WireGuard שאתם מפיקים באזור החשבון של ספק השירות שלכם. הגדירו את הקובץ למצב 600. היו כנים לגבי התועלת בכך: המפתח נשאר מחוץ למאגר שלכם, אך docker inspect gluetun עדיין מדפיס כל משתנה סביבה לכל מי שיכול לגשת ל-socket של Docker. המאמר קובצי סביבה וסודות ב-Docker Compose מכסה את האפשרויות החזקות יותר.

כיצד מכולה מחוץ למנהרה מתקשרת עם מכולה בתוכה

שני הכיוונים עובדים, וכל אחד משתמש בשם שונה. שתי המכולות זקוקות לרשת Docker משותפת, כלומר לרשת של gluetun, כיוון שלמכולה המצורפת אין רשת משלה. כיצד רשתות Docker Compose מחוברות מכסה את הגדרות ברירת המחדל.

מבחוץ פנימה, השתמשו בשם של gluetun ובפורט שבו היישום מאזין. מכולת reverse proxy מגיעה לממשק ה-web של qBittorrent בכתובת gluetun:8080. אין צורך ברשומה ports: עבור פעולה זו, כיוון שתעבורה בין מכולות נשארת ברשת ה-Docker ולעולם אינה נוגעת בפורט של המארח (host).

מבפנים החוצה, השתמשו בשם השירות של המכולה האחרת, למשל postgres:5432. החל מגרסה v3.41, gluetun פותרת שמות של מכולות אחרות מתוך מרחב השמות שלה; לכן, קבעו גרסה זו או חדשה יותר אם שם מסוים מסרב להיפתר.

ה-firewall של gluetun מחליט למי מותר לפתוח אליו חיבור. תעבורה מרשת ה-Docker של gluetun עצמה מורשית. לקוח ברשת משנה (subnet) אחרת, מחשב נייד ב-LAN שלכם או מכולה ברשת bridge נפרדת, ייחסם עד שתגדירו את אותה רשת משנה:

FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24

המשמעות המתועדת מדויקת: רשתות משנה מופרדות בפסיקים ש-gluetun והמכולות החולקות את מחסנית הרשת שלה מורשות לגשת אליהן.

חיבורים נכנסים מהאינטרנט הם בעיה נפרדת. עמיתים (peers) של לקוח torrent מגיעים מצד ה-VPN, לכן פרסום פורט 6881 במארח לא יועיל להם. עליכם להשתמש בפורט שעבר העברה (forwarded) מהספק שלכם, ולציין פורט זה ב-FIREWALL_VPN_INPUT_PORTS, מה שמאפשר פורטים מצד שרת ה-VPN. זהו החלק שרוב מחסניות המדיה שנבנו עם Docker Compose משאירות לא תקין.

מנגנון ה-kill switch: מה קורה כשהמנהרה מתנתקת

תבנית זו מצדיקה את מורכבותה בעת כשל. למכולה המחוברת אין נתיב יציאה חלופי. הנתיב היחיד שלה אל מחוץ למכונה הוא ה-namespace שהיא חולקת, לכן כאשר המנהרה מושבתת, אין לה לאן לחזור. ה-firewall של Gluetun אוכף את אותו כלל מהצד השני: תעבורה יוצאת עוברת דרך המנהרה או אל נקודת הקצה של שרת ה-VPN, וכל השאר נחסם. אין פרק זמן שבו חבילות מידע דולפות דרך הממשק הרגיל בזמן שהלקוח מתחבר מחדש.

Gluetun מנטר את החיבור של עצמו. בכל דקה הוא שולח ICMP echo (פינג) לכתובות ב-HEALTH_ICMP_TARGET_IPS, שברירת המחדל שלהן היא 1.1.1.1,8.8.8.8. בכל חמש דקות הוא מבצע בדיקת TCP ו-TLS (אבטחת שכבת תעבורה) מלאה מול HEALTH_TARGET_ADDRESSES, שברירת המחדל היא cloudflare.com:443,github.com:443. כאשר אלו נכשלים, הוא מאתחל את ה-VPN בתוך המכולה ומתעד זאת בלוגים:

WARN [vpn] restarting VPN because it failed to pass the healthcheck: periodic check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout

קראו את הלוגים של המכולה המחוברת תוך התחשבות בסדר זה. שורות כמו connection refused, operation not permitted ו-i/o timeout בתוך היישום הן תוצאה של מנהרה שקרסה, ולא הגורם לכך. התיעוד של Gluetun מציין זאת במפורש, כיוון שאנשים מדווחים על התוצאה ורודפים אחריה במשך שעות.

HEALTH_RESTART_VPN=on הוא ברירת המחדל ויש להשאירו פעיל. כבו אותו רק בזמן ניפוי שגיאות של כשל ספציפי, כיוון שבלעדיו מנהרה שקרסה תישאר מושבתת.

סדר פעולות: מניעת עליית ה-stack לפני שה-tunnel פעיל

ה-image כולל healthcheck של Docker:

HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheck

פקודה זו מריצה עותק שני וקצר-מועד של gluetun, המבצע שאילתה לשרת ה-health של המופע הפעיל ב-http://127.0.0.1:9999/. מנהרת VPN תקינה משיבה 200 OK. מנהרה תקולה משיבה 500 Internal server error עם מחרוזת שגיאה, וה-container מסומן כ-unhealthy לאחר כשל בודד.

condition: service_healthy הוא המנגנון שממתין לכך. שימוש ב-depends_on: [gluetun] בלבד ממתין רק לעליית ה-container, תהליך המתרחש שניות רבות לפני השלמת ה-handshake; כתוצאה מכך, היישום עולה לרשת שאינה זמינה ולעיתים קרובות מוותר על ניסיון ההתחברות הראשון. המדריך Healthchecks in Docker Compose מפרט את התחביר ואת שדות התזמון.

מגבלה אחת עלולה להפתיע משתמשים: Compose מעריך את התנאי הזה פעם אחת בלבד, בעת יצירת ה-container. הוא אינו עוצר או מאתחל את היישום אם gluetun הופך ל-unhealthy בשלב מאוחר יותר. מנגנון ה-auto-healing הפנימי של gluetun מטפל במקרה זה, וזו הסיבה שהוא מאתחל את תהליך ה-VPN במקום את ה-container עצמו.

בדיקת דליפת DNS לפני מתן אמון בהגדרה

מערכת ה-DNS ‏(Domain Name System) היא הדליפה ששורדת מנהרה תקינה. Gluetun מריץ פותר (resolver) משלו בתוך ה-namespace ומעביר שאילתות באמצעות DoT ‏(DNS over TLS) ל-Cloudflare כברירת מחדל: DNS_UPSTREAM_RESOLVER_TYPE=dot ו-DNS_UPSTREAM_RESOLVERS=cloudflare. השאירו את שניהם ללא שינוי, והשאילתות שלכם יוצפנו ויעברו דרך המנהרה.

ההגדרה שגורמת לכשל בכך היא DNS_UPSTREAM_PLAIN_ADDRESSES. משתמשים פונים אליה כאשר שם מתחם אינו מתורגם והם רוצים שהנתב או הפותר של ספק האינטרנט שלהם ישיב במקום. התיעוד של Gluetun מבהיר את המחיר: כל תעבורת ה-DNS לא תעבור דרך מנהרת ה-VPN ותדלוף מחוצה לה. התעבורה שלכם נשארת פרטית, אך רשימת שמות המתחם שאתם מבקרים בהם – לא. הגרסה של אותה טעות ב-WireGuard מכוסה ב-DNS שמפסיק לתרגם שמות מעל מנהרת WireGuard.

כדי לבדוק זאת, הגדירו את HTTPPROXY=on ב-Gluetun וחשפו את 8888:8888/tcp, לאחר מכן כוונו דפדפן אל ה-proxy הזה וטענו אתר לבדיקת דליפות DNS. התוצאה צריכה להציג את ספק ה-VPN שלכם או את Cloudflare, ולעולם לא את הנתב הביתי שלכם. התיעוד של Gluetun מזהיר כי חלק מבדיקות הדליפה מציגות תוצאות מוזרות, כיוון שהפותר בתוך ה-namespace הוא מתווך מטמון מקומי ולא השרת שמשיב בפועל. התייחסו לזיהוי מדינה שגויה או לפותר של ספק האינטרנט שלכם כאל אינדיקציה ברורה לבעיה.

הוספת Tailscale לצד ה-VPN sidecar, ומי מהם מקבל עדיפות

Tailscale היא רשת overlay המבוססת על WireGuard המאפשרת גישה למכונות שלכם, ומשתמשים מריצים אותה לצד VPN של ספק כדי לשמור על נתיב ניהול לתוך ה-stack. השניים כמעט לעולם לא מתנגשים, ויש לכך סיבה שחשוב להבין. התיעוד של Tailscale מציג את ברירת המחדל: היא פועלת כרשת overlay, היא מנתבת תעבורה רק בין התקנים שמריצים Tailscale, והיא אינה נוגעת בתעבורת האינטרנט הציבורית שלכם.

לכן, התשובה תלויה בהגדרה אחת.

  • Tailscale במכולה משלה, בתצורת ברירת מחדל: היא לעולם אינה רואה את התעבורה היוצאת של היישום. Gluetun נושא את כולה. Tailscale מגיעה ליישום ב-gluetun:8080, בדיוק כמו כל מכולה חיצונית אחרת.
  • Tailscale מחוברת ל-namespace של gluetun עם network_mode: "service:gluetun": היא זקוקה ל-cap_add משלה של net_admin ו-net_raw, מכיוון שהרשאות (capabilities) אינן עוברות יחד עם ה-namespace. במצב ברירת המחדל של userspace networking, כאשר TS_USERSPACE מופעל, tailscaled אינה יוצרת ממשק כלל ועובדת כ-SOCKS5 או כ-HTTP proxy, לכן היא אינה יכולה לשנות ניתוב. Gluetun עדיין נושא את הכל.
  • אותו מצב, עם TS_USERSPACE=false: tailscaled יוצרת התקן tunnel ומתקינה נתיבים, אך רק עבור טווח ה-tailnet ב-100.64.0.0/10 בתוספת כל נתיבי ה-subnet שאתם מפרסמים עם TS_ROUTES. תעבורה ציבורית עדיין יוצאת דרך gluetun.
  • כל אחד מהמצבים לעיל עם exit node שנבחר, sudo tailscale set --exit-node=<exit-node-ip>: Tailscale תובעת את נתיב ברירת המחדל ומנצחת. אל תשלבו זאת עם gluetun. נתיב ברירת מחדל אחד, בעלים אחד.

תופעת לוואי אחת נראית לעין כאשר Tailscale רצה בתוך ה-tunnel. העמיתים (peers) שלה רואים את הכתובת של ספק ה-VPN, לכן צפו שהיא תעבור לשימוש ב-relays בתדירות גבוהה יותר. tailscale status מציג relay "..." לצד עמית במקום direct כאשר זה קרה. החיבור עובד והוא איטי יותר. אם ה-overlay הוא הדבר היחיד שהייתם צריכים בפועל, ההבדל בין WireGuard רגיל ל-Tailscale הוא נקודת התחלה טובה יותר.

מה משתבש, ואיזו הודעה תראו

Docker מסרב ליצור את ה-container של היישום. Error response from daemon: conflicting options: port publishing and the container type network mode משמעותו שבלוק ports: עדיין מוגדר בשירות המחובר. העבירו אותו ל-gluetun.

Compose מסרב לקרוא את כל הקובץ. שירות לא יכול להגדיר גם את network_mode וגם את networks בו-זמנית. הגדירו את ה-networks בתוך gluetun.

-container אחר אינו מצליח לבצע resolution ליישום. curl: (6) Could not resolve host: qbittorrent הוא התנהגות תקינה, כיוון שה-container המחובר לא הצטרף לאף רשת ולא רשם שם. השתמשו ב-gluetun ובפורט המתאים.

ה-container השני המחובר לא עולה. שני תהליכים באותו namespace לא יכולים להאזין (bind) לאותו פורט, והתהליך שנכשל ידווח שהכתובת כבר בשימוש. שנו את הפורט הפנימי של היישום, או הריצו מופע שני של gluetun.

ליישום אין רשת לאחר שביצעתם שינוי ב-gluetun. הפעלה מחדש או יצירה מחדש של gluetun מנתקת את הקישוריות לכל מה שמחובר אליו. הפעילו מחדש את ה-containers הללו.

דפים קטנים נטענים וגדולים נתקעים. זוהי בעיית MTU (maximum transmission unit). המנהרה (tunnel) מוסיפה תקורה (overhead), ורכיב כלשהו בנתיב משליך חבילות גדולות מדי מבלי לשלוח הודעת שגיאה בחזרה. הנמיכו את WIREGUARD_MTU, נסו את 1400, ולאחר מכן את 1320.

gluetun לעולם לא מגיע למצב healthy. בדיקת העלייה מציינת את החשודים העיקריים: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. בדקו אם המפתח פג תוקף, לאחר מכן בדקו אם רשימת השרתים מיושנת, ולבסוף בדקו אם ה-firewall של המארח חוסם תעבורת UDP יוצאת.

FAQ

מדוע הפורטים שפרסמתי עבור המכולה הפסיקו לעבוד מאחורי Gluetun?

מכיוון ש-network_mode: "service:gluetun" מציב את המכולה בתוך ה-network namespace של Gluetun, ולכל namespace יש כתובת IP אחת וסט פורטים מאזינים אחד. היישום ממשיך להאזין, אך חוק ה-publish חייב להיות מוגדר על המכולה שמחזיקה ב-namespace. העבירו את רשימת ה-ports: לשירות ה-Gluetun. אם תותירו אותם על השירות המצורף, Docker לא ייצור אותם כלל: Error response from daemon: conflicting options: port publishing and the container type network mode.

כיצד אוכל לגשת למכולה שנמצאת בתוך מנהרת ה-VPN ממכולה שנמצאת מחוצה לה?

השתמשו בשם השירות של Gluetun ובפורט שבו היישום מאזין, למשל gluetun:8080. המכולה המצורפת אינה נמצאת באף רשת Docker משל עצמה, ולכן השם שלה לעולם לא יתורגם (resolve). אין צורך לבצע publish עבור תעבורה בין מכולות. בכיוון ההפוך, מכולה בתוך ה-namespace ניגשת למכולה חיצונית לפי שם השירות שלה, למשל postgres:5432, בגרסאות Gluetun v3.41 ומעלה. לקוח ברשת משנה (subnet) אחרת, כמו מחשב נייד ב-LAN שלכם, ייחסם על ידי ה-firewall של Gluetun עד שתוסיפו את רשת המשנה הזו ל-FIREWALL_OUTBOUND_SUBNETS.

האם Gluetun מתפקד כ-kill switch כאשר ה-VPN מתנתק?

כן, משתי סיבות בו-זמנית. למכולה המצורפת אין נתיב (route) מלבד זה שב-namespace המשותף, כך שמנהרה שקרסה מותירה אותה ללא מוצא מהמכונה. ה-firewall של Gluetun גם מאפשר תעבורה יוצאת רק דרך המנהרה ואל נקודת הקצה של שרת ה-VPN. לאחר מכן Gluetun מאתחל את ה-VPN באופן פנימי, תוך רישום WARN [vpn] restarting VPN because it failed to pass the healthcheck בלוג, במקום לצאת מהתהליך, כיוון שכל מכולה מצורפת מאבדת את הרשת שלה כאשר Gluetun עצמו מבצע אתחול.

Tailscale ו-Gluetun באותו stack: מי מהם נושא בתעבורה היוצאת?

Gluetun, בכל תצורה למעט אחת. כברירת מחדל, Tailscale מנתב תעבורה רק בין התקנים בתוך ה-tailnet שלכם ומותיר תעבורה ציבורית ללא שינוי. במצב ה-userspace המוגדר כברירת מחדל בתמונת המכולה, הוא אינו יוצר ממשק כלל, ולכן אינו יכול להשפיע על הניתוב. עם TS_USERSPACE=false הוא מתקין נתיבים עבור 100.64.0.0/10 ועבור רשתות המשנה שפרסמתם בלבד. היוצא מן הכלל הוא exit node: הגדרת sudo tailscale set --exit-node=<exit-node-ip> הופכת את Tailscale לנתיב ברירת המחדל, ואז הוא גובר. בחרו מוצר אחד שיחזיק בנתיב ברירת המחדל במקום להערים את שניהם.