איך לגשת למכולות מאחורי Gluetun ב-Docker
מכולה המשתמשת ב-network_mode של Gluetun אינה מחזיקה בממשקי רשת משלה. למדו כיצד לפרסם פורטים ישירות ב-Gluetun ולהגדיר חוקי firewall כדי לאפשר תקשורת תקינה בין שירותים.
מה קורה כשמכולה מצטרפת לרשת של gluetun
למכולה המגדירה network_mode: service:gluetun אין ממשקי רשת משלה. היא מצטרפת ל-network namespace של gluetun, ולכן פרסום פורטים וחוקי firewall מפסיקים להיות מאפיינים של אותה מכולה והופכים למאפיינים של שירות ה-gluetun. כל תשובה להלן נובעת מעובדה יחידה זו.
network namespace הוא עותק פרטי של ה-kernel למחסנית רשת: ממשקים משלו, טבלת ניתוב משלו, חוקי firewall משלו ו-sockets מאזינים משלו. Docker מעניק לכל מכולה אחד כזה כברירת מחדל. כאשר כותבים network_mode: service:gluetun, Docker מדלג על שלב זה ומציב את המכולה החדשה בתוך ה-namespace ש-gluetun כבר מחזיק. המכולה שומרת על מערכת הקבצים שלה ועל קובץ ה-/etc/hosts שלה, והקובץ השני חשוב בהמשך.
ניתן לראות זאת ישירות.
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrentפקודה זו מדפיסה container: ואחריו את מזהה המכולה של gluetun, בעוד שמכולה רגילה תדפיס bridge. מדריך זה ממשיך מהנקודה שבה ניתוב תעבורת Docker דרך VPN עם gluetun נעצר: המנהרה פועלת, וכעת שום דבר לא יכול לתקשר עם המכולה.
פרסום הפורט ב-gluetun, לא ביישום
השארת בלוק ports: בשירות המגדיר network_mode תגרום לכך ש-Docker יסרב ליצור את המכולה:
Error response from daemon: conflicting options: port publishing and the container type network modeהסיבה לכך ישירה. פרסום פורט משמעו הוספת חוק NAT (תרגום כתובות רשת) המעביר פורט של המארח אל מרחב השמות (namespace) של הרשת בתוך המכולה, אך למכולה זו אין מרחב כזה. העבירו את המיפוי לשירות gluetun. מספר הפורט אינו משתנה, כיוון שהיישום עדיין מאזין לאותו פורט בתוך מרחב השמות המשותף.
services:
gluetun:
ports:
- "8080:8080/tcp" # qBittorrent web UI
qbittorrent:
network_mode: "service:gluetun"
# no ports: block hereבלוק expose: בשירות התלוי הוא חסר תועלת באותה מידה, ובלוק networks: שם מהווה עצירה מוחלטת: Compose מדווח שהשירות מצהיר על network_mode ו-networks שסותרים זה את זה, ומסרב לטעון את הקובץ כלל.
השלכה אחת מורגשת בשלב מאוחר יותר. כל מכולה במרחב השמות חולקת מרחב פורטים יחיד, לכן שני יישומים ששניהם מאזינים כברירת מחדל ל-8080 יתנגשו, והשני שביניהם ינסה לעלות ייכשל עם שגיאת address already in use. שנו את אחד מהם בתצורה שלו, למשל באמצעות המשתנה WEBUI_PORT בתמונת qBittorrent של LinuxServer, ולאחר מכן פרסמו את המספר החדש ב-gluetun.
כיצד מכולות מאחורי gluetun מתקשרות זו עם זו?
בתוך ה-namespace הן כבר חולקות ממשק loopback. מכולה שנמצאת מאחורי gluetun מגיעה למכולה אחות ב-127.0.0.1:<port> ללא מעורבות של רשת Docker.
מחוץ ל-namespace, למכולה אין שם. ה-DNS המוטמע של Docker מתרגם שם שירות לכתובת של אותו שירות ברשת שהוגדרה על ידי המשתמש, אך למכולה זו אין כתובת באף רשת. לכן, מכולה רגילה כמו Sonarr לא מגיעה ללקוח הטורנטים ב-http://qbittorrent:8080. היא מגיעה אליו ב-http://gluetun:8080, כיוון שה-socket מאזין בתוך ה-namespace של gluetun, בכתובת של gluetun. זה מפתיע אנשים שמכירים את אופן הפעולה של רשתות ושמות שירותים ב-Docker Compose ומצפים שכללי השיום הרגילים יחולו. זה עובד גם ללא פרסום פורטים למארח (host), כיוון ששתי המכולות נמצאות על אותה רשת Compose.
בדקו את ה-DNS לפני שאתם מנפים שגיאות בכל דבר אחר. gluetun מריץ פותר (resolver) משלו וכותב מחדש את /etc/resolv.conf בתוך המכולה שלו, אך /etc/resolv.conf הוא קובץ פר-מכולה, לכן הקובץ ש-gluetun כתב אינו הקובץ שהיישום שלכם קורא.
docker exec qbittorrent cat /etc/resolv.confכיצד אוכל להגיע לשירות שרץ על ה-host של Docker?
השתמשו ב-host.docker.internal. נדרשות שתי הגדרות בשני מקומות שונים, כיוון ששני דברים שונים אינם מוגדרים כראוי.
השם מופיע ראשון. /etc/hosts מוגדר לכל מכולה (container), לכן הערך extra_hosts שייך למכולת היישום, ולא ל-gluetun.
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway הוא ערך מיוחד ש-Docker מחליף בכתובת פנימית של ה-host עצמו. בהתקנת Docker סטנדרטית על Linux, זוהי הכתובת של ה-bridge מסוג docker0, שבדרך כלל היא 172.17.0.1. אשרו את הכתובת שלכם באמצעות ip -4 addr show docker0 ב-VPS. גרסת Docker Desktop פותרת את השם הזה באופן עצמאי, וזו הסיבה שמדריכים שנכתבו על מחשבים ניידים מדלגים על השורה extra_hosts, מה שגורם לקובץ להיכשל בשרת.
הניתוב מופיע שני. הוספת השם רק מורה למכולה באיזו כתובת להשתמש. החבילה (packet) עדיין יוצאת דרך נתיב ברירת המחדל של gluetun, שהוא ה-tunnel, וה-firewall של gluetun חוסם אותה. הסימפטום הוא חיבור שנתקע ומסתיים ב-timeout, במקום חיבור שנדחה (refused). דחייה משמעותה שהחבילה הגיעה ומשהו ענה "לא". timeout משמעותו שהחבילה מעולם לא הגיעה.
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32לאחר מכן, ודאו ששירות ה-host אכן מאזין בכתובת הזו. שרת PostgreSQL שמוגדר להאזין רק ל-127.0.0.1 אינו נגיש מאף מכולה, ללא קשר ל-tunnel, כיוון ש-127.0.0.1 בתוך ה-namespace הוא ה-loopback של ה-namespace עצמו. הגדירו אותו להאזין ל-172.17.0.1 במקום זאת: כך הוא יקבל חיבורים ממכולות מבלי להיחשף לממשק הציבורי. אשתו זאת באמצעות ss -lntp | grep 5432 ב-host.
מה ההגדרה FIREWALL_OUTBOUND_SUBNETS משנה בפועל
התיעוד של gluetun מתאר זאת כרשימת רשתות משנה (subnets) מופרדות בפסיקים, ש־gluetun והמכולות החולקות איתו את מחסנית הרשת מורשים לגשת אליהן. התיעוד מציין כי הדבר כרוך בשינויים ב־firewall ובניתוב. שני החלקים חשובים. gluetun מוסיף נתיב (route) לכל רשת משנה המפורטת דרך ה-gateway של ה-Docker bridge, כך שחבילות מידע עבור כתובות אלו יוצאות דרך eth0 במקום דרך ה-tunnel. בנוסף, הוא פותח עבורן את ה-firewall, שכן ללא הגדרה זו, gluetun משליך (drops) תעבורה יוצאת שאינה מיועדת לשרת ה-VPN.
יש לכתוב את הערך ללא רווחים אחרי הפסיקים.
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32קל לפספס שתי תכונות. זוהי הגדרה ברמת ה-namespace, לכן היא חלה על כל מכולה שנמצאת מאחורי gluetun, ולא רק על זו שהתכוונת אליה. כמו כן, מדובר בתעבורה יוצאת בלבד: ההגדרה קובעת אילו חיבורים מכולה יכולה ליזום. חיבורים שמגיעים לפורט שפורסם (published port) עוברים בנתיב שונה ואינם זקוקים לרישום כאן.
גישה לממשק הניהול מתוך עמית Tailscale
Tailscale מעניקה לכל מכונה כתובת בטווח 100.64.0.0/10, הטווח השמור ל-carrier grade NAT. שני כיווני התקשורת דורשים טיפול שונה.
תעבורה נכנסת היא הפשוטה מבין השתיים. פרסום 8080:8080 ב-gluetun מבצע bind לפורט זה בכל הכתובות של המארח, וממשק ה-tailscale0 של המארח הוא אחת מהן; לכן, עמית פותח את http://<machine-name>:8080 ומגיע למכולה. ל-gluetun אין חלק בנתיב זה, כיוון שחוק ה-NAT של Docker יושב על המארח, מחוץ ל-namespace.
כדי להפוך את ממשק הניהול לנגיש רק דרך ה-tailnet, בצעו bind לפורט המפורסם לכתובת ה-Tailscale של המארח במקום לכל הכתובות.
ports:
- "100.101.102.103:8080:8080/tcp"מצאו את הכתובת הזו באמצעות tailscale ip -4 על המארח. ביצוע bind הוא אמצעי בקרה חזק יותר מחוק firewall במקרה זה, כיוון שהפורט כלל אינו נפתח בממשק הציבורי. פעולה זו גם עוקפת את הבעיה ב-פרסום פורטים ב-Docker שעוקף את ufw.
תעבורה יוצאת היא המקום שבו FIREWALL_OUTBOUND_SUBNETS חוזרת לתמונה. אם מכולה צריכה לפנות לעמית, הוסיפו את הכתובת של אותו עמית, והעדיפו /32 לכל עמית על פני ה-/10 כולה. שמות MagicDNS לא יתורגמו בתוך המכולה, כיוון שהמכולה אינה משתמשת ב-resolver של המארח; לכן, השתמשו בכתובת 100.x המספרית או קבעו אותה באמצעות שורת extra_hosts. אותו עיקרון חל כאשר אתם מריצים שרת בקרה עצמאי ל-Tailscale עם Headscale.
קובץ compose מלא עבור מבנה נפוץ
לקוח הורדות מאחורי VPN, שני ממשקי אינטרנט המאזינים רק בתוך ה-tailnet, ומכולה אחת שניגשת למסד נתונים PostgreSQL הרץ על המארח.
services:
gluetun:
image: qmcgaw/gluetun:latest
container_name: gluetun
cap_add:
- NET_ADMIN
devices:
- /dev/net/tun:/dev/net/tun
ports:
- "127.0.0.1:8000:8000/tcp" # gluetun control server, host only
- "100.101.102.103:8080:8080/tcp" # qBittorrent UI, tailnet only
- "100.101.102.103:9696:9696/tcp" # Prowlarr UI, tailnet only
volumes:
- ./gluetun:/gluetun
environment:
- VPN_SERVICE_PROVIDER=mullvad
- VPN_TYPE=wireguard
- WIREGUARD_PRIVATE_KEY=${WIREGUARD_PRIVATE_KEY}
- WIREGUARD_ADDRESSES=${WIREGUARD_ADDRESSES}
- SERVER_CITIES=Amsterdam
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,100.64.7.9/32
- TZ=Europe/Amsterdam
restart: unless-stopped
qbittorrent:
image: lscr.io/linuxserver/qbittorrent:latest
network_mode: "service:gluetun"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- WEBUI_PORT=8080
volumes:
- ./qbittorrent:/config
- /srv/downloads:/downloads
depends_on:
gluetun:
condition: service_healthy
restart: unless-stopped
prowlarr:
image: lscr.io/linuxserver/prowlarr:latest
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"
environment:
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
- PROWLARR__POSTGRES__HOST=host.docker.internal
- PROWLARR__POSTGRES__PORT=5432
- PROWLARR__POSTGRES__USER=prowlarr
- PROWLARR__POSTGRES__PASSWORD=${PROWLARR_DB_PASSWORD}
- PROWLARR__POSTGRES__MAINDB=prowlarr-main
- PROWLARR__POSTGRES__LOGDB=prowlarr-log
volumes:
- ./prowlarr:/config
depends_on:
gluetun:
condition: service_healthy
restart: unless-stoppedקראו את הקובץ כדי להבין את התבנית ולאו דווקא את שמות המוצרים. שני הממשקים מפורסמים דרך gluetun וקשורים לכתובת ה-tailnet של המארח, כך שהם מגיבים רק ב-Tailscale ולא בשום מקום אחר. רק Prowlarr כולל את השורה extra_hosts, כיוון ש-Prowlarr הוא המכולה שמבצעת resolve ל-host.docker.internal. הפרמטר FIREWALL_OUTBOUND_SUBNETS מציין שתי כתובות בודדות: כתובת ה-bridge של Docker במארח, כדי ש-Prowlarr יוכל לפתוח חיבור למסד הנתונים, וכתובת של עמית אחד ב-tailnet.
שרת ה-PostgreSQL נעדר במכוון מהקובץ. הוא רץ על ה-VPS כשירות מערכת רגיל המאזין ב-172.17.0.1:5432. זוהי אותה שכבתית כמו ב-arr stack על Docker Compose, כאשר מסד הנתונים מועבר אל מחוץ ל-Docker.
שמרו את ה-private key של WireGuard מחוץ לקובץ ה-compose. הפרמטר ${WIREGUARD_PRIVATE_KEY} קורא מקובץ .env הנמצא לצדו, תבנית המוסברת ב-env files and secrets for Docker Compose. הסעיף condition: service_healthy משתמש ב-healthcheck שמגיע מובנה עם ה-image של gluetun, כך ששום דבר לא מתחיל עד שהמנהרה מדווחת שהיא פעילה. Compose healthchecks מסבירים את המבנה הכללי.
פרסום בכל הכתובות במקום רק ב-tailnet
הסירו את קידומת הכתובת ואת ה-port binds ב-0.0.0.0, מה שיכלול את ה-IP הציבורי של ה-VPS. בצעו זאת רק מאחורי firewall שבשליטתכם, וקראו תחילה את ההערה על ufw לעיל.
ports:
- "8080:8080/tcp"אימות שהמנהרה אכן מעבירה תעבורה
הריצו את אותה בקשה פעמיים: פעם אחת מתוך ה-namespace ופעם אחת מה-host, ולאחר מכן השוו בין התוצאות.
docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.orgהפקודה הראשונה אמורה להציג את כתובת היציאה של ספק ה-VPN שלכם. השנייה אמורה להציג את כתובת ה-VPS. אם הכתובות זהות, התעבורה של ה-container אינה עוברת דרך המנהרה, וכל תיקון במדריך זה יהיה חסר משמעות עד שהבעיה הזו תיפתר.
טבלת הניתוב מציגה מה עובר דרך המנהרה ומה לא.
docker run --rm --network=container:gluetun alpine:3.22 ip route showנתיב ברירת המחדל (default route) צריך להצביע על ממשק המנהרה, tun0. מתחתיו אתם אמורים לראות נתיב אחד עבור כל רשומה ב-FIREWALL_OUTBOUND_SUBNETS, המצביע על ה-gateway של ה-Docker bridge. כל נתיב אחר שיוצא דרך eth0 מייצג תעבורה שעוקפת את ה-VPN.
שרת הבקרה של Gluetun מדווח על אותה כתובת IP ציבורית בפורט 8000, בכתובת /v1/publicip/ip. גרסאות עדכניות מחייבות הגדרת אימות עבור נתיבי שרת הבקרה, לכן הגדירו זאת לפני שתסתמכו על שירות זה.
הדליפה שיוצרת הגדרת subnet שגויה
FIREWALL_OUTBOUND_SUBNETS הוא חור שאתם יוצרים ב-firewall בכוונה, לכן גודל הסיכון תואם את גודל החור. ארבע דרכים להפוך אותו לגדול מדי:
0.0.0.0/0שולח את כל התעבורה מחוץ ל-tunnel. שתי בדיקות ה-IP שלעיל יזהו זאת בהרצה הראשונה, כיוון שהן יחזירו את אותה כתובת.- טווח רחב יותר מהיעד. פתיחת
10.0.0.0/8כדי להגיע למכונה אחת ב-10.0.1.7פותחת גם כל כתובת ש-torrent peer עשוי לפרסם בטווח הזה. הגדירו10.0.1.7/32. - טווח שחופף לכתובות ה-tunnel עצמו. התיעוד של gluetun מזהיר שזה גורם ל-gluetun לשלוח תעבורת VPN דרך ה-bridge במקום, מה שמשבש את ה-port forwarding. בדקו את הערך של
WIREGUARD_ADDRESSESלפני פתיחת טווח פרטי כלשהו. 100.64.0.0/10עבור Tailscale. מדובר בכארבעה מיליון כתובות שנפתחות כדי לאפשר גישה ל-peer אחד. רשמו את ה-peers הנחוצים לכם כערכי/32.
זכרו שההגדרה חלה על כל ה-namespace. פתיחת subnet כדי לאפשר ל-indexer להגיע ל-host service פותחת את אותו ה-subnet גם עבור ה-torrent client שמשתף את אותו ה-namespace. הריצו מחדש את בדיקת ה-IP הציבורי לאחר כל שינוי במשתנה זה, כיוון שזהו המבחן היחיד שמוכיח אם השינוי השיג את התוצאה הרצויה.
מה משתבש בעת הפעלה מחדש של gluetun
השירות gluetun מחזיק במרחב השמות (namespace), לכן מחזור החיים של gluetun הוא מחזור החיים של מרחב השמות כולו. הפעלה של מכולה תלויה בזמן ש-gluetun אינו פעיל תיכשל באופן מיידי:
Error response from daemon: cannot join network of a non running containerהפעלה מחדש של gluetun במקומו היא כשל שקט יותר. המכולות התלויות ממשיכות לרוץ בזמן שמרחב השמות שאליו הן מחוברות נבנה מחדש מתחתיהן, כך ש-docker ps מדווח שהכול תקין בעוד ששום דבר לא מגיב. לאחר כל שינוי בשירות gluetun, יש ליצור מחדש את כל הקבוצה במקום להפעיל מחדש רק חלק ממנה.
docker compose up -d --force-recreateאותו כלל תקף גם לעדכוני image. משיכת image חדש של gluetun ויצירה מחדש של שירות זה בלבד תשאיר את השירותים האחרים מצביעים על מרחב שמות שכבר אינו קיים.
FAQ
מדוע Docker מציג את השגיאה "port publishing and the container type network mode"?
מכיוון שבלוק ports: עדיין מוגדר בשירות שגם מגדיר network_mode: service:gluetun. פרסום פורט מוסיף חוק NAT המעביר פורט מהמארח (host) אל מרחב הרשת (network namespace) של המכולה, אך מכולה במצב זה אינה מחזיקה במרחב כזה. מחקו את בלוק ה-ports: מאותו שירות והוסיפו את המיפוי הזהה לשירות gluetun. מספר הפורט נשאר זהה, כיוון שהיישום עדיין מאזין עליו בתוך המרחב המשותף.
כיצד מכולות אחרות יכולות להגיע לשירות שנמצא מאחורי gluetun?
מכולות הנמצאות באותו מרחב רשת מגיעות זו לזו דרך 127.0.0.1. מכולות מחוץ למרחב משתמשות בשם השירות של gluetun, לכן http://gluetun:8080 עובד במקום שבו http://qbittorrent:8080 נכשל. מכולת היישום אינה מחזיקה בכתובת באף רשת Docker, ולכן לשרת ה-DNS הפנימי אין מה לפתור עבור שמה. אין צורך בפרסום פורטים עבור תקשורת זו, כל עוד שתי המכולות חולקות רשת Compose.
מה עלי להזין ב-FIREWALL_OUTBOUND_SUBNETS?
יש להזין רק את הכתובות שאליהן מכולה מאחורי gluetun חייבת ליזום חיבור, בצורה המצומצמת ביותר האפשרית. מכונה בודדת מוגדרת כ-/32. שני הערכים הנפוצים הם המארח של Docker ב-172.17.0.1/32 וכתובת /32 אחת עבור כל עמית (peer) ב-Tailscale שאליו אתם פונים. לעולם אל תוסיפו 0.0.0.0/0, ואל תוסיפו טווח שחופף לכתובות ה-tunnel של ה-VPN שלכם. חיבורים נכנסים לפורט שפורסם אינם זקוקים לערך כאן.
מדוע המכולה אינה מצליחה לפתור את שמות ה-MagicDNS של Tailscale?
MagicDNS פועל על ידי הפניית ה-resolver של המארח לשרת ה-DNS של Tailscale, והמכולה אינה משתמשת ב-resolver של המארח. היא משתמשת במה שמוגדר ב-/etc/resolv.conf שלה, אשר מאחורי gluetun הוא הגדרות ה-DNS של gluetun. אשרו זאת באמצעות docker exec <container> cat /etc/resolv.conf. השתמשו בכתובת 100.x המספרית של העמית, או קבעו את השם באמצעות ערך extra_hosts באותה מכולה.
כיצד אוכל לוודא שהתעבורה עדיין עוברת דרך ה-VPN?
בצעו בקשה אחת מתוך מרחב הרשת ואת אותה בקשה מהמארח, ולאחר מכן השוו את התשובות. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org אמור להחזיר את כתובת היציאה של ספק ה-VPN שלכם, בעוד ש-curl -s https://api.ipify.org על ה-VPS יחזיר את כתובת ה-VPS. שתי תשובות זהות מעידות על כך שה-tunnel אינו מעביר את תעבורת המכולה. בצעו בדיקה זו שוב לאחר כל שינוי ב-FIREWALL_OUTBOUND_SUBNETS.