SSD Nodes Learn Hosting plans →
Anleitungen Matt ConnorVon Matt Connor

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 offene Port

Der Compose-Leitfaden von Tailscale veröffentlicht einen Stack, der diesem hier ähnelt.

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-stopped

Jede Zeile des tailscale-Dienstes ist korrekt. TS_AUTHKEY authentifiziert den Knoten. TS_STATE_DIR gibt tailscaled vor, wohin der Dienst seinen Zustand schreiben soll, und der Bind-Mount hält diesen Zustand 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 Servicenamen verbindet erklärt wird. Der tailscale-Container ist selbst dem Tailnet beigetreten und leitet keinen Datenverkehr an den nginx-Container weiter. Daher gibt es im Tailnet überhaupt keine Route zu nginx. Der einzige externe Zugang zum VPS ist also Port 8080 auf dem Host.

Ein veröffentlichter Port wird an 0.0.0.0 gebunden, sofern Sie keine Adresse davor angeben oder eine andere Standard-Bindeadresse für das Netzwerk festlegen. Auf einem VPS antwortet dieser Port daher über die öffentliche IP-Adresse. Die Anwendung ist dem Namen nach in Ihrem Tailnet, tatsächlich aber im 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 sollte genannt werden. Das Beispiel gewährt net_admin und net_raw, bindet /dev/net/tun aber nie ein. 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 im Netzwerk-Namespace des tailscale-Containers aus. Beide Prozesse sehen dann dasselbe Loopback-Interface und dieselbe tailnet-Adresse, obwohl sie in getrennten Containern laufen.

services:
  tailscale:
    image: tailscale/tailscale:v1.102.5
    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-stopped

Der Schlüssel gehört in eine .env-Datei neben der Compose-Datei, nicht in die YAML-Datei. Dadurch enthält die eingecheckte Datei kein Secret. Eine Zeile genügt: TS_AUTHKEY=tskey-auth-.... Secrets aus einer eingecheckten Compose-Datei heraushalten beschreibt dieses Vorgehen 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 tailscale

tailscale status sollte für diesen Node eine Zeile mit einer 100.x-Adresse ausgeben. Danach folgen die anderen Rechner in Ihrem tailnet. Von einem Laptop, der ebenfalls angemeldet ist, liefert curl http://nginx-demo/ die nginx-Willkommensseite. 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 gemeinsam genutzten Namespace auf Port 80. Das tailnet erreicht den Dienst daher auf Port 80. Wenn Sie den Port ändern, auf dem die Anwendung lauscht, ändert sich auch der tailnet-Port. Dieser Trick mit einem gemeinsam genutzten Namespace ist nicht spezifisch für Tailscale. Dieselben Fragen zum Zugriff auf den Host und auf den restlichen Stack stellen sich auch bei einem Gluetun-Container, der das Netzwerk seiner Nachbarn übernimmt.

Welchen Authentifizierungsschlüssel benötigt der Container?

Der Schlüsseltyp bestimmt, was beim zweiten Start geschieht. Wählen Sie ihn daher vor dem Deployment aus. Erstellen Sie den 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:container an. Das Gerät gehört dann nicht mehr zu einer Person. 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 ihn eine Person erneut anmeldet oder ein Administrator den Schlüsselablauf deaktiviert. Ein getaggter Schlüssel verhindert 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, standardmäßig 90 Tage. Wenn ein Schlüssel abläuft, werden die Geräte, die er bereits authentifiziert hat, nicht getrennt. Der Schlüssel verhindert lediglich die Authentifizierung neuer Geräte. Verwenden Sie für einen dauerhaft laufenden Dienst einen wiederverwendbaren getaggten Schlüssel, der nicht ephemer ist. Für einen Stack, den Sie regelmäßig entfernen, beispielsweise eine Preview-Umgebung, hält ein ephemerer Schlüssel die Administrationskonsole ohne manuelle Löschung übersichtlich.

