איך לנתב מכולות Docker דרך VPN בלי לאבד פורטים
הפורטים נעלמים עם Gluetun ו־network_mode: service:gluetun? הסבירו את מרחב השמות המשותף וקבלו קובץ Compose עובד, כולל פתרון לשגיאת ports.
מדוע הפורטים נעלמים כאשר מנתבים מכולות Docker דרך VPN
כדי לנתב מכולות Docker דרך VPN, נותנים למכולה אחת את המנהרה, ולאחר מכן מחברים את שאר המכולות ל־network namespace שלה באמצעות network_mode: "service:gluetun". החיבור הזה הוא החלק שמפתיע משתמשים רבים. למכולה המחוברת כבר אין רשת משלה, ולכן גם הפורטים שפורסמו ושם השירות שלה ב־Docker נעלמים. מפרסמים את הפורטים במכולת ה־VPN, ומכולות אחרות ניגשות ליישום באמצעות השם של מכולת ה־VPN.
אם משאירים בלוק ports: במכולה המחוברת, Docker מסרב ליצור אותה:
Error response from daemon: conflicting options: port publishing and the container type network modeהכלי שבו משתמשים כאן הוא Gluetun — מכולה שמתחברת לספק VPN מסחרי (רשת פרטית וירטואלית) באמצעות WireGuard או OpenVPN, ומפעילה firewall משלה. גרסת v3.41.3 היא הגרסה הנוכחית נכון ל־August 2026. בדוגמאות נעשה שימוש ב־Mullvad עם WireGuard, ולכן נדרש חשבון ומפתח מהספק. אם אתם מעדיפים לסיים את המנהרה בחומרה שבבעלותכם, הפעלת שרת WireGuard משלכם ב־VPS מקימה את הצד השני, ו־wg-easy ב־Docker עוטף אותו בממשק אינטרנטי.
מה network_mode: "service:gluetun" עושה בפועל
כל מכולת Docker מקבלת בדרך כלל מרחב שמות רשת משלה: ממשקים, טבלת ניתוב, כללי firewall ו־sockets מאזינים משלה. מצב service: מדלג על השלב הזה ומפעיל את המכולה בתוך מרחב השמות של gluetun. מרחב שמות אחד פירושו כתובת IP אחת, והדבר משנה שישה היבטים.
- ליישום אין כתובת משלו. הכתובת שלו היא הכתובת של gluetun.
- היישום אינו מחובר לאף רשת Docker, ולכן שם השירות שלו אינו נרשם ואינו נפתר. מכולות אחרות חייבות להשתמש ב־
gluetun. - מכולות בתוך אותו מרחב שמות מתקשרות זו עם זו באמצעות
localhost. - שתי מכולות באותו מרחב שמות אינן יכולות להאזין לאותו פורט. התיעוד של Gluetun חד־משמעי בנושא זה: אין לכך פתרון עוקף.
- capabilities שייכות למכולה ולא למרחב השמות. Gluetun מחזיק ב־
NET_ADMINוב־/dev/net/tun, משום שהוא יוצר את ממשק המנהרה. המכולה המחוברת אינה יורשת אותן. - 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 -30docker 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 עדיין מציג כל משתנה סביבה לכל מי שיכול לגשת ל־Docker socket. קובצי סביבה וסודות ב־Docker Compose סוקר אפשרויות חזקות יותר.
איך מכולה מחוץ למנהרה מתקשרת עם מכולה שנמצאת בתוכה
שני כיווני התקשורת פועלים, וכל אחד מהם משתמש בשם אחר. שתי המכולות זקוקות לרשת Docker משותפת. במקרה זה מדובר ברשת של gluetun, משום שלמכולה המחוברת אין רשת משלה. כיצד רשתות Docker מוגדרות ב־Docker Compose מסביר את הגדרות ברירת המחדל.
מתקשורת מחוץ למנהרה אל מכולה שבתוכה, יש להשתמש בשם של gluetun ובפורט שבו היישום מאזין. מכולת reverse proxy מגיעה לממשק האינטרנט של qBittorrent דרך gluetun:8080. אין צורך בהגדרת ports:, משום שהתעבורה בין המכולות נשארת ברשת Docker ואינה עוברת דרך פורט של ה־host.
מתקשורת מתוך המנהרה החוצה, יש להשתמש בשם השירות של המכולה האחרת, למשל postgres:5432. מאז v3.41, Gluetun פותר שמות של מכולות אחרות מתוך ה־namespace שלו. לכן יש לקבע גרסה זו או גרסה חדשה יותר אם שם מסוים אינו נפתר.
חומת האש של Gluetun קובעת מי רשאי לפתוח אליו חיבור. תעבורה מהרשת של gluetun עצמו ב־Docker מותרת. לקוח מתת־רשת אחרת, מחשב נייד ברשת ה־LAN או מכולה ברשת bridge נפרדת, ייחסם עד שתציינו את תת־הרשת:
FIREWALL_OUTBOUND_SUBNETS=192.168.1.0/24המשמעות המתועדת מדויקת: רשימה של תת־רשתות המופרדות בפסיקים, ש־Gluetun והמכולות המשתפות את מחסנית הרשת שלו רשאים לגשת אליהן.
חיבורים נכנסים מהאינטרנט הם בעיה נפרדת. עמיתים של לקוח torrent מגיעים מצד ה־VPN, ולכן פרסום פורט 6881 ב־host אינו מועיל להם. יש צורך בפורט שהספק העביר עבורכם, ובציון הפורט הזה ב־FIREWALL_VPN_INPUT_PORTS, שמאפשר פורטים מצד שרת ה־VPN. זהו החלק שרוב מחסניות המדיה שנבנו באמצעות Docker Compose משאירות לא תקין.
מתג החירום: מה קורה כאשר המנהרה מתנתקת
התבנית הזאת מצדיקה את המורכבות שלה במצבי כשל. למכולה המחוברת אין נתיב חלופי. הנתיב היחיד שלה אל מחוץ למכונה עובר ב־namespace המשותף לה, ולכן כאשר המנהרה מושבתת אין נתיב חלופי. ה־firewall של Gluetun אוכף את אותו כלל מהצד השני: תעבורה יוצאת עוברת דרך המנהרה או אל נקודת הקצה של שרת ה־VPN, וכל השאר נזרק. אין חלון זמן שבו מנות דולפות דרך הממשק הרגיל בזמן שהלקוח מתחבר מחדש.
Gluetun מנטר את החיבור שלו. פעם בדקה הוא שולח echo של ICMP (פקודת ping) לכתובות שב־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 כולל בדיקת בריאות של Docker:
HEALTHCHECK --interval=5s --timeout=5s --start-period=10s --retries=1 CMD /gluetun-entrypoint healthcheckהפקודה מפעילה עותק קצר־חיים נוסף של gluetun, שבודק את שרת הבריאות של העותק הפעיל ב־http://127.0.0.1:9999/. Tunnel תקין מחזיר 200 OK. Tunnel תקול מחזיר 500 Internal server error עם מחרוזת שגיאה, וה־container מסומן כלא תקין לאחר כשל יחיד.
condition: service_healthy הוא המנגנון שממתין לכך. depends_on: [gluetun] רגיל ממתין רק להפעלת ה־container. פעולה זו מתרחשת כמה שניות לפני השלמת ה־handshake. לכן היישום עולה לרשת שאינה פעילה, ולעיתים קרובות מוותר על ניסיון החיבור הראשון שלו. בדיקות בריאות ב־Docker Compose מסביר את התחביר ואת שדות התזמון.
יש מגבלה אחת שלעתים אינה ברורה. Compose בודק את התנאי פעם אחת, בעת יצירת ה־container. הוא אינו עוצר או מפעיל מחדש את היישום אם gluetun הופך ללא תקין בהמשך. מנגנון ה־auto-healing הפנימי של gluetun מטפל במקרה הזה. לכן הוא מפעיל מחדש את תהליך ה־VPN, ולא את ה־container.
בדקו דליפת DNS לפני שאתם סומכים על התצורה
DNS (מערכת שמות המתחם) הוא הדלף שנשאר גם כאשר המנהרה מוגדרת כראוי. Gluetun מפעיל resolver משלו בתוך ה־namespace ומעביר שאילתות באמצעות DoT (DNS over TLS) אל Cloudflare כברירת מחדל: DNS_UPSTREAM_RESOLVER_TYPE=dot ו־DNS_UPSTREAM_RESOLVERS=cloudflare. השאירו את שניהם ללא שינוי, והשאילתות שלכם יהיו מוצפנות ויעברו דרך המנהרה.
ההגדרה שגורמת לבעיה היא DNS_UPSTREAM_PLAIN_ADDRESSES. משתמשים בה כאשר שם אינו נפתר, ורוצים שהנתב או ה־resolver של ספק האינטרנט יענה במקום זאת. התיעוד של Gluetun מציין את המחיר במפורש: כל תעבורת ה־DNS לא תעבור דרך מנהרת ה־VPN, אלא תדלוף מחוץ לה. התעבורה שלכם נשארת פרטית. רשימת שמות המתחם שאליהם אתם פונים אינה נשארת פרטית. הגרסה המקבילה של אותה טעות ב־WireGuard מתוארת ב־DNS שמפסיק להיפתר דרך מנהרת WireGuard.
כדי לבדוק זאת, הגדירו ב־gluetun את HTTPPROXY=on ופרסמו את 8888:8888/tcp, ולאחר מכן הפנו דפדפן אל ה־proxy הזה ופתחו בדיקת דליפות DNS. התוצאה צריכה לציין את ספק האינטרנט שלכם או את Cloudflare, ולעולם לא את הנתב הביתי שלכם. התיעוד של Gluetun עצמו מזהיר שבדיקות דליפה מסוימות עשויות להציג תוצאות חריגות, משום שה־resolver בתוך ה־namespace הוא מתווך מקומי ששומר תוצאות במטמון, ולא השרת שמספק לבסוף את התשובה. תוצאה שמציינת מדינה שגויה או את ה־resolver של ספק האינטרנט שלכם היא האינדיקציה החשובה באמת.
הוספת Tailscale לצד VPN sidecar, ואיזה מהם גובר
Tailscale היא רשת overlay המבוססת על WireGuard, שמאפשרת גישה למחשבים שבבעלותכם. משתמשים בה לצד VPN של ספק כדי לשמור נתיב ניהול אל המערכת. שתי המערכות כמעט לעולם אינן מתנגשות, ויש לכך סיבה שחשוב להבין. בתיעוד של 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 אינו יוצר ממשק כלל ופועל כ־proxy מסוג SOCKS5 או HTTP, ולכן אינו יכול לשנות את הניתוב. Gluetun עדיין מעביר את כל התעבורה. - אותה תצורה, עם
TS_USERSPACE=false: tailscaled יוצר התקן tunnel ומתקין נתיבים, אך רק עבור טווח ה־tailnet100.64.0.0/10, וכן עבור נתיבי subnet שאתם מפרסמים באמצעותTS_ROUTES. התעבורה הציבורית עדיין יוצאת דרך gluetun. - כל אחת מהתצורות לעיל עם exit node שנבחר,
sudo tailscale set --exit-node=<exit-node-ip>: Tailscale תופסת את נתיב ברירת המחדל וגוברת. אין לשלב תצורה זו עם gluetun. נתיב ברירת מחדל אחד, ובעלות אחת עליו.
אם הנתיבים המפורסמים הם המטרה, ואתם רוצים שכל הרשת הפרטית שמאחורי השרת תהיה נגישה, ולא רק השרת עצמו, הפעלת נתב subnet של Tailscale ב־VPS מסבירה כיצד לאשר את הנתיב, להפעיל IP forwarding ולהגדיר בצד הלקוח את הדגל ש־TS_ROUTES אינו מגדיר לבדו.
אם Tailscale נועדה לספק לכם כתובת URL לניהול ולא נתיב, tailscale serve ו־tailscale funnel מציבים HTTPS לפני gluetun:8080 עבור ה־tailnet שלכם, כאשר רק funnel פותח אותו לאינטרנט הציבורי.
תופעת לוואי אחת ניכרת כאשר Tailscale פועלת בתוך ה־tunnel. העמיתים שלה רואים את הכתובת של ספק ה־VPN, ולכן יש לצפות שהיא תעבור ל־relays בתדירות גבוהה יותר. tailscale status מציג relay "..." לצד עמית במקום direct כאשר הדבר קורה. החיבור פועל, אך הוא איטי יותר. אם ה־overlay הוא למעשה הדבר היחיד שנחוץ לכם, ההבדל בין WireGuard רגיל לבין Tailscale הוא נקודת פתיחה טובה יותר.
מה מתקלקל ומהי ההודעה שתראו
Docker מסרב ליצור את מכולת היישום. Error response from daemon: conflicting options: port publishing and the container type network mode פירושו שבלוק ports: עדיין משויך לשירות המצורף. העבירו אותו אל gluetun.
Compose מסרב לקבל את הקובץ כולו. שירות אינו יכול להגדיר גם network_mode וגם networks. הגדירו את הרשתות ב־gluetun.
מכולה אחרת אינה מצליחה לפתור את שם היישום. curl: (6) Could not resolve host: qbittorrent הוא ההתנהגות הנכונה, משום שהמכולה המצורפת לא הצטרפה לאף רשת ולא רשמה שם. השתמשו ב־gluetun ובפורט.
המכולה המצורפת השנייה אינה עולה. שני תהליכים באותו namespace אינם יכולים להאזין לאותו פורט. התהליך שהפסיד מדווח שהכתובת כבר בשימוש. שנו את הפורט הפנימי של היישום, או הפעילו gluetun נוסף.
ליישום אין רשת לאחר ששיניתם את gluetun. אתחול או יצירה מחדש של gluetun מנתקים את הקישוריות מכל המכולות המשויכות אליו. אתחלו את המכולות האלה.
דפים קטנים נטענים ודפים גדולים נתקעים. זו בעיית MTU (יחידת ההעברה המרבית). המנהרה מוסיפה תקורה, ורכיב כלשהו בנתיב משליך את החבילות הגדולות מדי בלי להחזיר שגיאה. הקטינו את WIREGUARD_MTU, נסו את 1400 ולאחר מכן את 1320.
Gluetun לעולם אינו נעשה תקין. בדיקת האתחול מציינת את החשודים הראשונים: WARN [vpn] restarting VPN because it failed to pass the healthcheck: startup check: dialing: dial tcp4: lookup cloudflare.com: i/o timeout. בדקו אם המפתח פג תוקף, לאחר מכן אם רשימת השרתים מיושנת, ולבסוף אם חומת האש של המארח חוסמת תעבורת UDP יוצאת.
FAQ
מדוע הפורטים שפרסמתי עבור המכולה הפסיקו לפעול מאחורי Gluetun?
משום ש־network_mode: "service:gluetun" מכניס את המכולה למרחב שמות הרשת של gluetun, ולמרחב שמות יש כתובת IP אחת ומערכת אחת של פורטים מאזינים. היישום ממשיך להאזין, אך כלל הפרסום חייב להיות מוגדר במכולה שבבעלותה מרחב השמות. העבירו את רשימת ports: לשירות gluetun. אם השארתם אותה בשירות המחובר, Docker אפילו לא ייצור אותה: Error response from daemon: conflicting options: port publishing and the container type network mode.
כיצד אפשר להגיע ממכולה שמחוץ למנהרת ה־VPN אל מכולה שבתוכה?
השתמשו בשם השירות של gluetun ובפורט שבו היישום מאזין, לדוגמה gluetun:8080. למכולה המחוברת אין רשת Docker משלה, ולכן השם שלה לעולם לא ייפתר. אין צורך לפרסם פורטים עבור תעבורה בין מכולות. בכיוון ההפוך, מכולה שבתוך מרחב השמות יכולה להגיע למכולה חיצונית באמצעות שם השירות שלה, למשל postgres:5432, ב־Gluetun v3.41 ומעלה. לקוח ברשת משנה אחרת, כגון מחשב נייד ברשת ה־LAN שלכם, ייחסם על ידי חומת האש של gluetun עד שתוסיפו את רשת המשנה הזו ל־FIREWALL_OUTBOUND_SUBNETS.
האם Gluetun פועל כמתג ניתוק כאשר ה־VPN מתנתק?
כן, וזאת משתי סיבות בו־זמנית. למכולה המחוברת אין נתיב מלבד הנתיב שבמרחב השמות המשותף, ולכן מנהרה שאינה פעילה משאירה אותה ללא נתיב אל מחוץ למחשב. חומת האש של 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 לנתיב ברירת המחדל, ואז הוא גובר. בחרו מוצר אחד שינהל את נתיב ברירת המחדל, במקום להפעיל את שניהם זה על גבי זה.