Tailscale mit Docker Compose ohne offenen Host-Port
Das Tailscale-Compose-Beispiel veröffentlicht einen Host-Port. Mit einem Sidecar bleibt die Anwendung ohne Portfreigabe und ist nur über ihren Tailnet-Namen erreichbar.
Tailscale in einem Docker-Compose-Stack ausführen
Für Tailscale in Docker Compose sind zwei Dienste erforderlich. Einer davon ist der tailscale/tailscale-Container, der Ihrem Tailnet beitritt. Der andere ist Ihr Anwendungscontainer. Er verwendet den Netzwerk-Namespace des ersten Containers, statt einen Port auf dem Host zu veröffentlichen. Dadurch öffnen Sie den Dienst auf Ihrem Laptop über seinen Namen. Aus dem öffentlichen Internet ist er nicht erreichbar.
Ein Tailnet ist das private Netzwerk, das Tailscale zwischen den Geräten aufbaut, bei denen Sie sich anmelden. Wenn dieser Begriff für Sie neu ist, lesen Sie zuerst was Tailscale ist und wie es zwei Rechner verbindet. Diese Anleitung setzt voraus, dass Docker Engine und das Compose-v2-Plugin bereits funktionieren, wie in ein erster Docker-Compose-Stack auf einem VPS beschrieben.
Das Herstellerbeispiel und der Port, den es offen lässt
Der eigene Compose-Leitfaden von Tailscale veröffentlicht einen Stack, der diesem sehr ähnlich ist.
services:
tailscale:
image: tailscale/tailscale:latest
container_name: tailscale
hostname: tailscale-nginx
environment:
- TS_AUTHKEY=tskey-auth-REPLACE-ME
- TS_STATE_DIR=/var/lib/tailscale
volumes:
- ./tailscale-state:/var/lib/tailscale
cap_add:
- net_admin
- net_raw
restart: unless-stopped
nginx:
image: nginx:latest
container_name: nginx_server
ports:
- "8080:80"
depends_on:
- tailscale
restart: unless-stoppedJede Zeile des tailscale-Dienstes ist korrekt. TS_AUTHKEY authentifiziert den Node. TS_STATE_DIR legt fest, wohin tailscaled seinen Status schreibt, und der Bind-Mount hält diesen Status auf der Festplatte. Der zweite Dienst ist das Problem.
Die beiden Container befinden sich im standardmäßigen Compose-Bridge-Netzwerk und haben jeweils eine eigene Adresse. Das ist das normale Verhalten, das in wie Compose Container über den Dienstnamen verbindet erklärt wird. Der tailscale-Container ist dem Tailnet für sich selbst beigetreten und leitet keinen Datenverkehr an den nginx-Container weiter. Der einzige Weg zu nginx führt daher über Port 8080 auf dem Host.
Ein veröffentlichter Port wird an 0.0.0.0 gebunden, sofern Sie keine Adresse davor angeben. Auf einem VPS antwortet dieser Port daher an der öffentlichen IP-Adresse. Dem Namen nach befindet sich die Anwendung in Ihrem Tailnet, tatsächlich ist sie jedoch aus dem Internet erreichbar. Eine Host-Firewall schützt Sie ebenfalls nicht, weil Docker seine eigenen Weiterleitungsregeln vor der Kette von ufw einfügt. Das ist die Falle, die in warum eine ufw-deny-Regel einen veröffentlichten Docker-Port nicht schließt beschrieben wird.
Ein weiteres Detail ist erwähnenswert. Das Beispiel gewährt net_admin und net_raw, ordnet /dev/net/tun jedoch nie zu. TS_USERSPACE ist standardmäßig auf true gesetzt. Der Container verwendet daher den Userspace-Netzwerk-Stack, und diese beiden Capabilities haben keine Wirkung.
Der Sidecar: ein Namespace, kein veröffentlichter Port
Führen Sie die Anwendung mit network_mode: service:tailscale in den Netzwerk-Namespace des Tailscale-Containers ein. Beide Prozesse sehen dann dasselbe Loopback-Interface und dieselbe Tailnet-Adresse, obwohl sie in getrennten Containern laufen.
services:
tailscale:
image: tailscale/tailscale:v1.102.3
container_name: ts-nginx
hostname: nginx-demo
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_HOSTNAME=nginx-demo
- TS_STATE_DIR=/var/lib/tailscale
volumes:
- ./ts-state:/var/lib/tailscale
restart: unless-stopped
nginx:
image: nginx:1.30.4-alpine
network_mode: service:tailscale
depends_on:
- tailscale
restart: unless-stoppedDer Schlüssel gehört in eine .env-Datei neben der Compose-Datei, nicht in die YAML-Datei. Dadurch enthält die committete Datei kein Secret. Eine Zeile: TS_AUTHKEY=tskey-auth-.... Secrets aus einer committeten Compose-Datei heraushalten behandelt dieses Muster ausführlicher.
Starten Sie den Stack und prüfen Sie beide Seiten.
docker compose up -d
docker compose exec tailscale tailscale status
docker compose logs --tail 20 tailscaletailscale status sollte für diesen Node eine Zeile mit einer 100.x-Adresse ausgeben, gefolgt von den anderen Rechnern in Ihrem Tailnet. Von einem Laptop, der ebenfalls angemeldet ist, liefert curl http://nginx-demo/ die nginx-Begrüßungsseite. Auf dem VPS selbst liefert sudo ss -lntp | grep 8080 keine Ausgabe, weil kein Port veröffentlicht wurde.
Warum Port 80 und nicht 8080: Im Userspace-Modus leitet tailscaled eine eingehende Tunnelverbindung an denselben Port auf localhost weiter. nginx lauscht im gemeinsamen Namespace auf Port 80. Daher ist die Anwendung im Tailnet über Port 80 erreichbar. Wenn Sie den Port ändern, auf dem die Anwendung lauscht, ändert sich auch der Tailnet-Port. Dieser Trick mit dem gemeinsamen Namespace ist nicht auf Tailscale beschränkt. Dieselben Fragen zum Zugriff auf den Host und den restlichen Stack stellen sich auch bei einem Gluetun-Container, der das Netzwerk seiner Nachbarn verwaltet.
Welchen Authentifizierungsschlüssel benötigt der Container?
Der Schlüsseltyp bestimmt, was beim zweiten Start geschieht. Wählen Sie ihn daher vor der Bereitstellung aus. Erstellen Sie einen Schlüssel auf der Seite „Keys“ der Administrationskonsole. Das Dialogfeld zeigt ihn nur einmal an.
- Einmalige Schlüssel authentifizieren ein einzelnes Gerät. Ein Stack, der ohne sein Zustandsverzeichnis neu erstellt wird, kommt nicht wieder online.
- Wiederverwendbare Schlüssel authentifizieren beliebig viele Geräte. Das ist normalerweise die richtige Wahl für einen Compose-Stack.
- Ephemere Schlüssel markieren den Node für die automatische Bereinigung. Tailscale entfernt ein ephemeres Gerät 30 bis 60 Minuten nach seiner letzten Aktivität.
- Vorab genehmigte Schlüssel überspringen die manuelle Gerätegenehmigung. Das ist nur relevant, wenn die Gerätegenehmigung für Ihr Tailnet aktiviert ist.
- Getaggte Schlüssel wenden bei der Authentifizierung ein ACL-Tag wie
tag:containeran. Das Gerät gehört danach keiner Person mehr, und der Ablauf des Schlüssels ist standardmäßig deaktiviert.
Die letzte Zeile ist für den Betrieb entscheidend. Ein Node-Schlüssel läuft standardmäßig nach 180 Tagen ab. Ein abgelaufener Node wird aus dem Tailnet entfernt, bis eine Person ihn erneut anmeldet. Ein getaggter Schlüssel deaktiviert diesen Ablauf. Deshalb gibt es Tags für Server und Container.
Der Ablauf eines Authentifizierungsschlüssels ist ein separates Ereignis. Diese beiden Fälle werden häufig verwechselt. Authentifizierungsschlüssel sind 1 bis 90 Tage gültig, wobei 90 Tage der Standardwert ist. Wenn ein Schlüssel abläuft, werden die Geräte, die er bereits authentifiziert hat, nicht getrennt. Der Schlüssel kann lediglich keine neuen Geräte mehr hinzufügen. Verwenden Sie für einen langfristig laufenden Dienst einen wiederverwendbaren getaggten Schlüssel, der nicht ephemer ist. Für einen Stack, den Sie häufig entfernen, beispielsweise eine Vorschauumgebung, hält ein ephemerer Schlüssel die Administrationskonsole ohne manuelles Löschen übersichtlich.
Warum kommt der Container als neue Maschine zurück?
Weil tailscaled seinen Zustand in die beschreibbare Schicht des Containers geschrieben hat und docker compose down den Container gelöscht hat.
Die Identität des Knotens befindet sich in diesem Zustandsverzeichnis. Wenn Sie es persistent speichern, behält der Container seinen Namen und seine 100.x-Adresse über Neustarts hinweg. Das gilt auch für die Serve-Konfiguration. Geht das Verzeichnis verloren, ist der nächste Start ein Erststart: Der Container authentifiziert sich erneut mit demselben Schlüssel, und die Administrationskonsole erhält eine zweite Maschine. Beide beanspruchen den Hostnamen nginx-demo. Daher versieht MagicDNS die neuere Maschine mit einem nummerierten Suffix, und jeder gespeicherte Link verweist auf den nicht mehr vorhandenen Knoten.
Zwei Voraussetzungen müssen erfüllt sein. TS_STATE_DIR=/var/lib/tailscale muss gesetzt sein, weil außerhalb von Kubernetes kein Standardwert dafür existiert. Außerdem muss dieser Pfad eingebunden sein, entweder über den oben gezeigten Bind-Mount oder über ein benanntes Volume. Die Unterschiede werden in Bind-Mounts im Vergleich zu benannten Volumes erläutert. Nur eine der beiden Einstellungen vorzunehmen, ist ein häufiger Fehler. Er bleibt unbemerkt: Der Stack funktioniert bis zum ersten down einwandfrei.
Überprüfen Sie die Konfiguration, statt sie vorauszusetzen.
docker compose down
ls -l ./ts-state
docker compose up -d
docker compose exec tailscale tailscale status./ts-state sollte bereits vor dem zweiten up tailscaled.state enthalten. Außerdem sollte der Knoten mit der zuvor verwendeten Adresse zurückkehren. Eine andere Adresse zeigt, dass der Mount nicht korrekt funktioniert.
Legen Sie das Image fest, und geben Sie den verwendeten Tag an
tailscale/tailscale:latest folgt dem neuesten stabilen Build. Ein docker compose pull in sechs Monaten ersetzt tailscaled stillschweigend durch eine andere Version. Beim nächsten Neustart wird dann Code ausgeführt, den Sie nie ausgewählt haben. Der obige Stack legt v1.102.3 fest, die stabile Version vom September 2026. Docker Hub veröffentlicht außerdem v1.102 für die Patch-Versionen sowie einen unstable-Tag. Letzteren sollten Sie auf einem Server nicht verwenden.
Führen Sie Upgrades bewusst durch.
docker compose pull tailscale
docker compose up -d
docker compose exec tailscale tailscale versionWenn Sie den Tag ändern und up -d ausführen, wird der Container neu erstellt. Das ist nicht dasselbe wie ein Neustart. Der Unterschied zwischen restart, up und rebuild ist lesenswert, bevor Sie eine Versionsänderung untersuchen, die nicht wirksam geworden ist.
Netzwerk im Userspace und seine Nachteile
TS_USERSPACE ist standardmäßig auf true gesetzt. Der Container führt dann einen TCP/IP-Stack im Userspace aus und greift nie auf /dev/net/tun zu. Dadurch funktioniert dieses Verfahren auch auf Hosts, die einem Container kein TUN-Gerät überlassen. Eingehende Verbindungen funktionieren weiterhin, weil eingehende Tunnelverbindungen an denselben Port auf localhost weitergeleitet werden. Genau deshalb benötigt der oben beschriebene Sidecar weder ein Gerät noch Capabilities.
Bei ausgehendem Datenverkehr entstehen die Nachteile. Im Userspace-Modus kann die Anwendung nicht einfach einen Socket zu einem anderen Tailnet-Knoten öffnen. tailscaled stellt stattdessen einen SOCKS5-Proxy und einen HTTP-Proxy bereit. Sie setzen daher TS_SOCKS5_SERVER=localhost:1055 beim Tailscale-Dienst und ALL_PROXY=socks5://localhost:1055 bei der Anwendung. Die Anwendung muss diese Einstellung berücksichtigen. Anwendungen, die Proxy-Umgebungsvariablen ignorieren, erreichen das Tailnet nicht.
Der Stack hat einige wichtige Einschränkungen. Es werden nur TCP und UDP übertragen. Andere IP-Protokolle wie SCTP passieren ihn daher nicht. ICMP ist auf Ping beschränkt, den der Daemon rekonstruiert. Dadurch entsteht eine geringfügig höhere scheinbare Latenz. Verbindungen werden am Knoten beendet und zum Ziel erneut aufgebaut. Sie sind daher nicht Ende-zu-Ende-verschlüsselt. Ein Knoten im Userspace kann außerdem weder einen Exit Node noch eine von einem anderen Knoten angekündigte Subnetzroute verwenden. Er kann solche Routen jedoch selbst ankündigen.
Wenn Sie transparenten ausgehenden Datenverkehr benötigen, wechseln Sie zur Kernel-Netzwerkfunktion, indem Sie dem Tailscale-Dienst drei Dinge hinzufügen.
environment:
- TS_USERSPACE=false
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- net_adminPrüfen Sie zunächst mit test -c /dev/net/tun && echo ok, ob der Host das Gerät bereitstellen kann. Bei KVM ist das Gerät vorhanden. Bei Container-Virtualisierung, die den Kernel des Hosts gemeinsam nutzt, kann es fehlen. Dann bleibt nur der Userspace-Modus. Stellen Sie dem Container das TUN-Gerät bereit, wenn er als Subnetzrouter für die Ankündigung eines privaten Bereichs oder als Exit Node für Ihre anderen Geräte dienen soll. Diese Funktionen sind im Userspace-Modus am stärksten eingeschränkt.
Auf den Dienst zugreifen: Serve oder der einfache MagicDNS-Name
Der einfache Weg ist der MagicDNS-Name. Von jedem Gerät im Tailnet funktioniert http://nginx-demo/, ebenso der vollständige Name http://nginx-demo.your-tailnet.ts.net/. Unverschlüsseltes HTTP bedeutet hier nicht, dass die Daten unverschlüsselt übertragen werden, weil WireGuard den Verkehr zwischen den beiden Knoten verschlüsselt. Welche Daten der Koordinationsserver sehen kann und welche nicht zeigt, wo diese Zusicherung endet. Da kein Zertifikat vorhanden ist, markiert der Browser den Ursprung als unsicher. Webfunktionen, die einen sicheren Kontext voraussetzen, werden nicht ausgeführt.
Der andere Weg ist Tailscale Serve, das innerhalb des Containers ausgeführt wird.
docker compose exec tailscale tailscale serve --bg localhost:80
docker compose exec tailscale tailscale serve statusDamit wird die Anwendung unter https://nginx-demo.your-tailnet.ts.net mit einem von Tailscale bereitgestellten Zertifikat veröffentlicht. MagicDNS und HTTPS-Zertifikate müssen beide auf der DNS-Seite der Administrationskonsole aktiviert sein. Andernfalls gibt es keinen Namen, der in ein Zertifikat aufgenommen werden kann. --bg schreibt die Konfiguration in den von Ihnen persistent gespeicherten tailscaled-Zustand. Dadurch bleibt sie beim Neustart des Containers erhalten. tailscale serve reset entfernt sie. TS_SERVE_CONFIG verweist auf eine JSON-Datei, wenn Sie diese Konfiguration lieber im Repository als in einem Shell-Befehl speichern möchten. Serve bleibt innerhalb des Tailnets. Funnel ist der separate Befehl, der denselben Dienst im öffentlichen Internet veröffentlicht. Lesen Sie daher den Unterschied zwischen Serve und Funnel, bevor Sie einen der beiden Befehle eingeben.
Fehlerfälle und die Meldungen, die Sie sehen werden
Error response from daemon: conflicting options: port publishing and the container type network mode. Im Sidecar-Dienst ist ein ports:-Block verblieben. Nur der Container, dem der Namespace gehört, darf Ports veröffentlichen. Eine App, die nur im Tailnet erreichbar sein soll, darf keine Ports veröffentlichen. Löschen Sie den Block.
Der App-Container läuft, und ihn erreicht keine Anfrage. Sie haben den Tailscale-Dienst allein neu erstellt. Der Namespace, dessen Eigentümer er war, wurde mit ihm entfernt. Die App ist an etwas angebunden, das nicht mehr existiert. Erstellen Sie beide gemeinsam mit docker compose up -d --force-recreate neu.
Im Admin-Portal wird kein Node angezeigt. Lesen Sie docker compose logs tailscale. Dort wird ein abgelehnter Key gemeldet. Ein einmaliger Key, der bereits verwendet wurde, und ein Key nach Ablauf seiner Gültigkeit stoppen den Node, bevor er das Tailnet überhaupt erreicht.
Der Node ist aktiv, tailscale status sieht korrekt aus, und curl http://nginx-demo/ hängt. Die App lauscht nicht dort, wo Sie es erwarten. Prüfen Sie sie aus dem gemeinsamen Namespace mit docker compose exec nginx wget -qO- http://localhost/. Wenn das ebenfalls fehlschlägt, liegt das Problem bei der App und nicht bei Tailscale. Wenn es erfolgreich ist, ist die App an eine einzelne Schnittstelle statt an alle Schnittstellen gebunden.
Die Maschine ist eine Stunde nach dem Stoppen des Stacks aus dem Portal verschwunden. Der Key war ephemer. Die Entfernung erfolgt 30 bis 60 Minuten nach der letzten Aktivität. Das entspricht dem vorgesehenen Verhalten.
Ab Version 1.78 kann das Image einen nicht authentifizierten /healthz-Endpunkt bereitstellen: Setzen Sie TS_ENABLE_HEALTH_CHECK=true. Dieser lauscht auf TS_LOCAL_ADDR_PORT, standardmäßig [::]:9002. Richten Sie einen Compose-Healthcheck darauf, damit ein Node, dessen Authentifizierung fehlschlägt, als fehlerhaft gemeldet wird, statt weiterhin ordnungsgemäß auszusehen. Wenn Sie nicht von den Koordinierungsservern von Tailscale abhängig sein möchten, kommuniziert dieselbe Compose-Datei über TS_EXTRA_ARGS=--login-server=https://headscale.example.com mit Ihrer eigenen Control Plane. Das ist der Ausgangspunkt für Headscale als eigenen Control-Server zu betreiben.
FAQ
Warum können andere Geräte in meinem Tailnet meinen App-Container nicht erreichen?
Der Tailscale-Container ist nur für sich selbst dem Tailnet beigetreten. Wenn die App im standardmäßigen Compose-Bridge-Netzwerk mit einer eigenen Adresse läuft, leitet der Tailscale-Knoten keine Verbindungen an sie weiter. Der einzige Zugangsweg ist dann der veröffentlichte Host-Port. Geben Sie der App network_mode: service:tailscale, damit sie den Netzwerk-Namespace des Tailscale-Containers verwendet, und entfernen Sie ihren ports:-Block. Die App ist anschließend im Tailnet über den Port erreichbar, an dem sie lauscht.
Benötige ich /dev/net/tun, um Tailscale in Docker Compose auszuführen?
Nicht für eingehenden Zugriff. TS_USERSPACE ist standardmäßig auf true gesetzt. In diesem Modus verwendet tailscaled einen eigenen Netzwerk-Stack und leitet eingehende Tunnelverbindungen an denselben Port auf localhost weiter. Ein Sidecar funktioniert daher ohne Gerät und ohne zusätzliche Capabilities. /dev/net/tun, TS_USERSPACE=false und net_admin benötigen Sie, wenn der Container ausgehende Verbindungen zum Tailnet transparent öffnen oder als Subnet-Router oder Exit-Node fungieren muss.
Sollte ich für einen Compose-Stack einen kurzlebigen oder wiederverwendbaren Authentifizierungsschlüssel verwenden?
Verwenden Sie für alles, was dauerhaft läuft, einen wiederverwendbaren Schlüssel mit Tag, der nicht kurzlebig ist. Durch das Tagging läuft der Node-Key nicht ab. Der Container wird dadurch nicht nach 180 Tagen aus dem Tailnet entfernt, weil niemand ihn erneut anmelden konnte. Verwenden Sie kurzlebige Schlüssel nur für Stacks, die Sie häufig löschen, beispielsweise für Vorschauumgebungen. Tailscale entfernt ein kurzlebiges Gerät 30 bis 60 Minuten nach seiner letzten Aktivität. Dadurch bleibt die Administrationskonsole übersichtlich.
Warum wird mein Container bei jedem Neustart als neuer Rechner angezeigt?
Das Zustandsverzeichnis wird nicht persistent gespeichert. Daher startet tailscaled ohne Identität und authentifiziert sich als vollständig neuer Node. Setzen Sie TS_STATE_DIR=/var/lib/tailscale, das außerhalb von Kubernetes keinen Standardwert hat, und mounten Sie diesen Pfad in ein Bind-Mount oder ein benanntes Volume. Wenn Sie nur eine der beiden Einstellungen setzen, wirkt die Konfiguration bis zum ersten docker compose down korrekt. Stellen Sie sicher, dass tailscaled.state im gemounteten Verzeichnis vorhanden ist, während der Stack heruntergefahren ist.