Gluetun: Host und andere Container erreichen
Container hinter gluetun haben keine eigenen Interfaces. Veröffentlichen Sie Ports auf gluetun und erlauben Sie nur die benötigten Subnetze außerhalb des Tunnels.
Was passiert, wenn ein Container dem Netzwerk von gluetun beitritt
Ein Container, der network_mode: service:gluetun setzt, verfügt über keine eigenen Netzwerkschnittstellen. Er tritt dem Netzwerk-Namespace von gluetun bei. Deshalb sind Portfreigaben und Firewall-Regeln nicht mehr Eigenschaften dieses Containers, sondern des Dienstes gluetun. Alle folgenden Antworten ergeben sich aus dieser einen Tatsache.
Ein Netzwerk-Namespace ist eine private Kopie des Netzwerk-Stacks im Kernel. Er verfügt über eigene Schnittstellen, eine eigene Routing-Tabelle, eigene Firewall-Regeln und eigene Listening-Sockets. Docker weist standardmäßig jedem Container einen solchen Namespace zu. Wenn Sie network_mode: service:gluetun schreiben, überspringt Docker diesen Schritt und platziert den neuen Container in dem Namespace, den gluetun bereits besitzt. Der Container behält sein eigenes Dateisystem und seine eigene /etc/hosts-Datei. Letztere ist später relevant.
Sie können das direkt sehen.
docker inspect -f '{{.HostConfig.NetworkMode}}' qbittorrentDer Befehl gibt container: gefolgt von der Container-ID von gluetun aus. Bei einem normalen Container würde stattdessen bridge ausgegeben. Dieser Leitfaden setzt dort an, wo Docker-Verkehr über ein VPN mit gluetun routen endet: Der Tunnel funktioniert, und nun kann nichts mehr mit dem Container kommunizieren.
Port auf gluetun veröffentlichen, nicht auf der Anwendung
Wenn auf dem Dienst, der network_mode setzt, ein ports:-Block verbleibt, verweigert Docker die Erstellung des Containers:
Error response from daemon: conflicting options: port publishing and the container type network modeDer Grund ist direkt. Durch die Veröffentlichung eines Ports wird eine NAT-Regel (Network Address Translation) hinzugefügt, die einen Host-Port in den eigenen Netzwerk-Namespace eines Containers weiterleitet. Dieser Container besitzt jedoch keinen eigenen Netzwerk-Namespace. Verschieben Sie das Mapping auf den Dienst gluetun. Die Portnummer ändert sich nicht, weil die Anwendung weiterhin auf diesem Port innerhalb des gemeinsam genutzten Namespace lauscht.
services:
gluetun:
ports:
- "8080:8080/tcp" # qBittorrent web UI
qbittorrent:
network_mode: "service:gluetun"
# no ports: block hereEin expose:-Block auf dem abhängigen Dienst ist ebenfalls nutzlos. Ein networks:-Block dort verhindert den Start vollständig: Compose meldet, dass der Dienst die sich gegenseitig ausschließenden Optionen network_mode und networks deklariert, und verweigert das Laden der Datei vollständig.
Eine Folge davon zeigt sich später. Alle Container im Namespace teilen sich einen einzigen Portbereich. Daher kollidieren zwei Anwendungen, die beide standardmäßig Port 8080 verwenden. Die Anwendung, die als zweite startet, schlägt dann mit einem Fehler wegen einer bereits verwendeten Adresse fehl. Ändern Sie eine der Anwendungen in ihrer eigenen Konfiguration, beispielsweise die Variable WEBUI_PORT im LinuxServer-qBittorrent-Image, und veröffentlichen Sie anschließend die neue Nummer auf gluetun.
Wie erreichen Container hinter gluetun einander?
Innerhalb des Namespace teilen sie bereits eine Loopback-Schnittstelle. Ein Container hinter gluetun erreicht seinen zugehörigen Container unter 127.0.0.1:<port>, ohne dass ein Docker-Netzwerk beteiligt ist.
Außerhalb des Namespace hat der Container keinen Namen. Der integrierte DNS-Server von Docker löst einen Servicenamen in einem benutzerdefinierten Netzwerk in die Adresse dieses Dienstes auf. Dieser Container hat jedoch in keinem Netzwerk eine Adresse. Ein normaler Container wie Sonarr erreicht den Torrent-Client daher nicht unter http://qbittorrent:8080. Er erreicht ihn unter http://gluetun:8080, weil der Socket im Namespace von gluetun und unter dessen Adresse lauscht. Das überrascht Nutzer, die wissen, wie Docker-Compose-Netzwerke und Servicenamen funktionieren und erwarten, dass die übliche Namensauflösung hier ebenfalls gilt. Es funktioniert auch ohne Veröffentlichung eines Ports auf dem Host, da sich beide Container im selben Compose-Netzwerk befinden.
Prüfen Sie den DNS zuerst, bevor Sie andere Ursachen untersuchen. Gluetun verwendet einen eigenen Resolver und schreibt /etc/resolv.conf in seinem eigenen Container neu. /etc/resolv.conf ist jedoch eine Datei pro Container. Die von gluetun geschriebene Datei ist daher nicht die Datei, die Ihre Anwendung liest.
docker exec qbittorrent cat /etc/resolv.confWie erreiche ich einen Dienst, der auf dem Docker-Host läuft?
Verwenden Sie host.docker.internal. Dafür sind zwei Einstellungen an zwei verschiedenen Stellen erforderlich, weil zwei unterschiedliche Dinge fehlerhaft sind.
Zuerst kommt der Name. /etc/hosts gilt pro Container. Der Eintrag extra_hosts gehört daher in den Anwendungscontainer, nicht in gluetun.
prowlarr:
network_mode: "service:gluetun"
extra_hosts:
- "host.docker.internal:host-gateway"host-gateway ist ein spezieller Wert, den Docker durch eine interne Adresse des Hosts selbst ersetzt. Bei einer normalen Linux-Docker-Installation ist das die Adresse der docker0-Bridge, häufig 172.17.0.1. Prüfen Sie den Wert mit ip -4 addr show docker0 auf dem VPS. Docker Desktop löst diesen Namen selbst auf. Deshalb lassen Anleitungen für einen Laptop die Zeile extra_hosts aus. Dieselbe Datei schlägt dann auf einem Server fehl.
Danach kommt die Route. Der Name legt nur fest, welche Adresse der Container verwenden soll. Das Paket verlässt den Container weiterhin über die Standardroute von gluetun. Diese führt durch den Tunnel, und die Firewall von gluetun verwirft das Paket. Das äußert sich durch eine Verbindung, die hängen bleibt und anschließend in einen Timeout läuft, nicht durch eine sofortige Ablehnung. Eine Ablehnung bedeutet, dass das Paket angekommen ist und ein Dienst mit „nein“ geantwortet hat. Ein Timeout bedeutet, dass es nie angekommen ist.
gluetun:
environment:
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32Prüfen Sie anschließend, ob der Dienst auf dem Host tatsächlich an dieser Adresse lauscht. Ein PostgreSQL-Server, der nur an 127.0.0.1 gebunden ist, ist aus keinem Container erreichbar, unabhängig davon, ob ein Tunnel verwendet wird. Der Grund ist, dass 127.0.0.1 innerhalb des Namespace auf die Loopback-Adresse dieses Namespace selbst zeigt. Binden Sie den Dienst stattdessen an 172.17.0.1. Dann akzeptiert er Verbindungen aus Containern, bleibt aber von der öffentlichen Schnittstelle getrennt. Prüfen Sie dies auf dem Host mit ss -lntp | grep 5432.
Was FIREWALL_OUTBOUND_SUBNETS tatsächlich ändert
Die Gluetun-Dokumentation beschreibt den Wert als kommagetrennte Liste der Subnetze, auf die gluetun und die Container mit gemeinsamem Netzwerk-Stack zugreifen dürfen. Sie weist außerdem darauf hin, dass dafür Änderungen an Firewall und Routing erforderlich sind. Beide Aspekte sind relevant. Gluetun fügt für jedes aufgeführte Subnetz eine Route über das Docker-Bridge-Gateway hinzu. Pakete für diese Adressen verlassen das System dadurch über eth0 und nicht über den Tunnel. Außerdem öffnet Gluetun die Firewall für diese Subnetze. Andernfalls verwirft gluetun ausgehenden Datenverkehr, der nicht für den VPN-Server bestimmt ist.
Geben Sie den Wert ohne Leerzeichen nach den Kommas an.
- FIREWALL_OUTBOUND_SUBNETS=172.17.0.1/32,192.168.1.0/24,100.64.7.9/32Zwei Eigenschaften werden leicht übersehen. Dies ist eine Einstellung auf Namespace-Ebene. Sie gilt daher für jeden Container hinter gluetun und nicht nur für den vorgesehenen Container. Außerdem gilt sie nur für ausgehenden Datenverkehr. Sie steuert Verbindungen, die ein Container selbst aufbaut. Verbindungen, die an einem veröffentlichten Port eingehen, nehmen einen anderen Pfad und benötigen hier keinen Eintrag.
Die Weboberfläche von einem Tailscale-Peer aus erreichen
Tailscale weist jedem Rechner eine Adresse aus 100.64.0.0/10 zu, dem für Carrier-Grade-NAT reservierten Bereich. In einem persönlichen Tailnet entstehen dafür keine Kosten. Ob das so bleibt, wenn weitere Personen auf dieselben UIs zugreifen sollen, hängt jedoch von den Benutzer- und Geräteobergrenzen des kostenlosen Tarifs ab. Da die Abrechnung nach Benutzern und nicht nach Rechnern erfolgt, hängt der tatsächliche Preis eines kostenpflichtigen Tailnets davon ab, wie viele Personen Sie einladen, und nicht davon, wie viele Container Sie für sie freigeben. Für die beiden Richtungen sind unterschiedliche Arbeiten erforderlich.
Der eingehende Zugriff ist einfach. Wenn Sie 8080:8080 auf gluetun veröffentlichen, wird dieser Port an alle Adressen des Hosts gebunden. Die tailscale0-Schnittstelle des Hosts gehört dazu. Ein Peer öffnet daher http://<machine-name>:8080 und erreicht den Container. Gluetun ist an diesem Pfad nicht beteiligt, weil die NAT-Regel von Docker auf dem Host außerhalb des Namespace liegt.
Damit die Oberfläche nur über das Tailnet erreichbar ist, binden Sie den veröffentlichten Port an die Tailscale-Adresse des Hosts und nicht an alle Adressen.
ports:
- "100.101.102.103:8080:8080/tcp"Ermitteln Sie diese Adresse auf dem Host mit tailscale ip -4. Das Binden ist hier eine wirksamere Kontrolle als eine Firewall-Regel, weil der Port an der öffentlichen Schnittstelle überhaupt nicht geöffnet wird. Wenn Sie die Oberfläche lieber über einen HTTPS-Namen statt über Host und Port erreichen möchten, kann tailscale serve den veröffentlichten Port vorschalten. Serve und Funnel unterscheiden sich darin, wer die Oberfläche letztlich erreichen kann, und nur eine der beiden Optionen hält die Oberfläche im Tailnet. Außerdem umgehen Sie damit das Problem, das entsteht, wenn Docker veröffentlichte Ports direkt an ufw vorbeiführt.
Beim ausgehenden Zugriff kommt FIREWALL_OUTBOUND_SUBNETS zum Einsatz. Wenn ein Container einen Peer erreichen muss, fügen Sie die Adresse dieses Peers hinzu. Bevorzugen Sie pro Peer ein /32 gegenüber dem gesamten /10. Befindet sich der angesprochene Rechner in einem privaten Netzwerk, das über einen VPS, der dieses Subnetz in Ihrem Tailnet bekannt gibt, erreichbar ist, tragen Sie den bekannt gegebenen Bereich statt der eigenen 100.x-Adresse des Routers ein. Prüfen Sie außerdem, ob der Host diese Routen selbst akzeptiert hat. MagicDNS-Namen werden im Container nicht aufgelöst, weil der Container nicht den Resolver des Hosts verwendet. Verwenden Sie daher die numerische 100.x-Adresse oder hinterlegen Sie sie mit einer extra_hosts-Zeile fest. Das gilt auch, wenn Sie mit Headscale einen eigenen Tailscale-Control-Server betreiben.
Eine vollständige Compose-Datei für die übliche Struktur
Ein Download-Client hinter dem VPN, zwei Web-UIs, die nur im Tailnet erreichbar sind, und ein Container, der eine auf dem Host laufende PostgreSQL-Datenbank liest.
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-stoppedLesen Sie die Datei im Hinblick auf das Muster und nicht auf die Produktnamen. Beide UIs werden auf gluetun veröffentlicht und an die Tailnet-Adresse des Hosts gebunden. Sie sind daher über Tailscale und nirgendwo sonst erreichbar. Nur Prowlarr enthält die Zeile extra_hosts, weil Prowlarr der Container ist, der host.docker.internal auflöst. FIREWALL_OUTBOUND_SUBNETS nennt zwei einzelne Adressen: die Docker-Bridge-Adresse des Hosts, damit Prowlarr eine Datenbankverbindung öffnen kann, und einen Tailnet-Peer.
Der PostgreSQL-Server fehlt absichtlich in der Datei. Er läuft auf dem VPS als gewöhnlicher Systemdienst und lauscht auf 172.17.0.1:5432. Das entspricht derselben Schichtung wie ein Arr-Stack mit Docker Compose, wobei die Datenbank außerhalb von Docker läuft.
Halten Sie den privaten WireGuard-Schlüssel aus der Compose-Datei heraus. ${WIREGUARD_PRIVATE_KEY} liest ihn aus einer .env-Datei daneben. Dieses Muster wird unter Umgebungsdateien und Secrets für Docker Compose behandelt. Die Klausel condition: service_healthy verwendet den Healthcheck, den das gluetun-Image bereits mitliefert. Daher startet nichts, bevor der Tunnel seinen Betriebszustand meldet. Compose-Healthchecks erläutern die allgemeine Form.
:::detailsVeröffentlichung auf jeder Adresse statt nur im Tailnet
Entfernen Sie das Adresspräfix und die Port-Bindings auf 0.0.0.0. Dadurch wird auch die öffentliche IP-Adresse des VPS eingebunden. Tun Sie dies nur hinter einer Firewall, die Sie kontrollieren, und lesen Sie vorher den obigen Hinweis zu ufw.
ports:
- "8080:8080/tcp":::
Bestätigen Sie, dass der Tunnel weiterhin Datenverkehr führt
Führen Sie dieselbe Anfrage zweimal aus: einmal innerhalb des Namespace und einmal auf dem Host. Vergleichen Sie anschließend die Ergebnisse.
docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org
curl -s https://api.ipify.orgDie erste Ausgabe sollte die Exit-Adresse Ihres VPN-Providers anzeigen. Die zweite sollte die Adresse des VPS anzeigen. Wenn beide Adressen übereinstimmen, läuft der Datenverkehr des Containers nicht durch den Tunnel. Alle weiteren Schritte in diesem Leitfaden sind erst relevant, wenn Sie das korrigiert haben.
Die Routingtabelle zeigt, welcher Datenverkehr über den Tunnel läuft und welcher nicht.
docker run --rm --network=container:gluetun alpine:3.22 ip route showDie Standardroute sollte auf die Tunnelschnittstelle tun0 zeigen. Darunter sollte für jeden Eintrag in FIREWALL_OUTBOUND_SUBNETS eine Route angezeigt werden, die auf das Docker-Bridge-Gateway zeigt. Jede andere Route, die über eth0 führt, umgeht das VPN.
Der Control Server von Gluetun meldet dieselbe öffentliche IP-Adresse auf Port 8000 unter /v1/publicip/ip. Neuere Versionen erfordern eine Authentifizierung für die Routen des Control Servers. Richten Sie diese daher ein, bevor Sie sich darauf verlassen.
Das Leck, das ein falsches Subnetz verursacht
FIREWALL_OUTBOUND_SUBNETS ist eine absichtlich in der Firewall geöffnete Lücke. Die Größe dieser Lücke entspricht daher der Größe des Risikos. Es gibt vier Möglichkeiten, sie zu groß zu machen:
0.0.0.0/0sendet sämtlichen Datenverkehr außerhalb des Tunnels. Die beiden IP-Prüfungen weiter oben erkennen dies beim ersten Durchlauf, weil sie dieselbe Adresse zurückgeben.- Ein Bereich, der größer als das Ziel ist. Wenn Sie
10.0.0.0/8öffnen, um einen einzelnen Host unter10.0.1.7zu erreichen, öffnen Sie damit auch jede Adresse, die ein Torrent-Peer in diesem Bereich bekanntgeben könnte. Tragen Sie stattdessen10.0.1.7/32ein. - Ein Bereich, der die eigenen Adressen des Tunnels überlappt. Die gluetun-Dokumentation weist darauf hin, dass gluetun dadurch den VPN-Datenverkehr stattdessen über die Bridge sendet. Dadurch funktioniert die Portweiterleitung nicht mehr. Prüfen Sie den Wert
WIREGUARD_ADDRESSES, bevor Sie einen privaten Bereich öffnen. 100.64.0.0/10für Tailscale. Damit werden ungefähr vier Millionen Adressen geöffnet, damit ein Peer erreicht werden kann. Führen Sie die benötigten Peers als/32-Einträge auf.
Beachten Sie, dass diese Einstellung für den gesamten Namespace gilt. Wenn Sie ein Subnetz öffnen, damit ein Indexer einen Hostdienst erreichen kann, wird dasselbe Subnetz auch für den Torrent-Client geöffnet, der diesen Namespace gemeinsam verwendet. Führen Sie nach jeder Änderung dieser Variable die Prüfung der öffentlichen IP-Adresse erneut aus. Nur mit dieser Prüfung lässt sich feststellen, ob die Änderung tatsächlich das gewünschte Ergebnis hatte.
Was beim Neustart von gluetun ausfällt
gluetun verwaltet den Namespace. Daher entspricht der Lebenszyklus des Namespace dem Lebenszyklus von gluetun. Wenn Sie einen abhängigen Container starten, während gluetun nicht läuft, schlägt der Start sofort fehl:
Error response from daemon: cannot join network of a non running containerEin Neustart von gluetun im laufenden Verbund verursacht den schwerer erkennbaren Fehler. Die abhängigen Container laufen weiter, während der Namespace, an den sie angebunden sind, im Hintergrund neu erstellt wird. Daher meldet docker ps einen fehlerfreien Zustand, obwohl keine Verbindung antwortet. Nach jeder Änderung am gluetun-Dienst müssen Sie die gesamte Gruppe neu erstellen, statt nur einen Teil davon neu zu starten.
docker compose up -d --force-recreateDas gilt auch für Image-Aktualisierungen. Wenn Sie ein neues gluetun-Image abrufen und nur diesen Dienst neu erstellen, verweisen die übrigen Container weiterhin auf einen Namespace, der nicht mehr vorhanden ist.
FAQ
Warum meldet Docker „port publishing and the container type network mode“?
Das liegt daran, dass für einen Dienst noch ein ports:-Block vorhanden ist, der gleichzeitig network_mode: service:gluetun setzt. Durch die Veröffentlichung eines Ports wird eine NAT-Regel hinzugefügt, die einen Host-Port in den eigenen Netzwerk-Namespace des Containers weiterleitet. Ein Container in diesem Modus verfügt jedoch über keinen eigenen Namespace. Löschen Sie den ports:-Block aus diesem Dienst und fügen Sie dieselbe Zuordnung beim gluetun-Dienst hinzu. Die Portnummer bleibt unverändert, weil die Anwendung weiterhin innerhalb des gemeinsamen Namespace auf diesem Port lauscht.
Wie erreichen andere Container einen Dienst hinter gluetun?
Container innerhalb desselben Namespace erreichen einander unter 127.0.0.1. Container außerhalb verwenden den Dienstnamen von gluetun. Daher funktioniert http://gluetun:8080, während http://qbittorrent:8080 nicht funktioniert. Der Anwendungscontainer besitzt keine Adresse in einem Docker-Netzwerk. Der integrierte DNS-Server kann seinen Namen daher nicht auflösen. Dafür ist keine Portveröffentlichung erforderlich, solange beide Container ein Compose-Netzwerk gemeinsam verwenden.
Was soll ich in FIREWALL_OUTBOUND_SUBNETS eintragen?
Tragen Sie nur die Adressen ein, zu denen ein Container hinter gluetun eine Verbindung herstellen muss. Geben Sie sie so eng wie möglich an. Ein einzelner Rechner wird als /32 angegeben. Die beiden häufigsten Einträge sind der Docker-Host unter 172.17.0.1/32 und ein /32 für jeden Tailscale-Peer, den Sie erreichen. Fügen Sie niemals 0.0.0.0/0 hinzu. Verwenden Sie außerdem niemals einen Bereich, der die eigenen Tunneladressen Ihres VPN überschneidet. Eingehende Verbindungen zu einem veröffentlichten Port benötigen hier keinen Eintrag.
Warum kann der Container meine Tailscale-MagicDNS-Namen nicht auflösen?
MagicDNS funktioniert, indem der Resolver des Hosts auf den DNS-Server von Tailscale verweist. Der Container verwendet jedoch nicht den Resolver des Hosts. Er verwendet die Angaben aus seinem eigenen /etc/resolv.conf. Hinter gluetun ist dies die DNS-Konfiguration von gluetun. Prüfen Sie dies mit docker exec <container> cat /etc/resolv.conf. Verwenden Sie die numerische 100.x-Adresse des Peers oder hinterlegen Sie den Namen mit einem extra_hosts-Eintrag in diesem Container.
Wie bestätige ich, dass der Datenverkehr weiterhin über das VPN läuft?
Führen Sie eine Anfrage innerhalb des Namespace und dieselbe Anfrage auf dem Host aus. Vergleichen Sie anschließend die Antworten. docker run --rm --network=container:gluetun curlimages/curl:latest -s https://api.ipify.org sollte die Exit-Adresse Ihres VPN-Anbieters zurückgeben. curl -s https://api.ipify.org auf dem VPS sollte dagegen die Adresse des VPS zurückgeben. Stimmen die beiden Antworten überein, läuft der Datenverkehr des Containers nicht durch den Tunnel. Wiederholen Sie diese Prüfung nach jeder Änderung an FIREWALL_OUTBOUND_SUBNETS.