Warum kommt der Container als neue Maschine zurück?

Weil tailscaled seinen Zustand an einem Ort gespeichert hat, der nicht erhalten blieb. Wenn TS_STATE_DIR nicht gesetzt ist, speichert das Image den Zustand nur im Arbeitsspeicher. Wenn TS_STATE_DIR gesetzt, der Pfad aber nicht eingebunden ist, landet der Zustand in der beschreibbaren Schicht des Containers. docker compose down löscht den Container anschließend.

Die Identität des Knotens liegt in diesem Zustandsverzeichnis. Wenn Sie es persistent speichern, behält der Container bei Neustarts seinen Namen und seine 100.x-Adresse sowie die vorhandene 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. MagicDNS versieht den neueren Knoten daher mit einem numerischen 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 definiert ist. Außerdem muss dieser Pfad eingebunden sein, entweder über den oben gezeigten Bind-Mount oder über ein benanntes Volume. Diese Auswahl wird unter Bind-Mounts im Vergleich zu benannten Volumes erläutert. Nur eine der beiden Einstellungen zu setzen, ist der häufigste Fehler. Ein Mount ohne TS_STATE_DIR verliert die Identität bei jedem Neustart. Ein TS_STATE_DIR ohne Mount schlägt hingegen unbemerkt fehl: Der Stack funktioniert bis zum ersten down einwandfrei.

Überprüfen Sie dies, statt es 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. Der Knoten sollte mit der zuvor verwendeten Adresse zurückkehren. Eine andere Adresse bedeutet, dass der Mount nicht korrekt funktioniert.

Das Image festlegen und den verwendeten Tag angeben

tailscale/tailscale:latest folgt dem neuesten stabilen Build. Ein docker compose pull in sechs Monaten lädt ein anderes tailscaled herunter, und der nächste docker compose up -d erstellt den Container mit Code neu, den Sie nie ausgewählt haben. Ein einfacher Neustart verwendet weiterhin das alte Image. Die Änderung wird daher erst wirksam, wenn der Container erneut erstellt wird, nicht beim Herunterladen des Images. Der Stack oben legt v1.102.5 fest, auf das die stable- und latest-Tags von Docker Hub am 10 October 2026 verwiesen haben. Im Changelog von Tailscale ist bereits der am 7 October 2026 veröffentlichte 1.104.1-Client aufgeführt. Prüfen Sie daher Docker Hub auf einen passenden Image-Tag, bevor Sie die Festlegung ändern. Docker Hub veröffentlicht außerdem v1.102 für die Patch-Reihe und einen unstable-Tag. Letzteren sollten Sie auf einem Server nicht verwenden.

Führen Sie Upgrades gezielt durch.

docker compose pull tailscale
docker compose up -d
docker compose exec tailscale tailscale version

Wenn 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 Kosten

TS_USERSPACE ist standardmäßig auf true gesetzt. Der Container führt dann einen TCP/IP-Stack im Userspace aus und verwendet niemals /dev/net/tun. Dadurch funktioniert dies auch auf Hosts, die einem Container kein TUN-Gerät überlassen. Eingehender Datenverkehr funktioniert weiterhin, weil eingehende Tunnelverbindungen an denselben Port auf localhost weitergeleitet werden. Genau deshalb benötigt der oben beschriebene Sidecar weder ein Gerät noch zusätzliche Capabilities.

Beim ausgehenden Datenverkehr entstehen die Einschränkungen. 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. Setzen Sie deshalb TS_SOCKS5_SERVER=localhost:1055 für den tailscale-Dienst und ALL_PROXY=socks5://localhost:1055 für die Anwendung. Die Anwendung muss diese Einstellungen berücksichtigen. Anwendungen, die Proxy-Umgebungsvariablen ignorieren, erreichen das Tailnet nicht.

