swarmexec_ Doku
Den Client bedienen — Befehle, Flags, Konfiguration & die TUI.
swarmexec bietet dir docker exec -it, Logs, Port-Forwarding und
Volume-Verwaltung für jeden Container in einem Docker Swarm,
aus einem einzigen Terminal. Ein kleiner Agent pro Node (ein
globaler Swarm-Service) erledigt die Arbeit auf seinem Node; der
Client fragt den Swarm-Manager, welcher Node dein Ziel
ausführt, und verbindet sich dann direkt mit dem Agent dieses Nodes über mTLS.
Diese Seite dokumentiert den Client.
Inhalt
1. Wie es funktioniert
Zwei Binaries, ein Wire-Protokoll:
- agent — läuft als
globalSwarm-Service (eine Task pro Node), bindet den Docker-Socket des Nodes ein und stellt eine schmale gRPC-API bereit, die exec / logs / port-forward in Container auf seinem eigenen Node weiterreicht. Du rollst ihn einmal mitswarmexec initaus. - client (diese CLI) — läuft auf deiner Workstation. Er spricht
mit der Swarm-Manager-API, um aufzulösen, welcher Node dein Ziel
ausführt und wie die Container-ID lautet, und verbindet sich dann direkt mit dem
Agent dieses Nodes auf Port
9443. Es gibt kein Agent-zu-Agent-Mesh.
Die Manager-Verbindung nutzt deinen Docker-CLI-Context (sie berücksichtigt also
--context, ssh://-Bastions, mTLS usw.). Die
Agent-Verbindung wird mit mutual TLS authentifiziert, oder mit einem geteilten
Secret gegen einen selbstsignierten Agent — siehe
Authentifizierung.
2. Installation
Setzt Docker Engine 19.03 oder neuer voraus (API 1.40) — auf dem Manager und auf jedem Node. swarmexec lehnt einen älteren Daemon schon beim Verbinden ab und nennt dabei beide Versionen, statt sich zu verbinden und dann eine Ansicht nach der anderen scheitern zu lassen. Zwei Funktionen brauchen vom Daemon des Nodes etwas mehr: die Live-Auslastung und die Image-Ansicht pro Node wollen Docker 23.0 (API 1.41/1.42) — darunter bleiben sie leer und sagen das auch, statt zu scheitern.
Der Client ist ein einzelnes statisches Binary. Hol dir den aktuellen Release-Build für deine Plattform:
Builds: linux-amd64,
linux-arm64,
darwin-arm64,
darwin-amd64,
windows-amd64.
Das Agent-Image ist öffentlich auf Docker Hub als
logleio/swarmexec-agent verfügbar.
3. Schnellstart
Richte deinen Docker-Context auf einen Swarm-Manager aus und provisioniere die
Agents dann einmalig. init erzeugt ein geteiltes Secret, rollt den
Agent auf jedem Node im selbstsignierten Modus aus und schreibt eine passende
Client-Konfiguration — damit die übrigen Befehle sofort funktionieren.
Baue die Agents wieder mit swarmexec down ab (deine
Client-Konfiguration bleibt dabei unangetastet).
4. Ziel-Selektoren
exec, logs und port-forward nehmen ein
Ziel als erstes Argument. Es wird in dieser Reihenfolge aufgelöst:
| Form | Beispiel | Bedeutung |
|---|---|---|
service | web | Die laufende Task des Service. Bei mehr als einem Replica ist das mehrdeutig — siehe unten. |
service.slot | web.2 | Ein bestimmter Replica-Slot. Während eines Rolling Update gewinnt die neueste Task im Slot. |
task-id | xxh8k1… | Eine Swarm-Task-ID. |
container-id | 3f9a2b… | Ein Container-ID-Präfix. Braucht einen Node: gib --node an, oder er wird durch Scannen der laufenden Tasks gefunden. |
Mehrdeutigkeit. Ein bloßer Service-Name mit mehreren Replicas
lässt sich nicht auf eine einzelne Task auflösen. exec fordert dich
interaktiv auf, aus einer nummerierten Liste zu wählen; logs und
port-forward fragen nie nach — sie geben die Kandidatenliste aus und
beenden sich. Vereindeutige mit einem Slot
(web.0, web.1, …).
5. Konfiguration
Einstellungen kommen aus vier Ebenen, jede überschreibt die vorherige:
Ein Flag überschreibt nur, wenn du es tatsächlich angibst, sodass ein Wert aus der Datei oder der Umgebung erhalten bleibt, solange er nicht ausdrücklich überschrieben wird.
Konfigurationsdatei
Der Standardpfad ist der erste der folgenden, der gesetzt ist:
$SWARMEXEC_CONFIG$XDG_CONFIG_HOME/swarmexec/config.yaml~/.config/swarmexec/config.yaml
Überschreibe ihn mit --config <path>. Die Datei ist YAML; eine
fehlende Datei ist in Ordnung, eine fehlerhafte ist ein Fehler. Sie wird mit
0600 geschrieben (sie kann das geteilte Secret enthalten). Die
Log-Datei liegt standardmäßig daneben —
~/.config/swarmexec/swarmexec.log — sofern du nicht
--log-file setzt. Schlüssel:
| Schlüssel | Typ | Bedeutung |
|---|---|---|
ca | string | CA-Zertifikat, das das Serverzertifikat des Agents verifiziert (mTLS) |
cert | string | Client-Zertifikat — sein CN ist deine Operator-Identität |
key | string | privater Client-Schlüssel |
port | int | Agent-Port (Standard 9443) |
addr_mode | string | hostname (Standard) oder ip — wie ein Node angewählt wird |
server_name | string | überschreibt den TLS-Servernamen, der zur Verifizierung des Agents verwendet wird |
agent_secret | string | geteiltes Secret für einen selbstsignierten Agent |
agent_secret_file | string | liest das Secret aus dieser Datei (hat Vorrang vor agent_secret) |
insecure | bool | überspringt die Prüfung des Agent-Serverzertifikats (selbstsignierte Agents) |
legacy_secret | bool | sendet zusätzlich das rohe Secret, für Agents älter als v1.17.3 — standardmäßig aus; siehe unten |
operator | string | Audit-Identität, wenn kein Client-Zertifikat verwendet wird (Standard: OS-Benutzername) |
logs.format | string | Standard-Log-Format für logs und die TUI: classic | json | logfmt | gelf | raw (leer = classic) |
logs.min_level | string | Standard-Mindeststufe: trace..fatal (weglassen für keinen Stufenfilter) |
ui.dim | float | wie stark der Hintergrund hinter einem offenen Overlay abgedunkelt wird, ein Bruch 0–1 (Standard 0.6; 0 = keine Abdunklung) |
Der logs:-Abschnitt legt Standardwerte für formatbewusstes
Log-Parsing und -Filtern fest; die Flags --log-format /
--min-level des logs-Befehls überschreiben sie:
Inspiziere die effektive, zusammengeführte Konfiguration (Secret maskiert) mit:
addr-mode
hostname (Standard) wählt den gemeldeten Hostnamen des Nodes an;
ip wählt seine angekündigte Adresse an. Verwende ip, wenn
die Node-Hostnamen von deiner Workstation aus nicht auflösbar sind —
init schreibt genau deshalb addr_mode: ip in die
generierte Konfiguration. Der Swarm-Leader meldet 0.0.0.0 für sich
selbst; der Client stellt seine echte Adresse automatisch aus der Raft-Peer-Liste
wieder her.
Umgebungsvariablen
| Variable | Setzt |
|---|---|
SWARMEXEC_CONFIG | Pfad der Konfigurationsdatei |
SWARMEXEC_CA / _CERT / _KEY | mTLS-Material |
SWARMEXEC_PORT | Agent-Port |
SWARMEXEC_ADDR_MODE | hostname / ip |
SWARMEXEC_SERVER_NAME | TLS-Servername |
SWARMEXEC_AGENT_SECRET / _FILE | geteiltes Secret / Secret-Datei |
SWARMEXEC_INSECURE | Prüfung überspringen (1/true/yes/on) |
SWARMEXEC_OPERATOR | Audit-Identität |
SWARMEXEC_UI_DIM | Overlay-Hintergrundabdunklung (ui.dim) |
SWARMEXEC_KEYS | Pfad zur Tastenbelegungsdatei der TUI (Standard: keys.yaml neben der Konfiguration) |
SWARMEXEC_SSH_MULTIPLEX | 0/off/false/no schaltet geteilte ssh-Verbindungen ab (siehe Gemeinsame ssh-Verbindungen) |
DOCKER_CONTEXT | Docker-Context für die Manager-API |
XDG_CONFIG_HOME | Basis für den Standard-Konfigurationspfad |
6. Authentifizierung
Der Client authentifiziert sich gegenüber dem Agent in einem von zwei Modi.
Modus A — mutual TLS (Standard)
Setze ca, cert und key (alle drei
erforderlich). Der Client verifiziert den Agent gegen deine CA und präsentiert sein
Zertifikat; der Agent autorisiert und auditiert dich anhand des
CN des Zertifikats. Das ist der Standard und die empfohlene
Vorgehensweise.
Modus B — selbstsignierter Agent + geteiltes Secret
Einfacher zu betreiben (ein Secret, keine PKI) — das richtet init ein.
Setze agent_secret (oder agent_secret_file) und entweder
eine ca, um den Agent zu verifizieren, oder
insecure: true, um die Verifizierung zu überspringen. Ein
Client-Zertifikat ist optional (aber cert und key müssen
gemeinsam gesetzt oder beide leer sein). Deine Audit-Identität ist der Wert von
operator (Standard: dein OS-Benutzername).
ca, und halte das Secret
aus deiner Shell-History heraus (verwende agent_secret_file oder die
Konfigurationsdatei).
Umstieg auf v1.17.3 — zuerst die Agents aktualisieren
invalid or missing agent secret, obwohl dein Secret stimmt.
Aktualisiere sie zuerst:
legacy_secret: true in der
Client-Konfiguration zusätzlich das rohe Secret — mit der oben beschriebenen
Preisgabe. Entferne es, sobald die Agents aktuell sind, und setze am Agent
-allow-legacy-secret=false, damit kein Client das Credential
versehentlich aufs Kabel legen kann.
7. Docker-Context & SSH
--context wählt den Docker-CLI-Context, der für die Manager-API
verwendet wird. Auflösungsreihenfolge: --context →
$DOCKER_CONTEXT → $DOCKER_HOST → der aktive Context in
~/.docker/config.json → der lokale Socket
unix:///var/run/docker.sock.
docker-Binary aus — er bindet das Docker-Go-SDK ein und spricht die
Manager-API direkt, wobei er alle Context-Metadaten selbst aus Dateien liest. Alles,
was er braucht, ist ein erreichbarer Swarm-Manager-Endpoint (die
Node-Auflösungsaufrufe funktionieren nur gegen einen Manager):
$DOCKER_HOSTauf einem entferntentcp://-Manager (mTLS) — dann ist auf deiner Workstation überhaupt kein Docker installiert;- ein
ssh://-Context — braucht denssh-Client (nicht docker); sowohl der Manager- als auch der Agent-Verkehr tunneln darüber; - der lokale Socket
unix:///var/run/docker.sock— nur der Standard-Fallback und die einzige Option, die einen lokalen Daemon voraussetzt.
docker-CLI selbst wird nur benötigt, um benannte Contexts zu
erstellen (docker context create) — oder erstelle sie mit
swarmexec context create <name> --docker-host …, sodass die
docker-CLI selbst dafür nicht mehr benötigt wird; verwende
$DOCKER_HOST, um benannte Contexts ganz zu überspringen.
(init liest lokale docker login-Anmeldedaten nur für ein
privates Agent-Image — nicht für das öffentliche Standard-Image.)
SSH-Bastion. Wenn der Context-Host ein
ssh://-Endpoint ist, wird die Manager-API über SSH getunnelt — und die
Agent-Verbindung ebenfalls: Da die node:9443-Endpoints der Nodes von
deiner Workstation aus meist nicht routbar sind, tunnelt der Client den
gRPC-Verkehr des Agents automatisch über denselben SSH-Host. Keine zusätzlichen
Flags nötig.
8. Globale Flags
Diese persistenten Flags gelten für jeden Befehl:
| Flag | Standard | Beschreibung |
|---|---|---|
--config | — | Pfad der Konfigurationsdatei (Standard ~/.config/swarmexec/config.yaml) |
--context | — | Docker-Context für die Manager-API; unterstützt ssh:// (auch $DOCKER_CONTEXT) |
--port | 9443 | Agent-Port |
--addr-mode | hostname | Node-Anwähladresse: hostname | ip |
--ca | — | CA-Zertifikat zur Verifizierung des Agents (mTLS) |
--cert | — | Client-Zertifikat (mTLS; CN ist die Operator-Identität) |
--key | — | privater Client-Schlüssel (mTLS) |
--server-name | — | überschreibt den TLS-Servernamen für die Agent-Verifizierung |
--agent-secret | — | geteiltes Secret für einen selbstsignierten Agent |
--agent-secret-file | — | Datei, aus der das geteilte Secret gelesen wird |
--insecure | false | Prüfung des Agent-Serverzertifikats überspringen |
--operator | OS-Benutzername | für das Audit gemeldete Operator-Identität |
--log-level | info | Log-Ausführlichkeit: debug | info | warn | error | off (off deaktiviert das Logging vollständig) |
--log-file | — | Pfad der Log-Datei (Standard swarmexec.log neben der Konfigurationsdatei) |
--info | — | zeigt Version, Lizenz und Kontaktinfo |
--version | — | gibt die Client- und Protokollversion aus |
slog, Textformat) in die Log-Datei und einen
In-Memory-Ringpuffer. In der TUI werden Logs nie ins Terminal geschrieben (das
würde die Anzeige beschädigen) — stattdessen speist der Ringpuffer einen
Live-Viewer in der App (siehe Die TUI).
--log-level off deaktiviert das Logging vollständig; eine Log-Datei,
die nicht geöffnet werden kann, ist nicht fatal (es wird dann nur auf den
Ringpuffer zurückgegriffen).
9. Befehle
init — die Agents provisionieren
Rollt den Agent als globalen Swarm-Service über die Manager-API aus: erstellt ein Docker-Secret mit geteiltem Secret, führt den Agent im selbstsignierten Modus auf jedem Node aus (Host-Port 9443) und schreibt die passende Client-Konfiguration. Führe es einmal pro Swarm aus.
| Flag | Standard | Beschreibung |
|---|---|---|
--image | docker.io/logleio/swarmexec-agent:latest | auszurollendes Agent-Image |
--secret | zufällig | zu verwendendes geteiltes Secret (Standard: ein zufälliges erzeugen) |
--service-name | swarmexec_agent | Name für den Agent-Service |
--port | 9443 | Host-Port, den der Agent veröffentlicht |
--force | false | den Service aktualisieren, falls er bereits existiert |
--save-config | true | die Client-Konfiguration schreiben |
--registry-auth | true | lokale Registry-Anmeldedaten weitergeben, damit Nodes ein privates Image pullen können |
--wait | true | warten, bis die Agents hochkommen, und Fortschritt melden |
--rollout-timeout | 90s | wie lange auf den Start der Agents gewartet wird |
--force erneut aus, um eine neue
Agent-Version auszurollen — siehe
Agents neu ausrollen, ohne das Secret zu ändern.
down — die Agents entfernen
Das Gegenstück zu init: entfernt den globalen Agent-Service und
standardmäßig das Docker-Secret mit dem geteilten Secret. Es rührt deine
Client-Konfiguration nicht an.
| Flag | Standard | Beschreibung |
|---|---|---|
--service-name | swarmexec_agent | Name des zu entfernenden Agent-Service |
--keep-secret | false | das Docker-Secret mit dem geteilten Secret nicht entfernen |
-y, --yes | false | nicht zur Bestätigung auffordern |
doctor — den Swarm diagnostizieren
Prüft die Manager-Verbindung, ob der Agent-Service ausgerollt ist, und testet den
Agent jedes bereiten Nodes auf Erreichbarkeit und Versionsabweichung. Gibt eine
Tabelle pro Node aus
(NODE AGENT VERSION PROTO); beendet
sich mit einem Wert ungleich null, wenn der Manager nicht erreichbar ist oder ein
Agent unhealthy ist. Ein Node mit dem Status too old (init --force)
betreibt einen Agent, der älter ist als dein Client — siehe
Agents neu ausrollen, ohne das Secret zu ändern.
| Flag | Standard | Beschreibung |
|---|---|---|
--connect-timeout | 10s | Connect-Timeout pro Node |
--json | false | JSON statt einer Tabelle ausgeben |
security report — ein Markdown-Bericht über den ganzen Cluster
Das Sicherheitsrisiken-Overlay prüft Service-Specs und zeigt das
Ergebnis auf dem Bildschirm. Dieser Befehl schreibt dieselbe Prüfung — plus die
Checks, die dem Cluster gehören und keinem einzelnen Service — als
Markdown heraus. So lässt sie sich fern vom Terminal lesen, an ein Ticket hängen
oder neben die Stack-Dateien committen und von Release zu Release diffen. Die
Ausgabe geht nach stdout, solange kein -o angegeben
ist, ist also genauso gut pipebar wie speicherbar.
Vier Prüfungen laufen auf Cluster-Ebene, von denen keine ein Service-Spec beantworten kann:
| Prüfung | Schweregrad | Was sie meldet |
|---|---|---|
network-unencrypted — „Overlay-Verkehr ist nicht verschlüsselt“ | medium | ein Overlay-Netz, das Service-Verkehr trägt, ohne Verschlüsselung der Datenebene. Swarm tunnelt Container-Verkehr zwischen Nodes im Klartext über VXLAN, solange das Netz nicht mit --opt encrypted angelegt wurde — und am laufenden Service sieht man so oder so keinen Unterschied. Netze ohne angehängte Services werden nicht gemeldet (sie tragen nichts), und ingress ebenso wenig: es lässt sich gar nicht verschlüsseln, ein Befund darauf wäre nie abzustellen |
autolock-disabled — „Manager sind nicht autolocked“ | medium | der Raft-Speicher der Manager ist nicht im Ruhezustand verschlüsselt. Er enthält jedes Secret, jede Config und den CA-Schlüssel des Clusters, und sein Schlüssel liegt auf derselben Platte — wer die Platte eines Managers mitnimmt, hat alles. Einschalten mit docker swarm update --autolock=true, und den Unlock-Key aufbewahren: ein neu gestarteter Manager fragt danach. Ist die Swarm-Konfiguration nicht lesbar, meldet der Bericht autolock-unknown statt zu raten — „aus“ anzunehmen erfände einen Befund, „an“ wäre eine Falschentwarnung |
agent-proto-mismatch / agent-version-skew / agent-too-old / agent-unreachable | high / medium / low | der Agent ist das, was bei jedem Exec, jedem Log und jedem Port-Forward die Autorisierung durchsetzt — ein Agent, der älter ist als dein Client, setzt womöglich eine Regel nicht durch, die der Client voraussetzt. Ein Protokoll-Mismatch ist high und wiegt schwerer als eine Versionsdifferenz: die beiden Seiten sind sich über den Vertrag selbst uneinig, nicht bloß über den Build. Ein Agent, der nicht geantwortet hat, wird als Lücke ausgewiesen, nicht als bestanden. Für einen unveröffentlichten (dev) Client wird kein Skew gemeldet — er weicht von jedem Release-Agenten naturgemäß ab, dieselbe Ausnahme macht doctor |
unused-secret / unused-config | low | ein Secret oder eine Config, auf die kein Service verweist. Sie wird weiterhin über den Raft-Speicher verteilt und ist für alles lesbar, was einen Manager erreicht — meist ein Zugangsdatum, das jemand rotiert und nie entfernt hat. Der alte Wert ist also noch im Cluster, lange nachdem alle ihn für weg halten |
Was der Bericht über sich selbst sagt. Er wird fern vom Terminal gelesen, von jemandem, der beim Lauf nicht dabei war — also trägt er seinen Kontext mit: welcher Cluster, wann, mit welchem Build. Außerdem hat er einen Abschnitt Not covered — nicht erreichte Agenten, unlesbare Swarm-Konfiguration, leere Node-Liste —, denn Schweigen in einem Sicherheitsbericht liest sich als Entwarnung für Boden, den er nie betreten hat. Die Reihenfolge hängt allein von den Befunden ab, zwei Berichte eines unveränderten Clusters unterscheiden sich also nur im Zeitstempel und lassen sich gegeneinander diffen — was nur deshalb etwas wert ist, weil ein fehlender Befund nicht gefunden heißt und nie nicht geschaut.
Die Datei wird 0600 geschrieben, eventuell nötige
Elternverzeichnisse 0700. Sie nennt jeden Service, jedes Netz und
jedes Secret des Clusters samt jeder gefundenen Schwäche — das ist eine
Angriffskarte, und die hat nichts in einer für jedes Konto der Maschine lesbaren
Datei verloren.
| Flag | Default | Beschreibung |
|---|---|---|
-o, --output | stdout | in diese Datei schreiben statt nach stdout |
--connect-timeout | 10s | Verbindungs-Timeout pro Node beim Prüfen der Agents |
--skip-agents | false | die Node-Agents nicht kontaktieren. Schneller; der Agent-Skew steht dann unter Not covered, statt stillschweigend zu fehlen |
Denselben Bericht schreibt auch das TUI: w im Sicherheitsrisiken-Overlay. Er wird dabei frisch clusterweit erhoben, ist also kein Abzug dessen, was das Overlay zeigt, und läuft außerhalb der UI-Goroutine — die Oberfläche bleibt bedienbar, während er jeden Node abfragt.
stack export / stack diff — ein deployter Stack als Datei
stack export liest einen deployten Stack aus dem Cluster zurück und
schreibt ihn als Compose-förmiges YAML. stack diff vergleicht eine
Stack-Datei mit dem, was tatsächlich läuft, und gibt einen
Unified Diff aus: eine +-Zeile ist etwas, das ein
Deploy der Datei hinzufügen würde, eine --Zeile
etwas, das es entfernen würde. Wie
git diff --exit-code endet der Befehl mit 1, sobald
es Unterschiede gibt — also auch in CI brauchbar.
Im TUI bietet E auf dem Stacks/Services-Tab beides für den Stack unter dem Cursor an — die Stack-Zeile, ein Service darin oder einer seiner Container.
Warum kein Textvergleich. Ein deployter Stack trägt Dinge, die
die Datei nie hatte: den Image-Digest, den der Daemon beim Deploy aufgelöst hat,
seine eigenen Defaults für Restart- und Update-Policy, und den Stack-Namespace vor
jedem Netz, Secret und Volume. Beide als Text zu vergleichen meldet Dutzende
Unterschiede für einen Stack, der exakt synchron ist — und das ist schlimmer als
gar kein Werkzeug, weil man lernt, die Ausgabe zu ignorieren. Stattdessen läuft
die Datei durch Dockers eigenen Compose-Loader und -Converter,
denselben Code, den docker stack deploy nutzt, und beide Seiten
werden anschließend von derselben Funktion reduziert. Beantwortet
wird die Frage „würde ein Deploy dieser Datei etwas ändern?“, nicht
„sind diese beiden Dateien gleich geschrieben?“. Daemon-Defaults fallen
auf beiden Seiten weg; ein Wert, der nicht der Default ist,
erscheint weiterhin — es wird also nichts Echtes verborgen.
Ein Export ist eine Beschreibung, kein Backup. Der Wert eines
Secrets ist in der Engine-API nur schreibbar und nie wieder
auslesbar, und Volume-Inhalte liegen auf den Nodes. Beides wird deshalb als
external deklariert, und die exportierte Datei sagt das in ihrem
eigenen Kopf: den Stack anderswo neu aufzubauen heißt, zuerst die Secrets
anzulegen. Spec-Felder, die das Rendering nicht trägt (tty,
ulimits, seccomp- und AppArmor-Einstellungen und einige weitere),
stehen an derselben Stelle — denn der gefährliche Fehler eines
Vergleichswerkzeugs ist nicht die falsche Antwort, sondern das selbstsichere
Schweigen. „Keine Unterschiede“ kommt daher immer mit seinen Vorbehalten.
Variablen werden aus deiner Umgebung interpoliert, genau wie bei
docker stack deploy. Eine Datei mit ${TAG} beschreibt
also für ein anderes TAG einen anderen Stack, und der Diff hängt von
der Umgebung ab, in der er läuft — was richtig ist: alles andere würde „keine
Änderung“ melden für ein Deploy, das das Image austauscht.
| Befehl | Beschreibung |
|---|---|
stack ls | die auf dem Cluster deployten Stacks |
stack export <stack> | den Stack als Compose-YAML schreiben; mit -o in eine Datei, sonst nach stdout |
stack diff <datei> [stack] | eine Datei mit dem deployten Stack vergleichen. Der Stack-Name ist standardmäßig der Dateiname ohne Endung; --stack oder ein zweites Argument überschreibt ihn. Endet bei jedem Unterschied mit 1 |
stack deploy — eine Stack-Datei einspielen, nach Prüfung
Lädt eine Stack-Datei, lässt die Sicherheitsprüfungen über das
laufen, was tatsächlich ausgerollt würde, und spielt sie ein. Was das gegenüber
docker stack deploy wertvoll macht, ist der Gate: die
Datei wird geprüft, bevor irgendetwas angelegt ist, und wenn etwas
oberhalb von „informativ“ gefunden wird, hält der Deploy an und fragt.
Im TUI macht E → Deploy a file dasselbe: zuerst die Befunde, d fährt fort, Esc geht weg. Zu diesem Zeitpunkt ist nichts angelegt — genau dafür ist die Pause da.
Die Prüfungen sind kein zweiter Regelsatz. Die Datei wird in genau
die ServiceSpec-Werte umgewandelt, die der Deploy einreichen würde, und
darauf laufen die vorhandenen acht Analyzer — dieselben Prüfungen,
die im Baum den Schild setzen und das !-Overlay sowie den Security-Report
füllen. Zwei Sätze würden auseinanderdriften: eine Regel, die an einer Stelle
verschärft wird und an der anderen nicht, heißt, dass der Gate eine Datei
durchlässt, die der Baum im selben Moment markiert, in dem sie läuft — und wem
„nichts gefunden“ gesagt wurde, dem wurde etwas Falsches gesagt. Außerdem
sehen die Prüfungen so, was der Cluster tun wird, nicht was die
Datei sagt; Composes Defaults und Kurzschreibweisen liegen dazwischen.
--yes heißt nicht „ignoriere die Befunde“. Es heißt
„frag nicht“, und ein Lauf, der etwas oberhalb von „informativ“ findet,
verweigert trotzdem und endet mit Exit ungleich null. Alles andere
würde den Gate zur Formalie machen, sobald ihn jemand in CI einbaut. Um trotz
Befunden auszurollen, muss man --force schreiben — anderer Tastendruck,
andere Rechtfertigung hinterher. Ein nicht-interaktiver Lauf ohne Antwort auf stdin
zählt als Nein.
| Flag | Beschreibung |
|---|---|
--check | nur prüfen und anhalten. Endet mit 1, wenn etwas oberhalb von „informativ“ gefunden wurde — damit lässt sich eine Pipeline absichern |
-y, --yes | nicht fragen. Rollt aus, wenn die Prüfungen sauber sind; verweigert, wenn nicht |
--force | trotz Befunden ausrollen, ohne Rückfrage |
--prune | Services des Stacks entfernen, die die Datei nicht mehr deklariert. Standardmäßig aus, wie bei docker: eine Datei, die nur ein Teil des Stacks ist, ist weit öfter ein Versehen als eine Löschanweisung |
Was ein Deploy tut, in dieser Reihenfolge: Netze, dann Secrets, dann Configs, dann Services — ein Service, der auf etwas noch nicht Vorhandenes verweist, scheitert, und der Stack bleibt halb angewandt zurück. Externe Netze werden zuerst geprüft, denn ein nicht existierendes ist der häufigste Grund, warum ein Deploy auf halber Strecke stehen bleibt. Ein vorhandenes Secret wird nie überschrieben: sein Wert ist in Swarm unveränderlich, ein Secret zu ändern heißt, ein neues unter neuem Namen anzulegen. Scheitert ein Deploy doch mittendrin, wird das bereits Angewandte zusammen mit dem Fehler gemeldet, statt es erraten zu lassen.
Agents neu ausrollen, ohne das Secret zu ändern
Früher oder später musst du die Agents in einem bereits provisionierten Swarm neu
ausrollen: ein Agent hängt, doctor zeigt einen Node als
too old (init --force), ein Kommando scheitert mit „agent is
older than this client (missing RPC) — update it with swarmexec init
--force“, oder ein Node wurde neu aufgesetzt und sein Agent kam nie
zurück. Die Lösung ist, init mit --force erneut
auszuführen — das Flag bedeutet genau den Service aktualisieren, falls er
bereits existiert. Ohne das Flag ist ein init auf einen
bestehenden Service ein Usage-Fehler, der dich auffordert, --force
zu ergänzen; geändert wird dabei nichts.
Damit wird der Agent auf jedem Node neu ausgerollt, und das geteilte Secret
bleibt unangetastet. Docker-Secrets sind unveränderlich, also
findet init das vorhandene swarmexec_agent_secret,
meldet es als reusing existing und behält den Wert, der bereits im
Cluster liegt. Jeder Client, der vorher funktioniert hat, funktioniert danach
weiter — es muss nichts neu verteilt werden.
--secret.
Existiert das Secret bereits, kann es nicht überschrieben werden: dein Wert wird
nicht auf den Cluster angewendet — aber er wird
trotzdem in deine Client-Konfiguration geschrieben. Ist er nicht
zufällig der echte Wert des bestehenden Secrets, weisen die Agents dich ab dem
Moment zurück, und der Fehler zeigt sich erst später und sieht nach einem defekten
Agent aus statt nach einer falschen lokalen Konfiguration. init warnt
in genau diesem Fall — nimm die Warnung ernst. Um das Cluster-Secret wirklich zu
ändern, musst du das Secret zuerst entfernen (kein Service darf es referenzieren)
und init erneut ausführen.
--save-config steht standardmäßig auf true, ein
erneutes Ausrollen schreibt also auch
~/.config/swarmexec/config.yaml neu. Übergib
--save-config=false, wenn die lokale Client-Konfiguration unberührt
bleiben muss — etwa wenn du die Agents von einem Rechner aus neu ausrollst, dessen
Konfiguration bereits korrekt ist, oder aus der CI.
Danach prüfen: doctor meldet einen Status pro Node;
ok auf jedem Node heißt, dass Client, Agent und Secret wieder
zusammenpassen.
ps — Tasks auflisten
Listet mögliche Tasks/Container und den Node, auf dem jede läuft. Nimmt einen
optionalen Service-Filter. Spalten:
SERVICE SLOT CONTAINER NODE IP UPTIME.
Dies spricht nur mit der Manager-API — es funktioniert sogar, bevor die Agents
erreichbar sind.
| Flag | Standard | Beschreibung |
|---|---|---|
--json | false | JSON statt einer Tabelle ausgeben |
exec — einen Befehl ausführen / eine Shell öffnen
Exec in einen Container, der irgendwo im Swarm läuft. Ohne Befehl öffnet es
/bin/sh. Ein TTY wird automatisch zugewiesen, wenn stdin ein Terminal
ist und du keinen Befehl angegeben hast; erzwinge es mit -t. Der eigene
Exit-Code des Remote-Befehls wird unverändert weitergegeben.
| Flag | Standard | Beschreibung |
|---|---|---|
-i, --stdin | true | stdin offen halten |
-t, --tty | auto | ein TTY zuweisen (auto: true genau dann, wenn stdin ein Terminal und kein Befehl ist) |
-u, --user | — | Benutzername oder UID (z. B. 1000:1000) |
-w, --workdir | — | Arbeitsverzeichnis im Container |
-e, --env | — | Umgebungsvariablen setzen (KEY=VALUE, wiederholbar) |
--node | — | Node-Hinweis/-Override für container-id-Ziele |
--connect-timeout | 10s | Timeout für die Verbindung zum Agent |
logs — Logs streamen
Streame die Logs eines Containers von irgendwo im Swarm.
| Flag | Standard | Beschreibung |
|---|---|---|
-f, --follow | false | neue Log-Zeilen weiter streamen |
--tail | 0 | Anzahl der Zeilen vom Ende, mit denen begonnen wird (0 = alle) |
-t, --timestamps | false | jeder Zeile einen Zeitstempel voranstellen |
--since | 0 | nur Logs neuer als dies (z. B. 10m, 1h) |
--log-format | classic | Zeilen parsen als classic | json | logfmt | gelf | raw (Standard aus der Konfiguration, sonst classic) |
--min-level | — | nur diese Stufe und höher anzeigen: trace | debug | info | warn | error | fatal |
--grep | — | nur Zeilen anzeigen, deren (geparste) Nachricht diesem Go-Regexp entspricht |
--node | — | Node-Hinweis/-Override für container-id-Ziele |
--connect-timeout | 10s | Timeout für die Verbindung zum Agent |
Formatbewusstes Parsen & Filtern. --log-format
sagt dem Client, wie er jede Zeile lesen soll, damit er eine Stufe und
eine Nachricht herausziehen kann: classic extrahiert eine
Stufe aus einer reinen Textzeile, json parst JSON im
Logstash-Stil (level/message-Felder), logfmt
parst den key=value-Stil, den viele Go-Anwendungen, der Docker-Daemon
und HashiCorp-Tools verwenden (msg/message,
level/lvl/severity, ts/time),
gelf
parst Graylog-GELF-JSON (numerische Syslog-Stufe) und raw reicht
Zeilen unverändert durch. --min-level verwirft dann alles unterhalb
der gewählten Stufe, und --grep behält nur Zeilen, deren geparste
Nachricht dem Regexp entspricht.
--min-level-Filter immer, damit mehrzeilige Stacktraces nicht
verloren gehen. Dieselben Standardwerte lassen sich einmalig unter dem
logs:-Konfigurationsabschnitt setzen (Flags
überschreiben).
Folgen über Container-Ersetzung hinweg. Mit -f auf
einem service- oder service.slot-Ziel folgt logs
weiter, wenn der gestreamte Container durch ein Rolling Update, einen Neustart
oder eine Neuplanung ersetzt wird: es löst den aktuell laufenden Container des
Service neu auf (denselben Slot, oder denselben Node bei einem
global-Service), verbindet sich automatisch neu — wie
docker service logs -f — und gibt eine gedimmte Hinweiszeile aus
(container replaced; reconnected to <id> on <node>). Es
wartet bis zu ~30s, bis ein Ersatz eingeplant wird, bevor es aufgibt, und stoppt
sauber, wenn der Service entfernt wird. Ein bloßes container-id-Ziel hat
keinen Nachfolger und stoppt daher einfach wie zuvor.
port-forward (Alias pf) — einen lokalen Port weiterleiten
Bindet einen lokalen TCP-Port und leitet ihn an einen Port innerhalb eines
Containers weiter, ohne diesen Port im Cluster zu veröffentlichen. Der lokale Port
entspricht standardmäßig dem entfernten. Ein Service als Ziel leitet an genau
eine seiner Tasks weiter (die, auf die das Ziel aufgelöst wird),
nicht über Replicas hinweg. Standardmäßig an 127.0.0.1 gebunden.
Drücke Ctrl-C zum Stoppen.
| Flag | Standard | Beschreibung |
|---|---|---|
--address | 127.0.0.1 | lokale Adresse zum Binden (Loopback hält den Port aus deinem Netzwerk fern) |
--node | — | Node-Hinweis/-Override für container-id-Ziele |
--connect-timeout | 10s | Timeout für die Verbindung zum Agent |
volume ls — Volumes auflisten
Listet Volumes über alle Nodes hinweg und welche Nodes jedes halten.
Swarm-Volumes sind node-lokal, der Client fragt daher jeden Node ab und aggregiert.
Optionaler Teilstring-Namensfilter. Spalten:
VOLUME DRIVER NODES USED BY AGE
(plus SIZE mit --size).
| Flag | Standard | Beschreibung |
|---|---|---|
--size | false | zusätzlich die Größe jedes Volumes auf der Platte berechnen (langsamer: du pro Volume) |
--sort | name | sortieren nach: name | nodes | used | age | size (size impliziert --size) |
--reverse | false | Sortierrichtung umkehren |
--connect-timeout | 10s | Connect-Timeout pro Node |
--json | false | JSON statt einer Tabelle ausgeben |
volume rm — ein Volume entfernen
Entfernt ein Volume auf jedem Node, der es hält (--all), oder auf
bestimmten Nodes (--node, wiederholbar). Eines von beiden ist
erforderlich.
| Flag | Standard | Beschreibung |
|---|---|---|
--all | false | auf jedem Node entfernen, der das Volume hält |
--node | — | nur auf diesen Nodes entfernen (wiederholbar) |
--force | false | docker's Force-Flag durchreichen |
-y, --yes | false | nicht zur Bestätigung auffordern |
--connect-timeout | 10s | Connect-Timeout pro Node |
ui — interaktive TUI
Eine interaktive Ansicht von Containern und Volumes, mit integriertem exec, logs und Port-Forwarding. Braucht ein interaktives Terminal. Siehe Die TUI für die Tasten.
| Flag | Standard | Beschreibung |
|---|---|---|
--connect-timeout | 10s | Timeout für die Verbindung zu einem Agent |
config show — Konfiguration inspizieren
Gibt die effektive, zusammengeführte Client-Konfiguration mit maskiertem Secret aus.
context (Alias ctx) — Docker-Contexts verwalten
Erstellt und verwaltet die Docker-Contexts, die --context (und
$DOCKER_CONTEXT) für die Manager-API auflösen. swarmexec schreibt
docker's eigenen On-Disk-Speicher, sodass hier erstellte Contexts mit der
docker-CLI austauschbar sind — und du brauchst docker nicht mehr
installiert, selbst um einen zu erstellen. Der eingebaute
default-Context kann nicht entfernt werden.
context create <name> — erstellt einen Context, der auf einen Manager-Host zeigt (nimmt genau ein Namensargument):
| Flag | Standard | Beschreibung |
|---|---|---|
--docker-host | — | erforderlich — Docker-Daemon-Endpoint: ssh:// | tcp:// | unix:// | npipe:// |
--description | — | optionale Beschreibung |
--ssh-jump | — | SSH-Jump-Host(s) für einen ssh://-Context, kommagetrennt (mehrstufiges ProxyJump / -J); wird sowohl in die Docker-API- als auch in die Agent-Tunnel-Verbindung eingefügt |
--use | false | ihn außerdem zum aktuellen Context machen |
context ls (Alias list) — Contexts auflisten; Spalten NAME CURRENT DOCKER ENDPOINT (der aktive mit * markiert):
context use <name> — den aktuellen Context setzen:
context rm <name> [name...] (Alias remove) — einen oder mehrere Contexts entfernen:
| Flag | Standard | Beschreibung |
|---|---|---|
-f, --force | false | erforderlich, um den aktuellen Context zu entfernen (seine Auswahl wird auf default zurückgesetzt) |
Gemeinsame ssh-Verbindungen
Bei einem ssh://-Kontext erreicht swarmexec den Cluster gleich
zweimal über ssh: die Docker-Manager-API über ssh's Connection-Helper, und jeden
Node-Agenten über einen eigenen Tunnel zum selben Host. Jedes Exec, jeder
Log-Stream, jedes Port-Forward, jeder Stats-Poll und jeder Refresh-Zyklus
öffnete bisher eine frische ssh-Verbindung — ein TCP-Handshake, ein
Schlüsselaustausch und eine Authentifizierung pro Dial, und noch einmal pro
Jump-Host — gegen einen Bastion-Host, der einen Moment vorher schon verbunden
war.
swarmexec lässt sie jetzt einen Transport teilen, mit OpenSSHs eigenem
ControlMaster: die erste Verbindung zu einem Ziel öffnet ihn, jede
weitere wird ein Kanal darauf. Gemessen an einem Drei-Node-Cluster hinter einem
Jump-Host ging swarmexec doctor von 8 Authentifizierungen
und 3,6 s auf 2 und 1,0 s. Am Verkehr selbst
ändert sich nichts, und der Effekt wächst mit dem, was man in einer Sitzung tut.
-
Die Control-Sockets liegen in
$XDG_RUNTIME_DIR/swarmexec/ssh/(ersatzweise im Cache-Verzeichnis), angelegt mit0700— ein Control-Socket ist eine lebende, authentifizierte Sitzung, also bleibt er im eigenen Verzeichnisbaum und niemals in/tmp. - Ein Socket pro Ziel und Route: zwei Kontexte, die denselben Manager über verschiedene Jump-Hosts erreichen, teilen sich nichts — sie sind nicht dieselbe Verbindung.
- Die geteilte Verbindung überlebt das Kommando um 60 s, damit das nächste billig ist, und beendet sich dann von selbst. Danach hält nichts von swarmexec mehr eine Verbindung.
- Ein Socket, den ein hart beendeter Master hinterlassen hat, ist harmlos: ssh stellt fest, dass er tot ist, und öffnet stattdessen eine normale Verbindung.
-
Unter Windows nicht verfügbar — dessen OpenSSH kennt keine geteilten
Verbindungen. Überall sonst schaltet
SWARMEXEC_SSH_MULTIPLEX=0es ab — relevant, wenn SieControlPathselbst konfigurieren, denn swarmexecs Einstellung steht auf der ssh-Kommandozeile und hat damit Vorrang vor~/.ssh/config.
10. Die TUI
swarmexec ui hat sieben Tabs — Stacks/Services (1),
Volumes (2), Forwards (3),
Networks (4), Secrets (5),
Nodes (6), Configs (7) — eine
Context-Sidebar am rechten Rand und einen
zweizeiligen Footer: oben die Tastenhinweise pro Tab, dann eine Statuszeile mit dem
aktiven Docker-Context
(ctx <name>, sodass immer klar ist, auf welchem Cluster du
bist), einer Live-Cluster-Zusammenfassung, der Anzahl der Forwards und — im
Volumes-Tab — wie viele Volumes du ausgewählt hast. Diese Tasten funktionieren auf
jedem Tab:
Die Tab-Leiste ist responsiv: in einem schmaleren Terminal wechselt sie auf Kurzlabels — St/Sv, Vol, Fwd, Net, Sec, Node, Cfg — damit alle sieben Tabs sichtbar bleiben, statt dass die letzten abgeschnitten werden. (Cfg ist Configs.) Die Zifferntasten und die Maus-Klickflächen bleiben unverändert.
| Taste | Aktion |
|---|---|
| ? | das Tastenbelegungs-Overlay öffnen — die vollständige, lebende Tastenreferenz (sie wird aus deiner Tastenbelegung erzeugt, umbelegte Tasten erscheinen also korrekt); die einzeilige Fußzeile hat nur Platz für die meistgenutzten Tasten |
| Tab | zum nächsten Tab wechseln |
| 1–8 | zu Stacks/Services / Volumes / Forwards / Networks / Secrets / Contexts / Nodes / Configs springen |
| j k | runter / hoch (auch ↓ ↑) |
| r | den aktiven Tab und die Cluster-Zusammenfassung aktualisieren |
| y | die aktuelle Liste in die Zwischenablage kopieren (OSC52) |
| m | Maus-Capture umschalten (aus = das eigene Auswählen/Kopieren deines Terminals) |
| ` | den Live-Log-Viewer öffnen / schließen (siehe unten) |
| q | beenden |
Stacks/Services-Tab
Tab 1, früher Containers. Er ist ein Baum, Stack → Service → Container, und darin stecken Exec, Logs, Port-Forwarding, Inspect und die Service-Editoren.
:latest-Versionsprüfung. Swarm fixiert :latest zum
Deploy-Zeitpunkt auf einen Digest, sodass ein mit :latest getaggter
Service tatsächlich ein festes Image ausführt. swarmexec löst das gegen die
Registry auf (mit deinen lokalen Docker-Anmeldedaten) und annotiert die
Service-Zeile direkt nach der Image-URI: die
echte Version in Klammern — aus dem
org.opencontainers.image.version-Label des Images gelesen — und einen
Aufwärtspfeil ↑, wenn das aktuelle :latest der
Registry ein neuerer Digest ist als der, auf den der Service fixiert ist (z. B.
nginx:latest (1.4.0) ↑).
Best-Effort und gecacht: Registry-Fehler lassen die Zeile einfach ohne Annotation.
Der IMAGE-Abschnitt des Inspect-Overlays zeigt dieselbe Version und,
wenn ein neueres Image existiert, eine auswählbare
newer version available-Zeile — geh darauf und drücke
u (oder Enter), um den Service zu aktualisieren.
Version setzen mit u — nicht nur, wenn es ein Update gibt.
u hängt nicht mehr davon ab, dass ein Upgrade angeboten wird.
Es steht in jedem Service-Inspect zur Verfügung, dessen Image-Tags swarmexec aus
der Registry lesen konnte — bei versionsgepinnten und bei
:latest-Services gleichermaßen — und aus jeder Zeile des Overlays,
sodass du den Hinweis nie suchen musst. Dieselbe Version erneut setzen, das aktuell Laufende
pinnen oder auf einen älteren Tag zurückgehen sind ganz
normale Anwendungsfälle; du musst nicht warten, bis es ein Update gibt. Der
Text in der Fußzeile zeigt dir, in welcher Situation du bist:
u update version, wenn eine neuere Version gefunden wurde (dann gibt
es auch die newer version available-Zeile mit einem konkreten Ziel), und
u set version, wenn nicht.
Was u öffnet, hängt davon ab, was die Registry weiß. Bei einem
versionsgepinnten Service mit einer neueren Version ist es
die Versionsauswahl: ein Eingabefeld, dessen Autovervollständigung die
neueren Tags derselben Familie vorschlägt, höchste zuerst und mit der
höchsten vorbelegt. Läuft der Service bereits auf dem neuesten Tag, ist es
dieselbe Auswahl, nur fallen die Vorschläge auf alle Tags des Repos zurück
(vorbelegt mit dem laufenden Tag) — so pinnst du oder rollst zurück. Ein Service auf
:latest bekommt die Auswahl ebenfalls, mit allen bekannten Tags: eine konkrete
Version dort zu wählen ist der Weg, einen :latest-Service vom
gleitenden Tag weg zu pinnen — genau das, was der Risiko-Befund
unpinned-image (siehe das Sicherheits-Overlay weiter unten) verlangt. Die
einzige Ausnahme ist ein :latest-Service mit anstehendem
Digest-Update: Der bleibt bei der einzelnen Bestätigung auf den
neuen Digest der Registry, die er immer war — eine Auswahl würde dort dazu verleiten,
latest einzutippen, was auf das nackte Tag auflöst und damit still genau die
Digest-Pinnung wegwerfen würde, die das Update auffrischen soll.
In der Auswahl ist die Vorschlagsliste auf 25 Einträge begrenzt (ein viel
genutztes Repo listet leicht Hunderte), und das Feld bleibt frei
editierbar: du kannst jeden vorhandenen Tag eintippen, auch einen älteren,
um ihn zu pinnen oder zurückzurollen auf ein bewährtes Release. Ein eingetippter
Tag wird gegen die Repo-Tags geprüft (ein nicht gelisteter Tag wird abgelehnt), und die
Auswahl eines älteren Tags zeigt vor dem Anwenden eine Downgrade-Warnung.
In jedem Fall ist die Änderung ein ServiceUpdate / Rolling Update, hinter einer
Bestätigung. Liefert die Registry überhaupt keine Tags und gibt es auch kein
konkretes Ziel — eine private oder nicht erreichbare Registry oder ein per Digest gepinnter
Service —, bekommst du einen erklärenden Hinweis, und es wird nichts
geändert.
| Taste | Aktion |
|---|---|
| / | die Suchleiste öffnen (filtert nach Service / Container / Node) |
| h l | ein-/ausklappen über alle drei Ebenen. h klappt die Zeile unter dem Cursor zu — ein Stack die ganze Gruppe, ein ausgeklappter Service seine Container — und wo nichts mehr zuzuklappen ist, springt es stattdessen heraus zum Elternknoten, sodass wiederholtes Drücken nach oben läuft: Container → Service → Stack. l klappt die Zeile unter dem Cursor auf oder steigt in ihr erstes Kind ab, wenn sie schon offen ist |
| Enter | auf einem Container: das Aktionsmenü öffnen; auf einem Service oder einem Stack: ihn ein-/ausklappen |
| s | Stack-Gruppierung umschalten — gruppierter Baum ⟷ flache Service-Liste (siehe unten) |
| L | Logs (großes L) — auf einem Container dessen eigene Logs, auf einem Service die aggregierten Logs aller seiner Tasks |
| ! | das Sicherheitsrisiken-Overlay für die gesamte Service-Liste öffnen (siehe unten) |
| p | Port-Forward für die Task unter dem Cursor |
| i | den Node unter dem Cursor inspizieren — ein navigierbares Overlay mit tabellarischer Zusammenfassung; bewege die Auswahl mit ↑/↓ oder j/k, y/Enter kopiert die ausgewählte Zeile, eine Tab-Leiste oben benennt die drei Ansichten, 1/2/3 wählen Tabelle / Stats / rohes Daemon-JSON direkt aus und t schaltet reihum durch (Esc/q/i schließt). Auf einem Service bearbeitet das Overlay ihn auch: s Skalieren, f Force-Update, p Ports, l Labels, e Env, n Netzwerke, S Secrets, v Mounts, A Aliase; D diagnostiziert, warum er nicht überall läuft; X entfernt ihn |
| X | einen Service direkt aus dem Baum entfernen (großes X, also Shift+x) — ohne Umweg über das Inspect-Overlay. Worauf es wirkt, richtet sich nach der Zeile unter dem Cursor: auf einer Service-Zeile wird dieser Service entfernt; auf einer Container-Zeile der zugehörige Service des Containers, denn eine einzelne Task lässt sich nicht für sich entfernen — Swarm würde sie sofort neu einplanen; auf einer Stack-Zeile wird nichts entfernt, stattdessen weist eine kurze Notiz in der Fußzeile darauf hin, dass die Services eines Stacks einzeln entfernt werden müssen oder mit docker stack rm <stack>. Es läuft über dieselbe Bestätigung wie das X im Inspect weiter unten — es löscht den Service dauerhaft und stoppt alle seine Tasks; das kann nicht rückgängig gemacht werden. Eine feste Großbuchstaben-Taste, keine umbelegbare Keymap-Aktion; die Fußzeile führt es rot als X remove, das ?-Overlay listet es ebenfalls. Das X im Inspect-Overlay bleibt unverändert |
Jeder Service wird angezeigt (auch einer, der auf null skaliert ist), jede Zeile
gerendert wie eine docker service ls-Zeile — Name, Modus,
laufende/gewünschte Anzahl, Image und veröffentlichte Ports — und danach eingefärbt,
wie es dem Service tatsächlich geht: Die laufenden/gewünschten Tasks geben die
Grundfarbe vor (aqua = alle Tasks oben, orange = teilweise, rot = unten, grau = auf
null skaliert), ein fehlschlagender Healthcheck überschreibt sie
(siehe unten). Ein ▸/▾-Marker zeigt, ob ein Service
eingeklappt oder ausgeklappt ist; seine laufenden Container verschachteln sich
darunter. Ein Service mitten in einem Rolling Update trägt ein
farbiges Badge in seiner Baumzeile — ⟳ updating oder
↺ rolling back — und derselbe Status erscheint
im Service-Inspect-Overlay; es verschwindet, sobald das Update abgeschlossen ist.
Eingefärbt nach Healthcheck, nicht nur nach Replica-Zahl. Die Farbe
einer Service-Zeile kam bisher allein aus laufend gegen gewünscht — ein Service,
dessen sämtliche Container ihren Healthcheck rissen, stand also weiterhin
als ruhiges aquafarbenes 3/3 da. Die Zahl stimmte, die Zeile log.
Der Task-Status von Swarm hilft hier nicht weiter: Eine Task ist
running, während ihr Container jede Probe verfehlt — das Urteil muss
also vom Node kommen. Eine fehlschlagende Probe gilt jetzt als Verschlechterung
derselben Art wie ein fehlendes Replica:
- alle geprüften Container unhealthy → die Zeile wird rot, das ist so schlimm wie „läuft keiner“;
- ein Teil unhealthy → orange, wie bei einem halb ausgerollten Update;
- alle healthy → die Farbe, die die Replica-Zahl der Zeile ohnehin gegeben hat, unverändert;
- Container ohne Healthcheck → die Zeile wird gar nicht umgefärbt. Über sie ist nichts bekannt, und Raten wäre derselbe Fehler in die andere Richtung.
Die Marker. Eine Service-Zeile trägt
✖ N unhealthy in Rot oder
◌ N starting in Gelb, solange die Proben
noch nicht durch sind; ein Container-Blatt trägt
✖ unhealthy bzw.
◌ starting. Unhealthy sticht starting — das
ist das, wo man ran muss. Eine Stack-Zeile fasst denselben Marker über alles darunter
zusammen (siehe Nach Stack gruppiert weiter unten). Ein
gesunder Container bekommt
keinen Marker, einer ohne Healthcheck ebenso wenig;
so bleibt der Marker eine Aussage. Sie stehen neben den
cpu/mem-Ressourcenmarkern weiter unten, und die Gesundheit
kommt in der Zeile zuerst: Sie ist das, was der Zahl direkt daneben
widerspricht.
Up 3 days (healthy)) — genau die Zeichenkette, die auch
docker ps ausgibt. Der Parser ist bewusst streng: Was
er nicht kennt, wird zu kein Urteil statt zu einer Vermutung. Formuliert
der Daemon diese Zeile eines Tages um, hört swarmexec also auf, Bescheid zu wissen,
statt einen fehlschlagenden Container als gesund zu melden. Es gilt dieselbe
Voraussetzung wie für die Nutzungszahlen: Es braucht Agents aus diesem Release oder
neuer, ein noch nicht neu ausgerolltes Cluster zeigt also gar keine
Health-Marker — ausrollen mit
swarmexec init --force.
Marker für die tatsächliche CPU- und Memory-Nutzung. Neben diesen
Badges kann eine Zeile auch zeigen, was sie gerade wirklich verbraucht —
bisher konnte swarmexec nur anzeigen, was der Scheduler gebucht hat (siehe
den Nodes-Tab weiter unten). Überschreitet ein Container oder ein Service
70 %, bekommt er einen orangen Marker, ab 90 %
einen roten — am Ende der Zeile, mit der Ressource in Worten
(cpu für CPU, mem für Memory) und dem Prozentwert
(cpu 94 %,
mem 91 %). Sind beide heiß, erscheinen
beide. Worte statt Symbolen, bei gleicher Breite: Ein Marker, dessen Bedeutung man
erst nachschlagen muss, tut seinen Job nicht — und Buchstaben können in keinem
Terminal an der Darstellung scheitern. Unter 70 % wird nichts gezeichnet, damit die
Marker ein Signal bleiben und
keine Tapete. Eine Service-Zeile nimmt den
schlechtesten ihrer Replicas, nicht den Durchschnitt — ein
Durchschnitt verdeckt genau den einen Container, der gleich stirbt, und der ist der
interessante. (Der absolute Memory-Wert einer Service-Zeile ist die Summe über die
Replicas, der Prozentwert das Maximum.)
Wovon der Prozentwert ein Prozentwert ist. Gemessen wird gegen das, was der Container tatsächlich nutzen darf: sein eigenes Limit, wenn er eins hat, sonst die Kapazität des Nodes. Genau darauf kommt es an — 91 % eines 256-MB-Limits heißt, ein OOM-Kill steht bevor; 91 % eines 64-GB-Nodes ist eine ganz andere Geschichte. Die Stats-Ansicht des Inspect-Overlays zeigt pro Container beide Seiten dieser Division und benennt die Bezugsgröße unter der Tabelle.
Stats); jeder Agent sampelt seine eigenen Container im Hintergrund und
antwortet aus dem Speicher, der Client fragt ihn also einfach im ohnehin
vorhandenen Refresh-Zyklus ab. Daraus folgen zwei Dinge. Ein
CPU-Prozentwert ist eine Differenz zwischen zwei Messungen und
existiert daher erst, wenn der Agent zwei genommen hat (ein paar Sekunden) —
solange zeigen die Ansichten …, nie 0 %; Memory braucht
keine Differenz und ist ab der ersten Messung da. Und ein Agent sampelt nur,
solange auch jemand hinschaut: Rund eine Minute nach der letzten
Anfrage hört er auf und vergisst seine Messwerte, statt veraltete auszuliefern.
Deshalb brauchen die ersten Zahlen nach dem Öffnen der UI einen Moment. Das ist
Absicht — ein Agent, dem niemand zusieht, soll nichts kosten.
Stats-RPC —, liefert schlicht keine Messwerte. Die Marker
bleiben aus, der Node-Block fehlt, alles andere bleibt unberührt; der Client nimmt
so einen Node zudem für eine Weile aus dem Rennen, statt ihn bei jedem Refresh neu
anzufragen. Auf einem Cluster, das noch nicht neu ausgerollt wurde, gibt es deshalb
überhaupt keine Nutzungszahlen und keine Health-Marker, bis die
Agents aktualisiert sind —
ausrollen mit swarmexec init --force. Das ist mit
Abstand der häufigste Grund, nichts zu sehen.
Nach Stack gruppiert. docker stack deploy versieht
jeden Service, den es anlegt, mit dem Label
com.docker.stack.namespace. swarmexec liest dieses Label aus der
Service-Spec, die der Manager ohnehin schon geliefert hat — Swarm kennt kein
Stack-Objekt, das Label ist die einzige Verbindung — und verschachtelt den Baum
drei Ebenen tief: Stack → Service → Container. Eine
Stack-Zeile zeigt den Stack-Namen, wie viele Services darunter hängen und die
summierte laufende/gewünschte Task-Anzahl
((3 svc · 7/8)), eingefärbt nach genau derselben Regel
wie eine Service-Zeile — der summierten laufend/gewünscht-Zahl, mit derselben
Healthcheck-Übersteuerung darüber. Dahinter stehen die Sammelzähler, sobald
sie nicht null sind: ⟳ n — Services in
diesem Stack mitten in einem Rolling Update —
🛡 n — Services mit einem
handlungswürdigen Sicherheitsbefund, dasselbe Schild, das die
Service-Zeilen tragen — und danach der Health-Marker,
✖ N unhealthy oder
◌ N starting, summiert über jeden
Container jedes Service im Stack. Auch ein zugeklappter Stack verrät so, ob darin etwas
Aufmerksamkeit braucht. Gerade beim Health-Marker zählt das: Die Stack-Zeile ist die
Ebene, die man zuerst überfliegt — ein trügerisches „läuft alles“ ist hier
schlimmer als eine Ebene tiefer, nicht harmloser.
Services, die mit docker service create angelegt wurden, tragen kein
Stack-Label; sie landen gesammelt unter (no stack),
das immer zuletzt einsortiert wird und damit nie echte Stacks nach unten drängt.
Nichts wird versteckt — jeder Service erscheint weiterhin genau
einmal. Stacks werden nach Namen sortiert und starten ausgeklappt, sodass der
gruppierte Baum dieselben Services zeigt wie der flache; von dir gesetzte Falten
überleben die Auto-Aktualisierung.
Die Gruppierung ist standardmäßig an, greift aber erst, wenn
mindestens ein Service im Cluster tatsächlich ein Stack-Label trägt — auf einem
Cluster ohne Stacks sieht der Baum genau aus wie bisher, ohne sinnlosen
(no stack)-Elternknoten. s schaltet zwischen gruppiertem
Baum und flacher Service-Liste um (die Fußzeile führt es als s stacks);
gibt es nirgends ein Stack-Label, sagt es das, statt einen identischen Baum neu zu
zeichnen. Die Taste ist als stack_group in
keys.yaml umbelegbar.
Die Suche (/) bleibt unverändert: der Filter greift weiterhin auf Service, Container und Node. Ein Stack, den der Filter leert, verschwindet aus dem Baum, und die Zähler jeder Stack-Zeile beschreiben, was tatsächlich darunter gelandet ist — sie folgen also dem Filter, statt weggefilterte Services anzupreisen.
Das Aktionsmenü bietet Logs, Bash,
Sh, Shell as user… (fragt nach einem
Benutzer/einer UID, wie docker exec -u — für Images, deren
Standardbenutzer die benötigten Werkzeuge oder Rechte nicht hat) und
Port forward (nicht verfügbare Shells
werden nach einem Test ausgegraut). In der Suchleiste behält Enter den
Filter und kehrt zur Liste zurück; Esc löscht den Filter und schließt
die Leiste.
Sicherheitsrisiken (!). Bei jedem Abruf der Service-Liste lässt swarmexec eine Reihe kleiner statischer Analyzer über die Spec jedes Service laufen — über dieselbe Spec, die der Manager ohnehin schon geliefert hat. Es gibt also keinen zusätzlichen API-Aufruf, der Agent ist nicht beteiligt, und in deinen Containern wird nichts ausgeführt. Ein Service mit einem Befund, der Handlung verdient, wird im Service-Baum mit einem vorangestellten 🛡 markiert — in einem Slot fester Breite vor dem Namen, sodass die Schilde eine senkrechte Scan-Spalte bilden. ! öffnet das Sicherheitsrisiken-Overlay, und w darin schreibt einen clusterweiten Markdown-Bericht — der mehr abdeckt als das Overlay, weil er die Prüfungen ergänzt, die dem Cluster gehören und keinem Service.
Die acht Prüfungen, die es heute gibt. Zuerst die, die einen Service markieren können, am Ende die rein informativen:
| Prüfung | Schweregrad | Was sie meldet |
|---|---|---|
docker-socket — „Docker socket mounted in“ | high | der schwerwiegendste Befund hier. Ein Bind-Mount, dessen Quelle der Socket des Docker-Daemons ist — /var/run/docker.sock, /run/docker.sock oder jeder Quellpfad, der auf /docker.sock endet. Alles in diesem Container kann mit dem Socket sprechen, und wer mit dem Socket sprechen kann, kann auf diesem Node einen privilegierten Container starten — das ist faktisch root auf dem Host, ganz gleich, als wer der Container selbst läuft. Read-only entschärft das nicht: der Socket ist eine API, keine Datei, deren Inhalt zählt — der Befund sagt das bei einem Read-only-Mount auch selbst dazu. Genannt wird der Zielpfad, an dem der Socket eingehängt ist |
added-capability — „capability NAME added“ | high / medium | eine Linux-Capability, die der Service seinem Container zusätzlich gibt. Swarm kennt kein --privileged, Capabilities sind also der Weg, über den ein Service zusätzliche Macht auf dem Host anfordert — deshalb lohnt der Blick auf die hinzugefügte Menge. Acht gelten als high, jeweils mit der Begründung, die der Befund mitführt: ALL (vergibt jede Capability), SYS_ADMIN (fast root: Mount-, Namespace- und cgroup-Kontrolle), SYS_MODULE (kann Kernel-Module laden), SYS_PTRACE (kann andere Prozesse einsehen und steuern), SYS_RAWIO (Raw-I/O-Zugriff auf Geräte), DAC_READ_SEARCH (umgeht Leserechte-Prüfungen im Dateisystem), NET_ADMIN (volle Kontrolle über das Netzwerk des Nodes), NET_RAW (kann Rohpakete fälschen und mitschneiden). Jede andere hinzugefügte Capability ist medium — ein Privileg über die Standardmenge hinaus. Ein Befund pro hinzugefügter Capability; beide Schreibweisen werden erkannt, in beliebiger Groß-/Kleinschreibung (CAP_SYS_ADMIN und SYS_ADMIN) |
host-network — „runs on the host network“ | high | der Service hängt am Netzwerk namens host: der Container teilt sich den Netzwerk-Stack des Nodes, es gibt also keine Netzwerk-Isolation, und das Published-Port-Mapping von Swarm greift nicht mehr. Er erreicht alles, was der Node erreicht — einschließlich Diensten, die nur auf localhost lauschen und die man üblicherweise für aus einem Container heraus unerreichbar hält |
unconfined — „seccomp disabled“ / „AppArmor disabled“ | high | die Sandbox auf Kernel-Ebene ist für den Container abgeschaltet: seccomp steht auf unconfined — der Container darf jeden Syscall absetzen, womit die wichtigste Hürde gegen Kernel-Exploits wegfällt — oder die AppArmor-Einschränkung ist deaktiviert. Beides wird getrennt gemeldet; ein Service, der beides abschaltet, bekommt zwei Befunde |
root-user — „runs as root“ | high | die Spec legt den Container ausdrücklich auf root fest — User=root oder UID 0 (auch numerische Formen wie 00 oder +0 werden erkannt; betrachtet wird nur der Benutzerteil von user:group) |
secret-in-env — „secret in environment variable“ | high | ein nach einem Credential aussehender Env-Schlüssel enthält einen literalen Wert in der Service-Spec, lesbar für jeden, der die Spec lesen darf. Nutze stattdessen ein Docker-Secret oder die *_FILE-Konvention |
root-user — „no user set“ | low | kein Benutzer ist gesetzt, der Container läuft also als der Standardbenutzer des Images — oft root. Nur informativ, markiert den Service also nicht: ein nicht gesetzter Benutzer ist bei fast jedem Service der Swarm-Standard, und ob es wirklich root ist, hängt vom USER des Images ab, das die Manager-Spec nicht preisgibt |
no-resource-limits — „no resource limits“ | low | der Task setzt weder ein CPU- noch ein Memory-Limit; ein außer Kontrolle geratener Container kann damit den ganzen Node aufbrauchen und alles andere darauf aushungern. Nur informativ — siehe unten |
unpinned-image — „image not pinned“ | low | das Image ist auf :latest festgelegt oder trägt überhaupt kein Tag (was auf :latest hinausläuft) — die laufende Version kann sich ohne Spec-Änderung ändern, ein Deploy ist damit nicht reproduzierbar. Ein per Digest gepinntes Image (…@sha256:…) ist exakt reproduzierbar und wird nie gemeldet, und ein Doppelpunkt, der zum Port des Registry-Hosts gehört (registry:5000/img), wird nicht für ein Tag gehalten. Nur informativ — siehe unten |
Warum die informativen Prüfungen nie einen Service markieren. Erst
ein Befund oberhalb von low — high oder medium — macht einen Service
handlungsrelevant und setzt das 🛡 an seine Baumzeile. Die
Low-Befunde (no-resource-limits, unpinned-image und
„no user set“ aus root-user) treffen auf nahezu jeden Service in
einem echten Cluster zu: ein Badge dafür würde fast jede Zeile mit einem
Schild versehen und genau das Signal auf einen Blick zerstören, für das der Marker da
ist. Unter den Tisch fallen sie trotzdem nicht — sobald ein Service wegen etwas
anderem markiert ist, stehen seine Low-Befunde im Overlay bei den übrigen, also dort,
wo sie sich wirklich zu lesen lohnen.
secret-in-env überhaupt auf Umgebungsvariablen, und ihr Befund nennt
ausschließlich den Schlüssel der Env-Variable; der Wert wird nie in
den Befund eingelesen, nie gerendert und nie kopiert. Die Prüfung ist außerdem auf Ruhe
ausgelegt: sie vergleicht ganze, durch _ getrennte Tokens
(PASSWORD, PASSWD, PASS,
PASSPHRASE, SECRET, TOKEN,
APIKEY, CREDENTIAL(S), PRIVATEKEY sowie die
Paare API_KEY, ACCESS_KEY, PRIVATE_KEY,
SECRET_KEY, CLIENT_SECRET, AUTH_TOKEN), sodass
COMPASS, PASSENGER_PORT oder BYPASS_AUTH
nicht gemeldet werden. Schlüssel, die ein Credential nur referenzieren,
sind ausgenommen (_FILE, _PATH, _URL,
_URI, _NAME, _ID, _TYPE,
_ENABLED, _REQUIRED, _LENGTH,
_TIMEOUT, _TTL, _EXPIRY,
_ALGORITHM) — ebenso Werte, die offensichtlich kein Credential sind:
ein absoluter Pfad, ein Boolean, eine Zahl.
Das Overlay listet jeden markierten Service („N of M service(s) flagged · 8 checks“), nach Service gruppiert, jeden Befund in einer eigenen Zeile mit einem Schweregrad-Punkt — ● high, ● medium, ● low — einem kurzen Titel und einer einzeiligen Erklärung. Befunde stehen nach Schweregrad absteigend. Sobald ein Service wegen etwas Handlungsrelevantem markiert ist, werden auch seine rein informativen Low-Befunde aufgeführt. Ist der Service unter dem Cursor darunter, wird er vorab hervorgehoben und ins Bild gescrollt. Ist nichts markiert, sagt das Overlay das ausdrücklich und nennt jede Prüfung, die gelaufen ist („Checked: Docker socket mounted in, added Linux capabilities, host network, seccomp / AppArmor disabled, container user, secrets in environment variables, missing resource limits, unpinned image.“) — so sagt die Entwarnung auch, was sie abdeckt. Ist die Service-Liste noch nicht geladen, meldet es es wurde noch nichts gescannt, statt fälschlich Entwarnung zu geben. j/k scrollen, Esc (oder q, oder erneut !) schließt.
security_risks in keys.yaml umbelegbar.
i öffnet ein Inspect-Overlay für den Node unter dem
Cursor — auf einem Service zeigt es
docker service inspect, und auf einem Container-Blatt
das Swarm-Task-Inspect (die Sicht des Managers auf diese Instanz:
State, Slot, Node, Container-ID, Container-Spec, Ressourcen und Statushistorie).
Beide kommen vom Swarm-Manager. Das Overlay öffnet sich mit einer
tabellarischen, operator-first Zusammenfassung mit Abschnitten,
die nach operativer Relevanz geordnet sind — zuerst Netzwerke, Labels,
Volumes/Mounts und Secrets (und Configs), dann Ports, Image, Modus, Env,
Ressourcen, Placement und Update-Policy (plus State, Node und Container-ID bei
einem Container/einer Task), mit IDs und Zeitstempeln zuletzt. Die erste Zeile des
Overlays — innerhalb des Rahmens und fest über dem scrollenden Inhalt — ist eine
Tab-Leiste, die seine drei Ansichten benennt:
table 1 stats 2 raw json 3, die aktuelle in der
Akzentfarbe. Sie hat dieselbe Form wie die Tab-Leiste des Hauptfensters — ein Label,
gefolgt von der Ziffer, die es auswählt. 1, 2 und 3
springen direkt zu Tabelle, Stats und rohem Daemon-JSON, und
t schaltet weiterhin reihum Tabelle → Stats → rohes
JSON und wieder zurück. Der Footer-Hinweis lautet nur noch
1-3/t view, und der Rahmentitel wiederholt die aktuelle Ansicht nicht mehr —
er heißt schlicht inspect service foo, denn die Leiste sagt bereits, wo
du bist. Beachte, dass die tabellarische Ansicht die Task-Sicht des Managers ist,
nicht ein vollständiges node-lokales docker container inspect des
laufenden Containers — ein echtes Container-Inspect über den Agent ist geplant.
Die Stats-Ansicht. Die mittlere der drei zeigt die tatsächliche Ressourcennutzung — dieselben Messwerte, die auch den Baum markieren; das Öffnen kostet also keinen zusätzlichen Aufruf, und die Zahlen aktualisieren sich weiter, solange die Ansicht offen ist. Sie ist eine Tabelle, eine Zeile pro Container des Service (oder der eine Container eines Task-Inspects):
Eine Tabelle, weil ein nackter Prozentwert nicht lesbar ist, wenn man nicht ohnehin
weiß, wie das Werkzeug misst — 94 % wovon? Jede Zelle bringt ihre
eigenen Einheiten mit, 0.07 / 4.00 cores braucht also
keine Legende, und der Prozentwert steht direkt neben der Zahl, von der er ein
Prozentwert ist. Mehrere Replicas vergleicht man, indem man eine Spalte
entlangliest, statt gestapelte Blöcke gegeneinanderzuhalten. Die Spalte
HEALTH formuliert das Urteil in Worten — healthy,
unhealthy, starting oder none für einen
Container, der gar keinen Healthcheck deklariert. none und
healthy sind mit Absicht zwei verschiedene Worte und
nicht ein Wort und eine Lücke: „kein Healthcheck konfiguriert“ und „die Probe läuft
durch“ sind verschiedene Tatsachen, und eine Lücke läse sich wie das Zweite. Ein
Container, den kein Agent gemeldet hat — nicht erreichbarer Node, ein für den
Stats-RPC zu alter Agent oder ein erst seit der letzten Messung
laufender Container —, zeigt in beiden Ressourcenspalten no reading
statt einer Null, die sich wie „im Leerlauf“ läse.
Die Fußnote unter der Tabelle benennt den Nenner: das eigene Limit des Containers, wenn er eins hat, sonst die Kapazität des Nodes. Genau das macht einen Prozentwert erst aussagekräftig — 85 % eines 8-GiB-Limits und 85 % eines 64-GiB-Nodes sind zwei verschiedene Geschichten. Memory steht in binären Einheiten (MiB/GiB), denselben, die auch das Node-Detail verwendet und die docker selbst meldet; dieselbe Zahl liest sich also nie an zwei Stellen unterschiedlich. Bei mehr als einem gemessenen Container kommt unter der Tabelle eine Zusammenfassung dazu: CPU und Memory des schlechtesten Replicas — nicht der Durchschnitt, der genau den einen Container verdeckt, der gleich OOM-gekillt wird — plus das gesamte Memory über alle. Der t-Eintrag im ?-Hilfe-Overlay lautet entsprechend cycle table / stats / raw JSON.
Das Overlay ist navigierbar und auswählbar, nicht nur ein einfaches Scrollen: Datenzeilen sind einzeln auswählbar und du bewegst den Cursor Zeile für Zeile mit ↑/↓ oder j/k (Abschnittsüberschriften und Leerzeilen werden übersprungen). y kopiert die ausgewählte Zeile immer über OSC52 in deine Zwischenablage — derselbe Yank-Mechanismus, der auch anderswo verwendet wird — sodass du eine einzelne ID, einen Mount oder eine Adresse greifen kannst, ohne Text von Hand zu markieren. Enter kopiert ebenfalls, außer auf einer einklappbaren NETWORKS-Zeile, wo es stattdessen diese Zeile aus-/einklappt (siehe unten). Alle drei Ansichten (die tabellarische Zusammenfassung, die Stats-Ansicht und das rohe JSON, erreichbar über 1–3 oder reihum mit t) sind Zeile für Zeile auswählbar und kopierbar. Ein Footer listet immer die verfügbaren Tasten, sodass auf einen Blick sichtbar ist, was du tun kannst; bei einem Service zeigt der Footer auch die untenstehenden Bearbeitungstasten.
| Taste | Aktion |
|---|---|
| ↑ ↓ j k | die Auswahl zwischen Datenzeilen bewegen (Überschriften/Leerzeilen übersprungen) |
| y | die ausgewählte Zeile in die Zwischenablage kopieren (OSC52) — immer |
| Enter | auf einer einklappbaren NETWORKS-Zeile diese Ebene aus-/einklappen — die Zeilen sind zwei Ebenen tief, ein Netzwerk und sein N containers-Drilldown; auf jeder anderen Zeile sie kopieren (OSC52) |
| 1 2 3 | eine Ansicht direkt auswählen — 1 tabellarische Zusammenfassung, 2 Stats (tatsächliche CPU-/Memory-Nutzung, siehe unten), 3 rohes Daemon-JSON; es sind die Ziffern, die die Tab-Leiste zeigt |
| t | reihum durch die drei Ansichten schalten — tabellarische Zusammenfassung → Stats (tatsächliche CPU-/Memory-Nutzung, siehe unten) → rohes Daemon-JSON → zurück; alle drei auswählbar, und die Tab-Leiste oben markiert die aktuelle |
| a | Aktionsmenü — jeder Service-Editor und jede Aktion in einer Liste, ohne Shift (nur Service; siehe unten) |
| s f p l e n S v A | den Service bearbeiten — Skalieren / Force-Update / Ports / Labels / Env / Netzwerke / Secrets / Mounts / Aliase (nur Service; S ist groß, Shift+s, um es von s Skalieren zu unterscheiden; A ist ebenfalls groß; siehe unten) |
| Esc q i | das Overlay schließen |
Der NETWORKS-Abschnitt ist einklappbar und
beantwortet schon eingeklappt die Frage „unter welcher Adresse ist das erreichbar“:
jedes angeschlossene Netzwerk erscheint als eingeklappte Zeile
+ <network> 🔒 vip 10.0.5.2/24 (2 dns names). Ein
🔒-Schlosssymbol nach dem Netzwerknamen markiert ein
verschlüsseltes Overlay-Netzwerk (Data-Plane-Verschlüsselung,
--opt encrypted); sein Fehlen bedeutet unverschlüsselt. Bei einem
Service-Inspect heißt die Adresse vip — die virtuelle
IP des Service in diesem Netzwerk, die Adresse, auf der der Swarm-Load-Balancer
antwortet, gelesen aus dem ServiceInspect des Managers
(Endpoint.VirtualIPs). Bei einem
Container/Task-Inspect heißt sie addr: die eigene
Adresse dieses Containers dort. Ein Service im Endpoint-Modus dnsrr
hat überhaupt keine VIP — das ist so gewollt und kein fehlender Wert —, deshalb steht
statt einer Lücke im ausgeklappten Netzwerk die Zeile
no vip — dnsrr endpoint mode, the DNS name resolves to the containers.
Das ingress-Netzwerk steht ebenfalls hier, obwohl es in keiner Spec
vorkommt — Swarm hängt den Service von sich aus daran (siehe unten). Es wird
hinter den Netzwerken aus der Spec angehängt, nie zwischen sie
gemischt, und seine Kopfzeile lautet
+ ingress vip 10.0.0.250/24 (routing mesh): der Hinweis
(routing mesh) steht dort, wo eine DNS-Namen-Zählung nur
(0 dns names) ergeben könnte.
Geh auf eine Netzwerkzeile und drücke Enter, um sie aus- oder einzuklappen.
Ausgeklappt listet sie die DNS-Namen auf, die auf den Service/Container in diesem
Netzwerk auflösen — den Service-Namen, tasks.<service> und alle
benutzerdefinierten Aliase —, sodass du sehen kannst, unter welchem DNS-Namen er
erreicht wird und welche Aliase er pro Netzwerk trägt. Darunter steht eine zweite
einklappbare Zeile, + 2 containers (im Singular
+ 1 container); Enter darauf öffnet die tiefste Ebene: eine
spaltenbündige Zeile pro Container, web.1 10.0.1.5/24 host-a — Task-Name,
seine Adresse in diesem Netzwerk und der Node, auf dem er läuft (replizierte Tasks
heißen <service>.<slot>, globale
<service>.<node>, da sie keinen Slot haben).
Gefiltert wird dabei über den Soll-Zustand (desired state) der Task,
nicht über ihren aktuellen: gelistet wird jede Task, die der Manager weiterhin laufen
lassen will — auch solche, die erst preparing, assigned oder
starting sind. Sie halten ihre Adresse bereits, und während eines Rolling
Updates sind das die meisten; ein Filter auf den aktuellen Zustand würde den Drilldown
also genau dann leeren, wenn er am interessantesten ist. Eine Task, die der Manager
aufgegeben hat — Soll-Zustand shutdown, also durch ein Rolling Update
ersetzt, weggeskaliert oder fehlgeschlagen —, erscheint nicht: ihre Adresse ist wieder
freigegeben und kann längst einem anderen Container gehören, sie zu zeigen wäre also
aktiv falsch. Eine Task, die noch keinen Traffic bedient, trägt ihren Zustand in
Klammern am Zeilenende, web.1 10.0.1.5/24 host-a (starting); eine laufende
hat kein Suffix. Die Zeilen stammen aus einem TaskList der Manager-API,
gefiltert auf den Service, und sind Best-Effort — ein API-Fehler lässt den Drilldown
einfach leer. Die beiden Ebenen klappen unabhängig voneinander; klappst du das
Netzwerk ein, verschwindet die Container-Ebene mit.
Die ingress-Zeile ist diejenige, die in keiner Spec steht: Swarm hängt
einen Service von sich aus an das ingress-Netzwerk, sobald er einen Port im Modus
ingress veröffentlicht. Diese Anbindung taucht in der Service-Spec
nirgends auf — ihre vip ist aber genau die Adresse, auf der das
Routing Mesh tatsächlich antwortet, und das ist das Erste, was du
wissen willst, wenn ein veröffentlichter Port sich merkwürdig verhält. Deshalb bekommt
sie eine eigene Zeile, angehängt hinter den Netzwerken aus der Spec und leicht anders
geformt: auf ingress löst kein Service-DNS-Name auf, deshalb trägt diese Kopfzeile dort,
wo andere Netzwerke ihre Namen zählen, den Hinweis (routing mesh).
Ausgeklappt listet sie stattdessen die über ingress veröffentlichten Ports
auf, die den Service überhaupt dorthin gebracht haben — der Grund für die Zeile —, je eine
Zeile pro Port: published 2222 -> 22/tcp. Ports, die im Modus
host veröffentlicht sind, umgehen das Routing Mesh und werden dort
bewusst nicht aufgeführt. Darunter geht es genauso in die Tiefe wie bei jeder anderen
Netzwerkzeile: dieselbe Ebene + 1 container, mit der eigenen Adresse jedes
Containers im ingress-Netzwerk. Ein Service, der keinen Port über ingress
veröffentlicht, hat dort keine VIP — und damit auch keine solche Zeile.
Ein GitLab-Service, der SSH auf 2222 veröffentlicht, beide Ebenen ausgeklappt:
Kopieren innerhalb von NETWORKS liefert die Adresse ohne
Maske — 10.0.5.2, die Form, die du in ein curl oder
ping einfügst: auf einer Netzwerkzeile deren vip/addr, auf einer
Container-Zeile die Adresse dieses Containers. Eine Netzwerkzeile ohne Adresse (dnsrr)
kopiert wie bisher den Netzwerknamen. Auf beiden Arten einklappbarer Zeilen schaltet
Enter um; auf jeder anderen Zeile kopiert es (und y kopiert
immer).
Wenn du einen Service inspizierst (nicht ein Container/Task-Blatt,
dessen Overlay schreibgeschützt bleibt), stellt der Overlay-Footer auch mehrere
Bearbeitungstasten bereit, jede ein Manager-API-ServiceUpdate, das ein
Rolling Update / eine Rekonziliation der Tasks des Service auslöst.
Fang mit a an — dem Aktionsmenü. Im Service-Inspect (nur
dort) öffnet a eine einzige Liste mit jedem Editor und jeder Aktion, die
das Overlay bietet — so musst du dir keine ~14 Tasten mit Groß-/Kleinschreibung
merken (d/D, s/f,
p/l/e, n/S/v,
r/P, R, X). Das ist der auffindbare Weg
ohne Shift — und kein abgespeckter: ein Eintrag schließt das Menü
und öffnet genau das, was auch die direkte Taste öffnet, beide sind also
gleichwertig und keine Varianten mit unterschiedlichem Verhalten.
Die Einträge in ihrer Reihenfolge:
Update image version… (Set image version…, wenn es keine neuere
gibt — auf einen Tag deiner Wahl pinnen oder zurückgehen ist genauso legitim),
Diff spec (previous → current), Roll back to the previous version,
Why — placement diagnosis, Scale, Force-update,
Edit ports, Edit labels, Edit env, Edit networks,
Edit secrets, Edit mounts, Edit resources,
Edit placement, Remove service
— der destruktive, deshalb rot markiert — und Cancel. Navigiert wird mit
j/k (g/G springen zum ersten/letzten
Eintrag), Enter wählt aus; Esc — oder der Eintrag
Cancel — schließt das Menü, ohne etwas zu tun. Die Image-Versionsauswahl steht
an erster Stelle, weil eine Version zu setzen die gewöhnlichste
Service-Änderung überhaupt ist; ihre direkte Taste u funktioniert
weiterhin aus jeder Zeile des Inspect. Ist das Image eines, für das sich keine
Version wählen lässt — eine Referenz ohne Tag, oder eine Registry, die die Tags des
Repos nicht herausgibt, typischerweise eine private ohne Zugangsdaten —, bleibt der
Eintrag in der Liste und sagt das, statt stillschweigend zu
verschwinden, und verweist auf das docker service update --image, das
funktioniert. Die direkte Taste zu jedem Eintrag:
| Taste | Aktion |
|---|---|
| d | diff — zeigt ein Unified-Diff der aktuellen Spec des Service gegen seine vorherige (was das letzte Rolling Update geändert hat), unter Verwendung von Swarms PreviousSpec. Geänderte Felder werden feldweise gruppiert mit - removed / + added-Zeilen aufgelistet (Image, Modus/Replicas, Env, Labels, Netzwerke, Aliase, Secrets, Mounts, Ports, Placement-Constraints, Spread-Präferenzen, Ressourcen-Limits/-Reservierungen); unveränderte Felder werden weggelassen. Wenn der Service nie aktualisiert wurde (keine vorherige Spec), sagt es das. Schreibgeschütztes Overlay — j/k scrollen, Esc schließt |
| R | auf die vorherige Version zurückrollen (großes R, also Shift+r, um es von r Ressourcen-Limits zu unterscheiden) — das Gegenstück zum d-Diff darüber: es macht das letzte Rolling Update rückgängig. Der Bestätigungsdialog zeigt genau dieses Diff umgekehrt unter „This will undo:“, denn das ist es, was der Rollback tatsächlich ändert — eine Zeile, die das Update hinzugefügt hat, steht dort als - removed, und eine, die es entfernt hat, kommt als + added zurück. Die Vorschau ist auf 12 Einträge begrenzt, gefolgt von einem Hinweis „… and N more“, der für das vollständige Diff auf d verweist (haben sich nur Metadaten geändert, meldet er, dass es keine feldweisen Unterschiede gibt). Beim Bestätigen läuft ein serverseitiger Rollback — ein Manager-API-ServiceUpdate mit ServiceUpdateOptions{Rollback: "previous"}, kein clientseitiges Neuanwenden der PreviousSpec — der Manager führt ihn also selbst aus, beachtet dabei die Rollback-Konfiguration des Service (Parallelität, Verzögerung, Failure-Action) und meldet den Fortschritt als Update-Status rollback_started: genau das oben beschriebene Badge ↺ rolling back, auf der Baumzeile und in diesem Overlay. Ein Service, der nie aktualisiert wurde, hat keine vorherige Spec — du bekommst einen erklärenden Hinweis, keinen Fehler. Auch über das a-Aktionsmenü erreichbar. Wie d, D und X ist das eine feste Inspect-Taste, keine umbelegbare Keymap-Aktion |
| s | scale — fragt nach einer neuen Replica-Anzahl und wendet sie an (nur replizierte Services; ein globaler Service meldet, dass er nicht skaliert werden kann) |
| f | force-update — deployt den Service neu, ohne seine Spec zu ändern (das Äquivalent zu docker service update --force: erhöht TaskTemplate.ForceUpdate), nach einer Bestätigung. Jede Task wird neu gestartet / neu geplant, womit du einen Service losbekommst, der in einem unvollständigen Zustand feststeckt (z. B. 1/2 Replicas). Ein ServiceUpdate (Rolling Update) |
| X | den Service entfernen (großes X) — löscht ihn dauerhaft und stoppt alle seine Tasks, hinter einer Bestätigung; kann nicht rückgängig gemacht werden. Ein Manager-API-ServiceRemove; bei Erfolg schließt sich das Inspect-Overlay und der Service-Baum aktualisiert sich |
| D | Placement diagnostizieren (großes D) — beantwortet warum ein Service nicht überall läuft, wo du es erwartest in einer Ansicht, statt ihn über mehrere docker-Befehle zu verfolgen. Für einen globalen Service listet es jeden Node mit ✓ laufend oder ✗ und dem Grund, warum er ausgeschlossen ist (Verfügbarkeit drain/pause, ein down-Zustand, ein nicht erfüllter Placement-Constraint oder eine Plattform-Diskrepanz) — deshalb kann ein 3-Node-Cluster legitim 2/2 anzeigen (der dritte Node ist nicht geeignet). Für einen replizierten Service listet es jede Task, die nicht läuft, mit der eigenen Meldung des Schedulers (Constraints, unzureichende Ressourcen, Image-Pull-Fehler, …). Constraints, die client-seitig nicht geprüft werden können (engine.labels.*), werden vermerkt, nicht geraten. Schreibgeschütztes Overlay |
| p | veröffentlichte Ports bearbeiten — öffnet einen gestagten Listeneditor der Ports des Service, jeder in der Form PUBLISHED:TARGET[/proto] (proto tcp|udp|sctp, Standard tcp), z. B. 8080:80/tcp |
| l | Labels bearbeiten — derselbe gestagte Listeneditor über key=value-Einträge |
| e | Umgebungsvariablen bearbeiten — derselbe gestagte Listeneditor über die Env der ContainerSpec, jede als KEY=VALUE eingegeben (z. B. LOG_LEVEL=debug); der Schlüssel ist erforderlich, der Wert darf leer sein und selbst = enthalten. Wie Ports/Labels/Mounts erlaubt dieser Editor sehr wohl e zum Bearbeiten, sodass du einen Wert an Ort und Stelle anpassen kannst. Anders als die übrigen Editoren öffnet e hier den Eintrag in einem scrollbaren mehrzeiligen Textbereich (statt eines einzeiligen Feldes), sodass lange Werte wie GITLAB_OMNIBUS_CONFIG bequem zu bearbeiten sind — tippe frei (Enter fügt einen Zeilenumbruch ein), Ctrl-S speichert, Esc bricht ab. Das Anwenden ersetzt die Env des Service in einem ServiceUpdate (Rolling Update) |
| n | Netzwerke bearbeiten — ein gestagter Listeneditor über die Netzwerkanschlüsse des Service; da ein Eintrag nur ein Name ist, ist er nur Hinzufügen/Entfernen (kein e Bearbeiten). Die Hinzufügen-Eingabe vervollständigt Netzwerknamen automatisch (schlägt Netzwerke vor, an die der Service noch nicht angeschlossen ist, oder tippe selbst eines). Das Anwenden ersetzt die Anschlüsse in einem ServiceUpdate. DNS-Aliase werden auch von hier aus bearbeitet: wähle in diesem Editor ein Netzwerk und drücke A, um eine gestagte Liste der DNS-Aliase des Service in diesem Netzwerk zu öffnen (a hinzufügen / e bearbeiten / d löschen / y kopieren / u rückgängig / w anwenden / Esc abbrechen); das Anwenden ersetzt die Aliase dieses Netzwerks in einem ServiceUpdate. Funktioniert auch, wenn der Service noch keine Aliase hat. (Du musst ein neu hinzugefügtes Netzwerk erst anwenden, bevor du seine Aliase setzen kannst.) |
| S | Secrets bearbeiten (großes S, also Shift+s, um es von s Skalieren zu unterscheiden) — derselbe gestagte Listeneditor wie Netzwerke über die Secrets, die der Service referenziert; ein Eintrag ist nur ein Name, also nur Hinzufügen/Entfernen (kein e Bearbeiten). Die Hinzufügen-Eingabe vervollständigt Secret-Namen automatisch (schlägt Secrets vor, die der Service noch nicht verwendet, oder tippe selbst eines). Das Anwenden ersetzt die Secret-Referenzen des Service in einem ServiceUpdate (Rolling Update); jedes Secret wird unter /run/secrets/<name> eingehängt. Funktioniert auch, wenn der Service aktuell keine Secrets hat |
| v | Mounts bearbeiten — ein gestagter Listeneditor über die Volumes und Bind-Mounts des Service. Hinzufügen (a) oder Bearbeiten (e) öffnet ein kleines Formular statt eines einzelnen Textfeldes: eine Bind-Mount-Checkbox, eine Quelle (wenn die Box nicht angehakt ist, ist es ein Volume-Name mit Autocomplete bestehender Cluster-Volumes; wenn angehakt, ist es ein Host-Pfad), ein Container-Pfad und eine Read-only-Checkbox. (Intern ist jeder Eintrag weiterhin volume:NAME:TARGET[:ro] / bind:/host/path:TARGET[:ro].) Bind-Quellen und alle Ziele müssen absolute Pfade sein. Das Anwenden ersetzt die Mounts in einem ServiceUpdate (Rolling Update). Bind-Mount-Schutz (weich): wenn die gestagte Liste Bind-Mounts enthält, zeigt das Anwenden zuerst eine Warnung, die die Nodes auflistet, auf denen der Service eingeplant werden könnte (berechnet aus Placement-Constraints plus Node-Rolle/-Labels/-Verfügbarkeit), und erinnert dich daran, dass jede Bind-Quelle auf allen davon bereits existieren muss — swarmexec kann Host-Pfade nicht verifizieren (der Agent hat keinen Zugriff auf das Host-Dateisystem), daher werden Bind-Pfade nicht vervollständigt oder auf Existenz geprüft; es ist nur ein Hinweis |
| r | Ressourcen-Limits bearbeiten — ein kleines Formular, um die CPU- und Memory-Limits des Service (und optional Reservierungen) zu setzen, zu ändern oder zu löschen. CPU wird in Kernen angegeben (z. B. 0.5, 2); Memory als menschenlesbare Größe (z. B. 512m, 2g, 1.5GiB). Ein leeres Feld löscht dieses Limit (Swarm behandelt es dann als unbegrenzt). Eine Reservierung darf ihr Limit nicht überschreiten. Das Anwenden macht ein ServiceUpdate (Rolling Update); bestehende Pids-Limits und Device-/generische Reservierungen bleiben erhalten |
| P | Placement bearbeiten (großes P, also Shift+p, um es von p Ports zu unterscheiden) — öffnet ein kleines Menü mit zwei gestagten Editoren: Constraints und Spread-Präferenzen.
ServiceUpdate (Rolling Update); der andere Teil bleibt erhalten. |
Diese Editoren sind gestagt: a fügt einen Eintrag
hinzu, d löscht den unter dem Cursor, y kopiert den
ausgewählten Eintrag über OSC52 in deine Zwischenablage (sodass du z. B. eine
einzelne Umgebungsvariable greifen kannst, während du sie ansiehst), u
macht die letzte gestagte Änderung rückgängig (Hinzufügen / Bearbeiten / Löschen),
solange du noch nicht angewendet hast — drücke es wiederholt, um schrittweise durch
die gestagte Historie zurückzugehen — und w wendet alle Änderungen auf
einmal in einem einzigen ServiceUpdate an (sodass z. B. das Hinzufügen
mehrerer Volumes und anschließendes Drücken von w ein Rolling
Update auslöst, nicht eines pro Eintrag), hinter einer Bestätigung. Wenn du
Esc drückst, während noch Änderungen gestagt sind, verwirft der Editor
sie nicht stillschweigend — er fragt, ob anwenden,
verwerfen oder weiter bearbeiten.
Solange Änderungen noch gestagt sind, werden sie hervorgehoben,
sodass du genau siehst, was sich ändern wird: hinzugefügte oder bearbeitete
Einträge erscheinen in
Grün (neu) und entfernte
Einträge bleiben als gedimmte rote „removed“-Zeilen
stehen — die Hervorhebung verschwindet, sobald du anwendest. Die Editoren für
Ports, Labels, Mounts,
Env und Aliase
(Aliase über A aus dem Netzwerk-Editor erreicht)
haben außerdem e, um den Eintrag unter dem Cursor zu bearbeiten; die
Editoren für Netzwerke und Secrets haben kein
e — ein Netzwerk- oder Secret-Eintrag ist nur ein Name, also nur
Hinzufügen (a) / Entfernen (d). Das e des
Env-Editors öffnet einen scrollbaren mehrzeiligen Textbereich
(Ctrl-S speichern / Esc abbrechen) für lange Werte; die
übrigen Editoren behalten die einzeilige Eingabe mit Enter zum
Bestätigen.
Der Baum aktualisiert sich automatisch alle ~10s, sodass ein im Hintergrund ersetzter Container oder ein skalierter Service von selbst auftaucht — der Cursor und alle ausgeklappten Services bleiben erhalten. r erzwingt weiterhin eine sofortige Aktualisierung.
Volumes-Tab
| Taste | Aktion |
|---|---|
| / | Suche — die Liste nach Volume-Name, Driver oder Node filtern |
| n | ein Volume erstellen — ein Formular mit Name, Driver (Standard local), Labels (k=v,k=v) und einem Node-Ziel (vervollständigt Node-Namen; leer lassen, um auf jedem Node zu erstellen, da Volumes node-lokal sind). Erstellt es auf dem Agent jedes Ziel-Nodes und meldet Erfolg/Fehler pro Node. Tab wechselt zwischen Feldern, Esc bricht ab |
| space | das Volume aus-/abwählen (markiert ▣) für ein Massen-Löschen |
| a | alle aktuell angezeigten Volumes aus-/abwählen |
| d | die ausgewählten Volumes — oder das unter dem Cursor — auf jedem Node löschen, der sie hält, hinter einer Bestätigung (mit einem Fortschritts-Overlay; die Löschungen laufen mit begrenzter Parallelität) |
| P | prune: jedes Volume löschen, das kein laufender Container einhängt und kein Service deklariert, hinter einer Bestätigung |
| Enter | zeigen, welche Nodes das Volume halten; die Labels des Volumes werden schreibgeschützt über den Nodes aufgelistet (Docker hat keine Volume-Update-API, daher können Labels nach der Erstellung nicht bearbeitet werden — setze sie beim Erstellen des Volumes) |
| i | zeigen, welche Services/Container es verwenden |
| A | das Volume an einen Service anhängen: einen Service auswählen (Name-Autocomplete) → den Container-Zielpfad eingeben (absolut) → Attach oder Attach read-only wählen. Fügt den Mount über ein ServiceUpdate (Rolling Update) hinzu — das Volume-Gegenstück zu den Netzwerk-/Secret-Attach-Aktionen; das Umgekehrte lebt im v-Mounts-Editor des Service-Inspects |
| s S | das Sortierfeld durchschalten (name → nodes → used → age → size) / umkehren |
Das Löschen ist node-bewusst: ein Volume wird auf jedem Node entfernt, der es hält. Prune verschont Volumes, die ein Service deklariert, selbst wenn keine Task läuft, sodass es die Daten eines gestoppten Stacks nicht löscht. Für feinere Kontrolle öffnet Enter die Liste pro Node, wo space Nodes auswählt, d die Kopie des markierten Nodes löscht und a auf allen Nodes löscht — jeweils hinter einer Bestätigung.
Forwards-Tab
| Taste | Aktion |
|---|---|
| Enter | die vollständigen Details des Forwards anzeigen (inklusive etwaiger Fehler) |
| d | den ausgewählten Forward stoppen |
| o | die URL des Forwards kopieren (http://127.0.0.1:<port>) |
Ein laufender Container wird im Baum als local→remote annotiert
(z. B. 9090→8080). Forwards laufen weiter, wenn ihr Overlay schließt
und wenn du auf einen anderen Cluster wechselst; sie stoppen bei
d oder wenn du die UI beendest. Die CLUSTER-Spalte nennt
den Context, zu dem jeder gehört, gedimmt für den gerade sichtbaren — ein Forward
auf einem anderen Cluster ist weiterhin deiner und lauscht weiter, aber sein
CONTAINER und NODE benennen Dinge, die du von hier aus
nicht sehen kannst. Die Baum-Annotation ist aus demselben Grund pro Cluster: eine
Container-ID ist nur innerhalb ihres eigenen Daemons eindeutig.
Deshalb wird ein lokaler Port, den du schon benutzt, vorab abgelehnt — mit Angabe des Forwards, der ihn hält, und des Clusters, zu dem er gehört; der Prompt bleibt offen, damit du einen anderen Port wählen kannst. Das „address already in use" des Betriebssystems bleibt für den Fall, in dem es recht hat: ein Port, den ein anderes Programm hält und den swarmexec nicht benennen kann.
Networks-Tab
Eine Liste der Netzwerke des Swarms — Name, Driver, Scope, Typ
(ingress / internal / attachable), ob das Netzwerk verschlüsselt
ist und wie viele Services an jedes angeschlossen sind. Die
TYPE-Spalte zeigt overlay oder den Driver-Namen (z. B.
bridge) für einfache Netzwerke statt eines Strichs. Die
ENC-Spalte zeigt 🔒 yes, wenn
das Overlay-Netzwerk Data-Plane-Verschlüsselung an hat (erstellt mit
--opt encrypted), sonst einen Strich. Der Name jedes Netzwerks und
seine TYPE-Zelle sind nach Art farbcodiert (höchste Priorität zuerst):
- attachable — grün
- internal — gelb
- ingress — grau
- anderes Overlay / swarm-scoped — aqua
- lokal (
bridge,host, …) — gedimmtes Grau
| Taste | Aktion |
|---|---|
| n | ein Netzwerk erstellen — öffnet ein Formular (Standard-Driver overlay) mit den gängigen Swarm-Optionen: attachable (standalone Container beitreten lassen), encrypted (Overlay-Data-Plane-Verschlüsselung), internal (kein externes Routing), IPv6, eine optionale MTU, ein optionales Subnet/Gateway (IPAM) und Labels (k=v,k=v). Tab wechselt zwischen Feldern, Enter auf Create bestätigt, Esc bricht ab. Erstellt es über ein Manager-API-NetworkCreate und aktualisiert dann den Tab |
| Enter / i | die angeschlossenen Services zeigen, jeder mit seinen laufenden Containern darunter verschachtelt. Die eigenen Labels des Netzwerks werden schreibgeschützt oben aufgelistet (Docker hat keine Netzwerk-Update-API, daher können Netzwerk-Labels nach der Erstellung nicht bearbeitet werden — setze sie beim Erstellen des Netzwerks). Jeder Service-Header zeigt außerdem, wie viele DNS-Aliase er in diesem Netzwerk hat (oder no aliases) |
| Enter | in der Members-Ansicht: die DNS-Aliase des ausgewählten Service aus-/einklappen (standardmäßig eingeklappt, damit die Liste kompakt bleibt); die Aliase erscheinen eingerückt unter dem Service-Header |
| A | in der Members-Ansicht: die DNS-Aliase des ausgewählten Service in diesem Netzwerk hinzufügen / bearbeiten — öffnet denselben gestagten Alias-Editor wie die Inspect-Ansicht (a hinzufügen / e bearbeiten / d löschen / w anwenden / Esc abbrechen); das Anwenden ersetzt die Aliase dieses Netzwerks in einem ServiceUpdate und aktualisiert die Ansicht. Funktioniert auch, wenn der Service noch keine Aliase hat. (A ist groß, Shift+a, um es von a Attach zu unterscheiden; standalone Nicht-Swarm-Container sind nicht bearbeitbar — Aliase sind ein ServiceUpdate pro Service) |
| a | in der Members-Ansicht: einen Service an dieses Netzwerk anhängen — eine Autocomplete-Eingabe (schlägt Services vor, die noch nicht angeschlossen sind; du kannst auch einen beliebigen Namen tippen), dann eine Rolling-Update-Bestätigung |
| d | in der Members-Ansicht: einen Service von diesem Netzwerk trennen — eine Autocomplete-Eingabe (schlägt die aktuell angeschlossenen Services vor; du kannst auch einen beliebigen Namen tippen), dann eine Rolling-Update-Bestätigung |
Anhängen oder Trennen macht ein Service-Update, das das Netzwerk in der
Service-Spec hinzufügt oder entfernt, und aktualisiert dann den Networks-Tab. Das
ist eine Manager-API-Operation (ServiceUpdate) — derselbe Kanal, den
der Client für die Topologie nutzt — und sie löst ein Rolling
Update aus, das die Tasks des Service neu startet.
Secrets-Tab
Eine schreibgeschützte Liste der Secrets des Swarms — Name, wie viele Services jedes verwenden, Alter, letzte Aktualisierung und Label-Anzahl. Secret-Werte werden nie angezeigt: die Docker-API legt sie nicht offen.
| Taste | Aktion |
|---|---|
| Enter | die Metadaten des Secrets und die Services/Container zeigen, die es verwenden |
| a | in der Detailansicht: das Secret an einen Service anhängen — eine Autocomplete-Eingabe (schlägt Services vor, die es noch nicht verwenden; du kannst auch einen beliebigen Namen tippen), dann eine Rolling-Update-Bestätigung |
| d | in der Detailansicht: das Secret von einem Service trennen — eine Autocomplete-Eingabe (schlägt Services vor, die es aktuell verwenden; du kannst auch einen beliebigen Namen tippen), dann eine Rolling-Update-Bestätigung |
Das Anhängen fügt das Secret dem Service hinzu, eingehängt unter
/run/secrets/<name> (wie docker service update
--secret-add); das Trennen entfernt die Secret-Referenz. Dann aktualisiert
sich der Secrets-Tab. Wie beim Netzwerk-Anhängen/-Trennen ist jedes ein
Manager-API-ServiceUpdate, das ein Rolling Update
auslöst, das die Tasks des Service neu startet.
Context-Sidebar
Die Cluster stehen in einer Spalte am rechten Rand, nicht in einem Tab. Den Cluster zu wechseln ist etwas, das man von dort aus tut, wo man gerade ist — und die Liste der Cluster ist das, was einem sagt, wo das ist. Sie gehört also auf den Bildschirm und nicht auf eine Seite, zu der man erst hinreisen muss.
c gibt ihr aus jedem Tab die Tastatur; c erneut oder
Esc gibt sie an den Tab zurück, auf dem du warst. Der aktive Cluster
ist mit ▶ markiert — derselbe Name, den der Footer zeigt. Ein
Cluster, den du in dieser Sitzung schon besucht hast, trägt ein grünes
·: er ist noch verbunden, der Wechsel dorthin ist sofort da. Einer,
der die Verbindung verweigert hat, trägt ein rotes ✗. Der eingebaute
default-Context ist geschützt.
Die Sidebar richtet ihre Breite nach dem längsten Context-Namen und tritt auf schmalen Terminals beiseite: unter etwa 92 Spalten blendet sie sich aus, statt den Baum abzuschneiden, und kommt zurück, solange c sie hält. Endpoints stehen nicht in der Spalte — dort ist kein Platz dafür, und i zeigt das vollständige Detail.
Ein Cluster-Wechsel behält deine Sitzung. Die UI baut sich nicht
neu auf: wo der Cursor stand, welche Stacks und Services aufgeklappt waren und der
/-Filter werden pro Cluster gemerkt — du landest also wieder
genau dort, wo du aufgehört hast. Port-Forwards laufen weiter; der
Forwards-Tab bekommt dafür eine CLUSTER-Spalte, damit auf einen Blick
klar ist, welche zum gerade sichtbaren Cluster gehören und welche nicht.
Gepollt wird nur der Cluster, den du ansiehst: beim Wegschalten hört der andere auf zu aktualisieren, und seine ssh-Verbindung läuft kurz darauf von selbst aus. Der erste Wechsel zu einem Cluster dauert so lange, wie ihn zu erreichen dauert — die Verbindung wird aufgebaut, bevor sich auf dem Bildschirm etwas ändert. Scheitert sie, erfährst du, welcher Cluster es war, und bleibst auf dem, der funktioniert. Jeder weitere Wechsel zurück ist sofort da.
| Taste | Aktion |
|---|---|
| c | die Sidebar aus jedem Tab fokussieren; c erneut oder Esc gibt die Tastatur zurück |
| i | Context-Details — Endpoint und Jump-Hosts, für die in der Spalte kein Platz ist |
| u / Enter | den ausgewählten Context aktivieren — macht ihn zum aktuellen (auch für dockers gespeicherten current context) und schaltet die UI auf diesen Cluster um, mit deiner Position, deinen Filtern und deinen Port-Forwards |
| n | einen Context erstellen — ein geführtes Formular. Eine Checkbox „Connect to Docker over SSH“ entscheidet den Endpoint: wenn an, füllst du SSH-User / Host / Port aus, und eine zweite Checkbox „Use a jump host“ zeigt ein Feld Jump-Host(s) (komma-separiert, Multi-Hop-ProxyJump) — swarmexec speichert sie am Context und injiziert -J in beide ssh-Verbindungen, die Docker-API und den Agent-Tunnel, sodass Bastions ohne Bearbeiten von ~/.ssh/config funktionieren. Wenn aus, gibst du einen einfachen tcp:// / unix://-Host ein. Das Formular setzt den Docker-Host für dich zusammen, und Test pingt den zusammengesetzten Endpoint, bevor du speicherst. Auch auf der CLI: swarmexec context create <name> --docker-host ssh://ops@mgr --ssh-jump bastion1,bastion2 |
| d | den ausgewählten Context entfernen (hinter einer Bestätigung); das Entfernen des aktuellen setzt die Auswahl auf default zurück |
Nodes-Tab
Listet die Nodes des Swarms mit allgemeinen Infos und ein paar Aggregationen —
NODE (Hostname; Manager in aqua, der Leader markiert mit
★), ROLE (manager / worker), AVAIL
(active / pause / drain, farbcodiert), STATE (ready / down),
ENGINE-Version, TASKS (auf dem Node eingeplante laufende
Tasks), VOLS (Volumes, die der Node hält — node-lokal, füllt sich
daher einen Moment nach dem Rest) und LABELS (Anzahl).
| Taste | Aktion |
|---|---|
| Enter / i | Node-Details — ein schreibgeschütztes Overlay mit Hostname, ID, Rolle/Leader, Verfügbarkeit, State, Adresse, Engine, Plattform, CPUs, Memory, Anzahl laufender Tasks, den Blöcken reservierte Ressourcen, tatsächliche Nutzung und Images auf diesem Node (siehe unten), Volume-Anzahl und der vollständigen Label-Liste |
| l | die Labels des ausgewählten Nodes bearbeiten — ein gestagter Listeneditor (a hinzufügen / e bearbeiten / d löschen / w anwenden / Esc abbrechen) über key=value-Einträge. Das Anwenden macht ein NodeUpdate — anders als bei einem Service greift ein Node-Update sofort (kein Rolling Update). Node-Labels werden häufig als Ziele von Placement-Constraints verwendet (node.labels.<k>) |
| a | die Verfügbarkeit des Nodes setzen — ein Menü aus Active / Pause / Drain, das den aktuellen Zustand des Nodes markiert. Active plant Tasks hier normal ein; Pause behält die laufenden Tasks und verhindert nur neue Platzierungen; Drain verschiebt jeden Task vom Node herunter. Active und Pause greifen direkt; Drain läuft über eine Bestätigung, die nennt, wie viele laufende Tasks der Swarm stoppen und anderswo neu einplanen wird (Services, deren Tasks nirgendwo sonst platziert werden können, werden unschedulable). Wie eine Label-Änderung ist es ein NodeUpdate (Read-Modify-Write auf der Node-Spec) und greift sofort — Nodes haben kein Rolling Update. Umlegbar als node_availability |
| P | Image-Plattenplatz auf dem Node freiräumen (großes P, also Shift+p — dieselbe Geste, die der Volumes-Tab für sein Prune verwendet). Öffnet ein kleines Menü mit den zwei unten beschriebenen Modi, jeder hinter einer eigenen Bestätigung; es lohnt sich, vorher zu lesen, welcher welcher ist. Die Fußzeile endet mit P reclaim images, das ?-Overlay führt es als reclaim image disk space on the node. Umlegbar als node_prune_images |
Reservierte Ressourcen. Die Node-Details enthalten einen Block
reserved by tasks (scheduler's view): je für CPU und
Memory einen Balken plus reserviert / Kapazität und
wie viel frei ist. Der Balken ist grün, wird ab 75 % gelb und bei 100 % oder darüber
rot; eine Überbuchung wird ausdrücklich benannt, und „frei“ wird nie negativ. Ein
Node, der keine Kapazität meldet, sagt das, statt einen Balken zu zeichnen. Der Block
kostet keinen zusätzlichen API-Call — die Node-Liste holt die Task-Liste ohnehin.
Reservations — genau die Rechnung, die
der Swarm-Scheduler selbst anstellt, wenn er entscheidet, ob ein Task auf einen Node
passt, und der übliche Grund, warum ein Service unschedulable bleibt. Der
tatsächliche CPU-/RAM-Verbrauch ist node-lokal und nicht, was hier steht —
er hat direkt darunter einen eigenen Block.
Tasks ohne deklarierte Reservierung werden gezählt und separat
ausgewiesen: Sie buchen hier nichts, können aber trotzdem den ganzen Node
auslasten — ein niedriger Prozentwert heißt also nicht, dass der Node im Leerlauf
ist, und in den meisten Clustern setzen ohnehin nur wenige Services überhaupt
Reservierungen. Ein Task hält seine Reservierung ab dem Moment der Zuweisung bis zu
einem Endzustand: Tasks in preparing oder starting zählen also mit, shutdown /
failed / complete / rejected dagegen nicht (der Swarm behält diese als Historie in
der Task-Liste).
Tatsächlich von Containern genutzt. Darunter steht ein zweiter Block — in use by containers (measured on the node) — mit denselben proportionalen Balken: was die Container des Nodes gerade wirklich verbrauchen, CPU in Cores und Memory, jeweils gegen die Kapazität des Nodes. Die beiden Blöcke bleiben bewusst getrennt, denn sie beantworten verschiedene Fragen und liegen regelmäßig weit auseinander: Ein Node kann voll gebucht und trotzdem im Leerlauf sein — oder kaum gebucht und am Limit. Die Zahlen kommen vom Agent dieses Nodes, derselben Quelle wie die Nutzungsmarker im Service-Baum — solange die CPU-Differenz noch nicht vorliegt, steht dort measuring…, und ein Node, dessen Agent nicht erreichbar oder zu alt ist, bekommt gar keinen Block statt einer Reihe Nullen, die wie „im Leerlauf“ aussähe.
Images auf diesem Node. Zuletzt steht in den Details ein Block images on this node: wie viel der Layer-Store des Nodes belegt und in wie vielen Images, und wie viel davon in ungetaggten Überbleibseln freigeräumt werden kann (ein Node ohne ungetaggte Images sagt das, statt eine Null anzubieten). Gibt es darüber hinaus etwas, kommt eine weitere Zeile dazu: wie viel zusätzlich getaggt, aber ungenutzt ist — bewusst als Kosten formuliert, das Entfernen bedeutet, diese Images wieder zu ziehen, und nicht als Angebot. Den Block gibt es, weil Images die eine node-lokale Ressource sind, die von der Cluster-Seite überhaupt nicht zu sehen ist: Die Manager-API hat keinerlei Sicht auf Images — anders als bei Volumes, von denen sie immerhin weiß, dass sie gemountet sind. Volumes ließen sich aus swarmexec längst clusterweit verwalten; ein Node, dessen Platte still mit alten Layern volllief, war nicht einmal sichtbar. Der Block lädt eigenständig, parallel zur Node-Liste, und erscheint daher einen Moment nach dem Rest des Overlays.
Freiräumen — P im Nodes-Tab. Das Menü dahinter hat zwei Einträge, bewusst keine Aktion mit Checkbox, denn es sind zwei verschiedene Handlungen, und eine Checkbox lädt dazu ein, versehentlich die zerstörerische zu erwischen:
| Eintrag | Was entfernt wird |
|---|---|
| Untagged leftovers | die Layer, die Rebuilds zurücklassen und aus denen sich nichts starten lässt. Sicher: Das kann keinen Service stoppen und keinen Pull erzwingen |
| Every unused image | entfernt zusätzlich getaggte Images, die gerade jetzt kein Container benutzt. Auf einem Swarm-Node gehören dazu jeder Service, der aktuell auf null skaliert ist, und jede Task zwischen zwei Restarts — jede davon muss ihr Image erneut ziehen |
Zwei Modi, zwei Berechtigungen. Der Agent autorisiert sie als zwei
eigenständige Aktionen — image.prune für die ungetaggten Überbleibsel
und image.prune.all für den Rundumschlag —, sodass eine Policy das
Freiräumen ungetaggter Layer erlauben kann, ohne den zerstörerischen Modus
mit zu erlauben. Jedes Prune landet im Audit-Log des Agents: mit
welchem Modus, wie viel freigeräumt wurde und wie viele Images gegangen sind.
docker system df berichtet, und stimmen exakt mit ihm überein —
auf einem echten Node nachgemessen bei 56 Images, 32,70 GB auf Platte,
22,99 GB freiräumbar. Insbesondere summiert swarmexec nicht
die Größen der einzelnen Images: Die Größe eines Images enthält jeden Layer, aus dem
es gebaut ist, und Layer werden geteilt — diese Summe meldet also deutlich zu viel;
in der Entwicklung behauptete sie 32,7 GB, wo der Daemon 22,2 GB sagte.
Eine Einschränkung, die dazugehört: Der Wert für nur ungetaggt ist eine
Untergrenze — ein Layer, den sich zwei ungetaggte Images teilen,
gehört zur Unique-Size von keinem der beiden —, ein Prune der ungetaggten Images
räumt also womöglich etwas mehr frei als angekündigt, nie weniger.
Dieselbe Voraussetzung wie bei den Nutzungs- und Health-Zahlen oben: Es braucht
Agents aus diesem Release oder neuer. Ein Node mit älterem Agent
zeigt schlicht gar keinen Image-Block und meldet auf P, dass er nicht
geantwortet hat — ausrollen mit
swarmexec init --force.
Configs-Tab
Tab 8 — das Gegenstück zum Secrets-Tab für das andere Objekt, das der
Swarm in Container reicht, mit einem entscheidenden Unterschied: der Inhalt
eines Configs ist lesbar. Der Secrets-Tab kann einen Wert nie zeigen (die
Docker-API gibt ihn nicht heraus); die Config-Detailansicht zeigt den
tatsächlichen Inhalt, sodass du siehst, was ein Service wirklich
bekommt, ohne auf docker config inspect auszuweichen. Genau darum geht
es in diesem Tab.
Die Liste ist schreibgeschützt: CONFIG (Name), USED BY (wie
viele Services es einhängen), SIZE, AGE,
UPDATED und LABELS (Anzahl).
| Taste | Aktion |
|---|---|
| Enter / i | die Detailansicht öffnen: Name, ID, Größe, erstellt, aktualisiert, die Services, die das Config einhängen, seine Labels — und den Inhalt des Configs |
| j k / g G | in der Detailansicht: scrollen · an den Anfang / ans Ende springen (Esc schließt) |
Der Inhalt wird bei Bedarf geholt, wenn die Detailansicht aufgeht —
nicht zusammen mit der Liste — denn ein Config kann eine ganze
nginx.conf sein; bis dahin steht dort loading…, ein leeres
Config meldet (empty). Gezeigt wird eine Vorschau, begrenzt
sowohl auf 64 KiB als auch auf
500 Zeilen. Beide Grenzen sind nötig: Begrenzt man nur die Bytes,
kann eine Datei aus sehr kurzen Zeilen immer noch zu Zehntausenden gerenderten Zeilen
werden — jede zusätzlich eingerückt — und genau das lässt die Ansicht kriechen. Eine
Kürzung wird immer benannt („… truncated at 500 lines“ bzw. an der
Byte-Grenze), nie still abgeschnitten, und binärer Inhalt wird gemeldet, nicht
ausgekippt („(binary content, N — not shown)“, erkannt an NUL-Bytes im Kopf
der Daten). Das vollständige Config ist ein docker config inspect
entfernt.
USED BY wird genauso hergeleitet wie im Secrets-Tab: Der Swarm führt
keinen Rückwärtsindex von einem Config zu seinen Nutzern, also liest
swarmexec die Service-Specs (ein ServiceList) und erkennt eine Referenz
an der ID oder am Namen des Configs — eine Spec kann beide Formen
enthalten, deshalb werden beide nachgeschlagen und zusammengeführt. Best effort:
Lässt sich die Service-Liste nicht lesen, erscheinen die Configs trotzdem, nur ohne
Zuordnung.
Eingebettete Shell & Logs
In einer eingebetteten Shell löst Ctrl-] ab (ohne den Prozess zu
beenden). Die Logs-Ansicht — ein einzelner Container oder die aggregierten Logs
eines Service — unterstützt dasselbe formatbewusste Parsen und Filtern wie der
logs-Befehl:
| Taste | Aktion |
|---|---|
| f | Follow umschalten |
| F | Log-Format durchschalten (classic → json → logfmt → gelf → raw) |
| l | Mindeststufen-Filter durchschalten (off → trace → … → fatal → off) |
| / | ein Grep-Regexp auf die Nachricht eingeben (leer löscht es) |
| m | Maus-Capture umschalten (aus = das eigene Auswählen/Kopieren deines Terminals) |
| ↑ ↓ | scrollen |
| Esc / q | schließen |
Die Ansicht rendert die gepufferten Zeilen live neu, wenn du Format, Stufe oder
Grep änderst, und die Titelleiste zeigt den aktiven Status
fmt:… lvl:… grep:….
Beim Folgen verbindet sich die Log-Ansicht über Container-Ersetzung hinweg neu,
genau wie es der logs-Befehl tut — sowohl für
die Logs eines einzelnen Containers als auch für die aggregierten Logs eines
Service (jedem Replica nach Slot gefolgt). Reconnect-Hinweise erscheinen inline in
der Log-Ansicht.
Log-Viewer
Drücke ` (Backtick) von jedem Tab aus, um ein Live-Log-Overlay umzuschalten. Es zeigt die neuesten Einträge aus dem In-Memory-Ringpuffer — neueste unten — aktualisiert sich, während es offen ist, und ist nach Stufe farbcodiert (rot = error, gelb = warn, grau = debug). ↑/↓ scrollen; Esc, q oder ` schließen es. Das ist das Fenster der TUI auf dieselben Logs, die in die Log-Datei gehen, da das Terminal sie selbst nicht anzeigen kann, während die UI zeichnet.
Eigene Tasten
Die Shortcut-Tasten sind in ~/.config/swarmexec/keys.yaml umbelegbar
(neben config.yaml; überschreibe den Pfad mit
$SWARMEXEC_KEYS). Jeder Wert ist eine einzelne Taste oder das Wort
space. Die strukturellen Tasten — Enter, Esc,
Tab, die Pfeile, die Tab-Nummerntasten 1–8 und die
vim-Aliase j/k/g/G — sind fest. Der
Footer zeigt immer deine tatsächlichen Tasten, und das ?-Overlay listet
sie alle auf.
Das Laden schlägt nie fehl: eine ungültige oder reservierte Taste, eine unbekannte Aktion oder zwei Aktionen, die auf einem Tab an dieselbe Taste gebunden sind, fallen jeweils auf den Standard zurück, und die UI zeigt die Warnungen einmalig beim Start.
11. Exit-Codes
| Code | Bedeutung |
|---|---|
0 | Erfolg |
2 | Nutzungs-/Flag-/Konfigurationsfehler oder eine abgebrochene interaktive Auswahl |
125 | Transportfehler — nicht erreichbarer Manager oder Agent, Dial-/TLS-Fehler, Stream-Fehler, abgebrochene Bestätigung (spiegelt Dockers 125) |
N | bei exec der eigene Exit-Code des Remote-Befehls ungleich null, unverändert weitergegeben |