Der Stack selbst hat einige wichtige Einschränkungen. Übertragen werden nur TCP und UDP. Andere IP-Protokolle wie SCTP werden nicht übertragen. ICMP ist auf Ping beschränkt, das 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. Seit Version 1.20 kann ein Knoten im Userspace einen Exit Node oder eine Subnetzroute erreichen, die ein anderer Knoten bekannt gibt. Dies ist jedoch nur über diese Proxys möglich. Der Knoten kann solche Routen außerdem selbst bekannt geben.

Wenn Sie transparenten ausgehenden Datenverkehr benötigen, wechseln Sie zum Kernel-Netzwerkmodus, indem Sie dem tailscale-Dienst drei Dinge hinzufügen.

    environment:
      - TS_USERSPACE=false
    devices:
      - /dev/net/tun:/dev/net/tun
    cap_add:
      - net_admin

Prüfen Sie zuerst mit test -c /dev/net/tun && echo ok, ob der Host dies bereitstellen kann. Unter KVM ist das Gerät vorhanden. Bei einer Container-Virtualisierung, die den Kernel des Hosts gemeinsam verwendet, kann es fehlen. In diesem Fall bleibt nur der Userspace-Modus. Geben Sie dem Container das TUN-Gerät, wenn er als Subnetzrouter für die Bekanntgabe eines privaten Bereichs oder als Exit Node für Ihre anderen Geräte vorgesehen ist. Diese Rollen 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 status

Damit 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 den App-Container nicht erreichen?

Der Tailscale-Container ist nur selbst dem tailnet beigetreten. Wenn die Anwendung im standardmäßigen Compose-Bridge-Netzwerk mit einer eigenen Adresse läuft, leitet der Tailscale-Knoten keinen Datenverkehr dorthin weiter. Von außerhalb des Hosts ist dann nur der veröffentlichte Host-Port erreichbar. Geben Sie der Anwendung network_mode: service:tailscale, damit sie den Netzwerk-Namespace des Tailscale-Containers verwendet, und entfernen Sie ihren ports:-Block. Die Anwendung ist dann im tailnet über den Port erreichbar, auf dem sie Verbindungen entgegennimmt.

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 seinen 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 werden benötigt, 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 langlebige Umgebungen einen wiederverwendbaren Schlüssel mit Tag, der nicht kurzlebig ist. Bei einem Gerät, das sich mit einem Schlüssel mit Tag authentifiziert, ist der Ablauf des Node-Schlüssels standardmäßig deaktiviert. Der Container wird daher nicht nach 180 Tagen aus dem tailnet entfernt, weil noch keine erneute Anmeldung erfolgt ist. Das Hinzufügen eines Tags zu einem bereits angemeldeten Knoten ändert dessen Ablaufzeit erst, wenn er sich erneut authentifiziert. Verwenden Sie kurzlebige Schlüssel nur für Stacks, die Sie häufig löschen, beispielsweise Vorschauumgebungen. Tailscale entfernt ein kurzlebiges Gerät 30 bis 60 Minuten nach seiner letzten Aktivität, und die Administrationskonsole bleibt übersichtlich.

Warum wird mein Container bei jedem Neustart als neuer Rechner angezeigt?

Das Statusverzeichnis wird nicht dauerhaft gespeichert. Daher startet tailscaled ohne Identität und authentifiziert sich als vollständig neuer Knoten. Setzen Sie TS_STATE_DIR=/var/lib/tailscale, das außerhalb von Kubernetes keinen Standardwert hat, und binden Sie diesen Pfad an einen Bind-Mount oder ein benanntes Volume. Ohne TS_STATE_DIR liegt der Status im Arbeitsspeicher und geht bei jedem Neustart verloren. Wenn Sie die Einstellung ohne Mount setzen, sieht zunächst alles korrekt aus, bis zum ersten docker compose down. Stellen Sie sicher, dass tailscaled.state im eingebundenen Verzeichnis vorhanden ist, während der Stack beendet ist.