Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Server & Netzwerk 13.09.2026 · 21 min Lesezeit

Kestra mit Docker Compose installieren und betreiben: Workflow-Orchestrierung im Eigenbetrieb

Kestra löst gewachsene Cronjob-Sammlungen durch versionierbare YAML-Workflows mit vollständiger Ausführungshistorie ab. Diese Anleitung zeigt die Installation mit Docker Compose auf Basis eines eigenen Testlaufs: was beim Start wirklich passiert, warum die API mit 401 antwortet, weshalb der Plattenplatz und nicht der Arbeitsspeicher der Engpass ist und wie das Aufräumen am Bind-Mount scheitert.

Illustration zur Anleitung: Kestra mit Docker Compose installieren und betreiben, mit Workflow-Graph, PostgreSQL-Backend und Hinweis auf den benötigten Plattenplatz KI-generiert

Wer mehrere Skripte, Datenbank-Exporte, API-Abfragen und Backup-Läufe zuverlässig in einer festen Reihenfolge starten muss, stößt mit einer wachsenden Sammlung von Cronjobs irgendwann an eine Grenze. Es fehlt der Überblick, welcher Lauf wann gescheitert ist, es fehlt ein sauberer Wiederanlauf, und Abhängigkeiten zwischen Jobs landen als Wartezeit im Skript. Kestra ist eine quelloffene Plattform für genau diese Aufgabe: Workflows werden als YAML beschrieben, zentral ausgeführt, protokolliert und in einer Weboberfläche sichtbar gemacht.

Diese Anleitung führt durch eine vollständige Installation mit Docker Compose, beschreibt den beobachteten Startverlauf, erklärt das für viele überraschende Authentifizierungsverhalten der API und behandelt Sicherheit, Backup, Updates und die saubere Deinstallation. Die Angaben zum Startverhalten, zu den Ressourcen und zu den aufgetretenen Meldungen stammen aus einem eigenen Testlauf vom 13.09.2026 auf einem Ubuntu-Host mit Docker 29.1.3, Compose 2.40.3, 2 vCPU und 5,4 GB RAM. Was nur aus der offiziellen Dokumentation stammt und nicht selbst nachgestellt wurde, ist im Text ausdrücklich so gekennzeichnet.

Was Kestra ist und wofür sich der Aufwand lohnt

Kestra ist ein Orchestrator. Es führt Aufgaben nicht selbst aus, sondern startet sie kontrolliert: ein Shell-Kommando, ein Python-Skript, eine SQL-Abfrage, einen HTTP-Aufruf. Jeder Workflow, in Kestra Flow genannt, ist eine YAML-Datei mit einer Liste von Tasks. Ausgelöst wird er über einen Zeitplan, ein Webhook, ein eingehendes Ereignis oder von Hand aus der Oberfläche.

Der praktische Gewinn gegenüber einer Sammlung von Cronjobs liegt weniger in der Ausführung selbst als in allem drumherum. Jeder Lauf wird als Execution gespeichert, mit Startzeit, Dauer, Status, Logs pro Task und den übergebenen Variablen. Ein fehlgeschlagener Lauf lässt sich einzeln nachvollziehen und neu starten, ohne dass jemand auf dem Server in Logdateien suchen muss.

  • Deklarative Workflows: Ein Flow ist reines YAML und lässt sich in Git versionieren, reviewen und zwischen Umgebungen kopieren.
  • Sichtbare Historie: Jede Ausführung landet mit Logs in der Datenbank und ist über die Oberfläche filterbar.
  • Abhängigkeiten statt Wartezeiten: Tasks laufen nacheinander oder parallel, ohne dass jemand im Skript ein sleep setzen muss.
  • Trigger-Vielfalt: Zeitplan, Webhook, Dateiankunft oder ein anderer Flow als Auslöser.
  • Isolierte Task-Runner: Aufgaben können in eigenen Containern laufen, mit den Abhängigkeiten, die sie brauchen, statt auf dem Host installiert zu werden.

Für kleine Umgebungen ist der interessanteste Punkt, dass die Einstiegshürde niedrig bleibt: Die offizielle Compose-Datei besteht aus zwei Diensten, Kestra und PostgreSQL. Es braucht weder einen Message-Broker noch einen separaten Scheduler-Prozess, wie er bei anderen Orchestratoren üblich ist.

Kestra im Vergleich zu n8n, Airflow und klassischem Cron

Die Entscheidung für ein Werkzeug fällt selten im luftleeren Raum. Drei Alternativen tauchen in der Praxis regelmäßig auf, und sie lösen jeweils ein etwas anderes Problem.

WerkzeugDefinition der AbläufeTypische StärkeTypische Schwäche
Cron und systemd-TimerEine Zeile pro JobÜberall vorhanden, kein zusätzlicher Dienst, minimaler RessourcenbedarfKeine Historie, keine Abhängigkeiten, Fehler bleiben unsichtbar
n8nGrafisch im BrowserSehr viele fertige Integrationen zu SaaS-Diensten, schneller Einstieg ohne CodeGrafische Flows sind in Git schwer zu reviewen, Fokus liegt auf Integration statt auf Ablaufsteuerung
Apache AirflowPython-Code (DAGs)Sehr ausgereift im Data-Engineering, großes ÖkosystemDeutlich mehr Komponenten im Betrieb, Workflows sind an Python gebunden
KestraYAML, in der Oberfläche oder aus GitSprachunabhängig, versionierbar, vollständige Ausführungshistorie, kompakter StackSehr großes Container-Image, jüngeres Ökosystem als Airflow

Die Faustregel aus der Praxis: Wenn es vor allem darum geht, zwei SaaS-Dienste miteinander sprechen zu lassen, ist n8n oft der schnellere Weg. Wenn es um Datenpipelines in einem Python-Team geht, ist Airflow gesetzt. Kestra passt dazwischen, nämlich dann, wenn Abläufe aus gemischten Bausteinen bestehen, Bash hier, SQL dort, ein Container-Task daneben, und die Definition trotzdem als Textdatei im Repository liegen soll.

Grenzen und wann Kestra nicht passt

Ein Orchestrator ist zusätzliche Infrastruktur, die selbst betreut, gesichert und aktualisiert werden will. Es gibt Fälle, in denen sich das nicht rechnet.

  • Drei bis fünf einfache Cronjobs ohne Abhängigkeiten: Hier bleibt crontab die sparsamere Lösung. Der Aufwand für einen zusätzlichen Dienst plus Datenbank steht in keinem Verhältnis.
  • Sehr kleine Hosts: Der Stack braucht realistisch 4 GB RAM und mindestens 10 GB freien Plattenplatz. Auf einem Raspberry Pi mit SD-Karte oder einem 2-GB-VPS ist das keine sinnvolle Basis.
  • Ausführung im Sekundentakt: Kestra ist für Batch- und Ereignisverarbeitung gebaut, nicht als Ersatz für einen Nachrichtenbus mit Latenzanforderungen im Millisekundenbereich.
  • Umgebungen ohne Container: Ohne Docker oder Kubernetes wird der Betrieb deutlich mühsamer, weil die Verteilung als Container-Image der klar bevorzugte Weg ist.
  • Strenge Trennung von Host und Workload: Der Docker-Task-Runner verlangt Zugriff auf den Docker-Socket. Wer das aus guten Gründen ausschließt, verliert eine der attraktiveren Funktionen.

Voraussetzungen und Ressourcen: der Plattenplatz ist der Engpass

Dies ist der wichtigste Abschnitt der gesamten Anleitung, weil er den häufigsten Abbruchgrund betrifft. Bei Kestra ist nicht der Arbeitsspeicher das Nadelöhr, sondern die Festplatte.

Im Testlauf belegte das Image kestra/kestra:latest entpackt 7,48 GB, dazu kamen rund 650 MB für postgres:18. Der freie Plattenplatz auf dem Testhost sank durch den Pull von 9,9 GB auf 2,5 GB. Der Vorgang lief gerade noch durch. Auf einem Host mit weniger Reserve endet er mit no space left on device, und zwar oft erst nach mehreren Minuten Download, weil der letzte Layer allein rund 70 Sekunden zum Entpacken benötigte.

# Freien Platz vor der Installation pruefen
df -h /var/lib/docker

# Alte, nicht mehr verwendete Images entfernen und Platz zurueckgewinnen
docker image prune -a -f

Die Empfehlung lautet daher: vor der Installation aufräumen und mindestens 10 GB frei einplanen, besser 15 GB, damit auch ein späteres Update noch Platz für das neue Image findet. Ein Update lädt zunächst das neue Image herunter, bevor das alte entfernt werden kann, der Platzbedarf ist also kurzzeitig doppelt so hoch.

Beim Arbeitsspeicher ist die Lage entspannter. Der Kestra-Container zeigte kurz nach dem Start einen Peak von 971 MiB, der PostgreSQL-Container 121 MiB, zusammen also gut 1,1 GB. Der Java-Prozess meldete im Log 2702MB max memory, die Heap-Obergrenze richtet sich also nach dem verfügbaren Speicher des Hosts. Für einen produktiven Einsatz mit mehreren gleichzeitig laufenden Flows sind 4 GB RAM eine vernünftige Untergrenze.

Unterstützte Plattformen und getestete Versionen

Kestra wird als Container-Image ausgeliefert und läuft überall dort, wo eine aktuelle Docker-Engine verfügbar ist: auf Linux-Servern, auf einem NAS mit Container-Unterstützung, in einer VM oder in Kubernetes. Der hier beschriebene Weg über Docker Compose ist der offizielle Einstiegspfad und laut Projektdokumentation ausdrücklich für Entwicklung und Evaluierung gedacht, weil der Kestra-Dienst in dieser Variante als root läuft.

KomponenteIm Test verwendete VersionAnmerkung
BetriebssystemUbuntu, 2 vCPU, 5,4 GB RAMKein AVX-Befehlssatz erforderlich
Docker Engine29.1.3Compose-Plugin statt des alten docker-compose
Docker Compose2.40.3Aufruf als docker compose
Kestra2.0.1Log meldete Updating instance version from none to 2.0.1
PostgreSQL18Aus der offiziellen Compose-Datei übernommen
Java-Laufzeit im Image25.0.4+7-LTSIm Image enthalten, keine Installation auf dem Host nötig

Zum Projekt selbst, abgerufen am 13.09.2026 über die GitHub-API: Das Repository kestra-io/kestra zählt 28.099 Sterne, der letzte Push stammt vom 12.09.2026, das Repository ist nicht archiviert, die Lizenz ist Apache-2.0. Als jüngste Veröffentlichung war v2.0.1 vom 11.09.2026 eingetragen. Das Projekt wird also aktiv gepflegt, was bei einer Komponente, die zentral in Abläufe eingreift, kein nebensächliches Kriterium ist.

Die compose.yml und ihre Konfiguration

Die offizielle Compose-Datei liegt im Hauptrepository und definiert zwei Dienste sowie zwei benannte Volumes. Sie wurde für den Test unverändert übernommen, mit einer einzigen Ausnahme beim Port, die weiter unten beschrieben ist.

volumes:
  postgres-data:
    driver: local
  kestra-data:
    driver: local

services:
  postgres:
    image: postgres:18
    volumes:
      - postgres-data:/var/lib/postgresql
    environment:
      POSTGRES_DB: kestra
      POSTGRES_USER: kestra
      POSTGRES_PASSWORD: k3str4
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
      interval: 30s
      timeout: 10s
      retries: 10

  kestra:
    image: kestra/kestra:latest
    pull_policy: always
    stop_grace_period: 6m
    user: "root"
    command: server standalone
    volumes:
      - kestra-data:/app/storage
      - /var/run/docker.sock:/var/run/docker.sock
      - /tmp/kestra-wd:/tmp/kestra-wd
    environment:
      KESTRA_CONFIGURATION: |
        datasources:
          postgres:
            url: jdbc:postgresql://postgres:5432/kestra
            driverClassName: org.postgresql.Driver
            username: kestra
            password: k3str4
        kestra:
          repository:
            type: postgres
          storage:
            type: local
            local:
              base-path: "/app/storage"
          queue:
            type: postgres
          tasks:
            tmp-dir:
              path: /tmp/kestra-wd/tmp
          url: http://localhost:8080/
    ports:
      - "8080:8080"
    depends_on:
      postgres:
        condition: service_started

Eine Besonderheit fällt beim ersten Lesen auf: Kestra verwendet keine .env-Datei für seine eigene Konfiguration. Alle Einstellungen stecken als eingebetteter YAML-Block in der Umgebungsvariablen KESTRA_CONFIGURATION. Wer etwas ändern will, ändert diesen Block direkt in der Compose-Datei. Die folgende Übersicht erklärt, was die einzelnen Werte tun.

WertBedeutung und Hinweis
POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORDZugangsdaten der Datenbank. Sie müssen identisch unter datasources.postgres wiederholt werden. Der voreingestellte Wert k3str4 ist ein Platzhalter und gehört vor dem ersten Start ersetzt.
kestra.repository.type und kestra.queue.typeLegen das Backend fest. postgres ist die Wahl für den dauerhaften Betrieb, h2 existiert nur für kurze Tests ohne Datenbank.
kestra.storage.local.base-pathInterner Ablagepfad für Task-Ausgaben und Dateien, im Container /app/storage, auf das Volume kestra-data gemappt.
kestra.urlBasis-URL für erzeugte Links und Callbacks. Sie muss den tatsächlich erreichbaren Hostnamen samt Port enthalten, sonst zeigen Links ins Leere.
kestra.tasks.tmp-dir.pathArbeitsverzeichnis für laufende Tasks, per Bind-Mount auf /tmp/kestra-wd des Hosts. Dieses Verzeichnis verursacht später beim Aufräumen Arbeit.
kestra.server.basic-authIn der Vorlage auskommentiert. Benutzername muss eine gültige E-Mail-Adresse sein, das Passwort mindestens acht Zeichen mit Großbuchstabe und Ziffer.
/var/run/docker.sockNötig für den Docker-Task-Runner. Dieser Mount ist gleichbedeutend mit Root-Rechten auf dem Host, siehe eigener Abschnitt zur Sicherheit.
pull_policy: alwaysZieht bei jedem up das Image neu. In Verbindung mit dem Tag latest bedeutet das unkontrollierte Versionswechsel.
stop_grace_period: 6mKestra beendet laufende Aufgaben mit einer Frist von fünf Minuten. Die sechs Minuten stellen sicher, dass Compose nicht vorher hart abbricht.

Installation Schritt für Schritt

Der folgende Ablauf wurde in dieser Form ausgeführt. Er legt ein eigenes Verzeichnis an, lädt die offizielle Compose-Datei, prüft sie und startet den Stack.

# Arbeitsverzeichnis fuer den Stack anlegen
mkdir -p /opt/kestra && cd /opt/kestra

# Offizielle Compose-Datei aus dem Projektrepository laden
curl -fsSL -o docker-compose.yml   https://raw.githubusercontent.com/kestra-io/kestra/develop/docker-compose.yml

# Syntax und aufgeloeste Variablen pruefen, bevor etwas startet
docker compose config -q

# Stack im Hintergrund starten, der Pull laeuft dabei mit
docker compose up -d

Vor dem up sollten drei Anpassungen in der Datei erfolgen. Erstens das Datenbank-Passwort an beiden Stellen ersetzen. Zweitens den Image-Tag auf eine feste Version setzen statt latest. Drittens, falls Port 8080 auf dem Host schon belegt ist, die Portzuordnung und die passende kestra.url ändern. Im Test war genau das nötig:

    image: kestra/kestra:v2.0.1
    ports:
      - "18080:8080"
          url: http://localhost:18080/

Wichtig ist, dass beide Werte zusammenpassen. Wird nur der Port geändert, erzeugt Kestra weiterhin Links auf Port 8080, die von außen nicht funktionieren. Der Befehl docker compose config -q meldet solche Fehler nicht, weil er nur die YAML-Struktur prüft, nicht die inhaltliche Stimmigkeit.

Der erste Start und was dabei wirklich passiert

Der erste Start zerfällt in zwei sehr unterschiedlich lange Phasen. Der Image-Download dominiert die Wartezeit, der eigentliche Start des Dienstes ist erstaunlich kurz. Die folgende Übersicht stammt aus dem beobachteten Lauf.

ZeitpunktBeobachtetes Ereignis
ca. 08:04docker compose up -d, der Pull beginnt. Der letzte Layer benötigt allein rund 70 Sekunden zum Entpacken.
ca. 08:08:11Netzwerk, Volumes und beide Container sind erstellt und gestartet.
08:08:18 bis 08:08:48curl liefert noch 000, es lauscht also noch kein Prozess auf dem Port.
08:08:32Log: Applying migration [2.0.04-basic-auth-password], gefolgt von applied successfully in 71ms.
08:08:53Log: Machine information: 2 available cpu(s), 2702MB max memory, Java version 25.0.4+7-LTS
08:08:59Erster HTTP 200 auf /ui/, rund 48 Sekunden nach dem Containerstart.
08:09:05Log: Loaded 9 "Getting Started" flows from community blueprints.
08:09:07 bis 08:09:15Executor, Controller auf Port 50051, Scheduler, Indexer und Worker melden sich als gestartet.

Zwei Punkte verdienen Aufmerksamkeit. Erstens führt Kestra die Datenbankmigrationen selbsttätig beim Start aus, ein separater Migrationsschritt entfällt. Zweitens legt eine frische Instanz automatisch neun Beispiel-Flows aus den Community-Blueprints an. Wer eine leere Oberfläche erwartet, findet stattdessen Beispielinhalte vor. Diese lassen sich in der Oberfläche löschen, sie sind nicht schreibgeschützt.

Der Startverlauf lässt sich live mitlesen:

# Logs des Kestra-Dienstes verfolgen
docker compose logs -f kestra

# Nur die Migrationsmeldungen herausfiltern
docker compose logs kestra | grep -i migration

Zugriff auf die Oberfläche und das Verhalten der API

Die Weboberfläche ist nach dem Start unter http://<host>:8080/ erreichbar, im Testaufbau unter Port 18080. Beim Prüfen per Kommandozeile fällt ein Verhalten auf, das regelmäßig für Verwirrung sorgt und deshalb hier ausdrücklich dokumentiert wird.

# Wurzelpfad: leitet auf die Oberflaeche um
curl -s -o /dev/null -w "%{http_code}
" http://127.0.0.1:18080/

# Oberflaeche selbst: liefert 200
curl -s -o /dev/null -w "%{http_code}
" http://127.0.0.1:18080/ui/

# API-Endpunkt: liefert 401, auch ohne aktivierte basic-auth
curl -s http://127.0.0.1:18080/api/v1/version

Die beobachteten Antworten waren eindeutig: GET / gibt 307 und leitet auf /ui/ um, GET /ui/ gibt 200, und GET /api/v1/version gibt 401 mit dem Rumpf {"title":"Authentication required"}. Dasselbe gilt für /api/v1/configs.

Das ist kein Konfigurationsfehler. Kestra in Version 2.x verlangt für API-Zugriffe eine Authentifizierung, auch wenn basic-auth in der Konfiguration gar nicht aktiviert ist. Praktisch bedeutet das zweierlei. Erstens ist ein Healthcheck, der gegen einen API-Endpunkt prüft und ein 200 erwartet, von vornherein zum Scheitern verurteilt. Wer einen Compose-Healthcheck ergänzen will, sollte /ui/ als Ziel nehmen. Zweitens ist ein 401 auf der API kein Hinweis darauf, dass der Dienst nicht läuft, sondern im Gegenteil ein Beleg dafür, dass er antwortet.

    healthcheck:
      test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8080/ui/ || exit 1"]
      interval: 30s
      timeout: 10s
      start_period: 120s
      retries: 5

Der erste Flow als Funktionstest

Ein Funktionstest sollte so einfach sein, dass ein Fehlschlag eindeutig auf die Installation zurückfällt und nicht auf den Flow. Ein Flow mit einem einzigen Shell-Task erfüllt das. Er wird in der Oberfläche unter Flows über die Schaltfläche zum Anlegen eingefügt und anschließend von Hand ausgeführt.

id: erster_test
namespace: test.sedv

tasks:
  - id: hallo
    type: io.kestra.plugin.core.log.Log
    message: "Kestra laeuft, Ausfuehrung {{ execution.id }}"

  - id: datum
    type: io.kestra.plugin.scripts.shell.Commands
    taskRunner:
      type: io.kestra.plugin.core.runner.Process
    commands:
      - date -u +"%Y-%m-%dT%H:%M:%SZ"

Der erste Task schreibt eine Zeile ins Log, der zweite führt ein Shell-Kommando aus. Der Process-Task-Runner startet das Kommando direkt im Kestra-Container und benötigt keinen Docker-Socket. Nach dem Start erscheint der Lauf unter Executions mit Status, Dauer und den Logs beider Tasks. Erscheint dort ein grüner Abschluss, sind Datenbank, Queue, Scheduler und Worker nachweislich funktionsfähig.

Nicht selbst getestet: Die Ausführung eines Flows mit dem Docker-Task-Runner, bei dem jeder Task in einem eigenen Container startet, wurde im Testlauf nicht durchgeführt. Dass der dafür nötige Socket-Mount in der Compose-Datei enthalten ist, sagt nichts darüber aus, ob ein solcher Flow in einer bestimmten Umgebung durchläuft. Wer darauf setzt, sollte das vor dem produktiven Einsatz gezielt prüfen.

Sicherheit: Docker-Socket, Basic-Auth und Management-Port

Die offizielle Compose-Datei ist laut ihren eigenen Kommentaren für Entwicklungszwecke gedacht. Drei Punkte daraus verdienen vor einem Einsatz jenseits eines abgeschotteten Testnetzes eine bewusste Entscheidung.

Der Docker-Socket ist der gewichtigste Punkt. Die Zeile - /var/run/docker.sock:/var/run/docker.sock gibt dem Container die Möglichkeit, beliebige weitere Container zu starten, unter anderem mit gemountetem Host-Dateisystem. Das ist funktional gleichbedeutend mit Root-Rechten auf dem Host. Wer den Docker-Task-Runner nicht benötigt, sollte diese Zeile entfernen und stattdessen den Process-Runner verwenden. Wer ihn braucht, sollte den Host so behandeln, als liefe darauf ein privilegierter Dienst, also getrennt von anderen Anwendungen betreiben und den Zugang zur Kestra-Oberfläche eng begrenzen.

Der Dienst läuft als root. Der Eintrag user: "root" ist ausschließlich dem Socket-Zugriff geschuldet, das Basis-Image des Projekts läuft nach eigener Aussage ohne Root. Entfällt der Socket-Mount, kann diese Zeile ebenfalls entfallen.

Basic-Auth ist standardmäßig aus. Der entsprechende Block ist in der Vorlage auskommentiert. Ohne ihn kann jeder, der den Port erreicht, in der Oberfläche Flows anlegen und ausführen, also Code auf dem Host starten. Der Block wird so aktiviert:

        kestra:
          server:
            basic-auth:
              username: admin@example.com
              password: GeheimesPasswort1

Der Benutzername muss das Format einer E-Mail-Adresse haben, das Passwort mindestens acht Zeichen lang sein und einen Großbuchstaben sowie eine Ziffer enthalten. Nicht selbst getestet: Der Betrieb mit aktivierter Basic-Auth wurde im Testlauf nicht durchgespielt. Die Anforderungen an Benutzername und Passwort stammen aus den Kommentaren der offiziellen Compose-Datei.

Weitere Maßnahmen, die sich in der Praxis bewährt haben:

  • Port 8080 nicht auf allen Schnittstellen veröffentlichen, sondern mit 127.0.0.1:8080:8080 an localhost binden und einen Reverse Proxy mit TLS davorsetzen.
  • Den Management-Port 8081 geschlossen lassen. Er ist in der Vorlage aus gutem Grund auskommentiert, denn er ist unauthentifiziert und gibt interne Informationen preis.
  • Das Datenbank-Passwort ersetzen. k3str4 steht in jeder öffentlichen Kopie der Datei.
  • Den PostgreSQL-Dienst keinen Port nach außen veröffentlichen lassen, die Kommunikation läuft über das interne Compose-Netz.
  • Zugriff auf das Verzeichnis mit der Compose-Datei einschränken, weil dort Zugangsdaten im Klartext stehen.

Persistente Daten und Volumes

Der Stack legt zwei benannte Volumes an, dazu kommt ein Bind-Mount auf dem Host. Wer weiß, was wo liegt, kann gezielt sichern und im Fehlerfall gezielt verwerfen.

AblageortPfad im ContainerInhalt und Bedeutung
Volume postgres-data/var/lib/postgresqlFlows, Ausführungshistorie, Queue und sämtliche Metadaten. Der wichtigste Datenbestand.
Volume kestra-data/app/storageInterner Storage mit Task-Ausgaben und von Flows erzeugten Dateien.
Bind-Mount /tmp/kestra-wd/tmp/kestra-wdReines Arbeitsverzeichnis laufender Tasks. Nicht sicherungswürdig, verursacht aber beim Aufräumen Probleme.

Benannte Volumes liegen auf dem Host unter /var/lib/docker/volumes/<name>/_data. Der tatsächliche Name trägt das Compose-Projekt als Präfix, bei einem Projektverzeichnis test-kestra also test-kestra_postgres-data. Nachsehen lässt sich das mit:

# Alle Volumes des Projekts auflisten
docker volume ls | grep kestra

# Belegten Platz je Volume ermitteln
docker system df -v | grep kestra

Backup und Wiederherstellung

Ein brauchbares Backup besteht aus zwei Teilen: einem Datenbankauszug und einem Archiv des internen Storage. Beide Befehle wurden im Testlauf ausgeführt. Der Datenbankauszug einer frisch installierten Instanz mit den neun Beispiel-Flows war 171 KB groß, wächst mit der Ausführungshistorie aber deutlich.

# Datenbank als SQL-Dump sichern
docker compose exec -T postgres pg_dump -U kestra -d kestra > kestra.sql

# Internen Storage als Tar-Archiv sichern
docker run --rm -v test-kestra_kestra-data:/d -v "$PWD":/b alpine   tar czf /b/kestra-storage.tgz -C /d .

Der zweite Befehl verwendet einen kurzlebigen Alpine-Container, der das Volume und das aktuelle Verzeichnis einhängt. Das ist der saubere Weg, an den Inhalt eines benannten Volumes zu kommen, ohne im Docker-Datenverzeichnis des Hosts zu hantieren. Der Volumenname muss dabei an das eigene Projekt angepasst werden.

Die Wiederherstellung folgt derselben Logik in umgekehrter Reihenfolge. Der Kestra-Dienst wird angehalten, damit nicht gleichzeitig geschrieben wird, während die Datenbank noch läuft und den Dump entgegennimmt.

# Kestra anhalten, Postgres weiterlaufen lassen
docker compose stop kestra

# Datenbank aus dem Dump zurueckspielen
cat kestra.sql | docker compose exec -T postgres psql -U kestra -d kestra

# Internen Storage aus dem Archiv zurueckspielen
docker run --rm -v test-kestra_kestra-data:/d -v "$PWD":/b alpine   sh -c 'rm -rf /d/* && tar xzf /b/kestra-storage.tgz -C /d'

# Kestra wieder starten
docker compose up -d kestra

Nicht selbst getestet: Der Wiederherstellungslauf wurde im Testaufbau nicht ausgeführt, nur das Backup. Die Befehle sind aus der Struktur des Stacks abgeleitet und folgen dem üblichen Muster für Compose-Stacks mit PostgreSQL. Ein Restore gehört vor dem produktiven Einsatz einmal ausprobiert, am besten auf einem zweiten Host, denn ein Backup, das nie zurückgespielt wurde, ist eine Vermutung und kein Sicherungskonzept.

Für den laufenden Betrieb empfiehlt sich, beide Befehle in ein Skript zu legen und per Zeitplan auszuführen. Der Dump sollte danach auf ein anderes System übertragen werden, ein Backup auf derselben Platte hilft beim Plattenausfall nicht weiter.

Updates und die Grenzen des Rollbacks

Der Update-Vorgang selbst ist kurz. Image ziehen, Stack neu hochfahren, Migrationen beobachten.

# Vor jedem Update: Datenbank sichern
docker compose exec -T postgres pg_dump -U kestra -d kestra > kestra-vor-update.sql

# Neues Image laden und Container ersetzen
docker compose pull
docker compose up -d

# Migrationsmeldungen kontrollieren
docker compose logs -f kestra | grep -i migration

Dass Kestra Datenbankmigrationen selbsttätig beim Start ausführt, wurde im Testlauf beobachtet: Die Meldung Applying migration [2.0.04-basic-auth-password] wurde 71 Millisekunden später mit applied successfully quittiert. Ein manueller Migrationsschritt ist also nicht vorgesehen.

Nur laut offizieller Dokumentation, nicht selbst nachgestellt: Für Sprünge über eine Hauptversion hinweg, etwa von 1.3 auf 2.0, stellt das Projekt eine eigene Migrationsanleitung mit einer festen Upgrade-Sequenz bereit. Diese Sequenz einzuhalten ist Pflicht, Zwischenversionen dürfen nicht übersprungen werden. Ebenfalls aus der Dokumentation stammt der wichtigste Satz zum Thema Rückweg: Ein Downgrade nach einer bereits angewandten Migration wird nicht unterstützt. Der einzige Rollback ist damit die Wiederherstellung der Datenbank aus einem Backup, das vor dem Update erstellt wurde.

Daraus folgt eine klare Empfehlung für die Compose-Datei. Der Tag latest in Verbindung mit pull_policy: always bedeutet, dass bei jedem docker compose up ungefragt eine neue Version gezogen wird, unter Umständen also mitten in einem Neustart nach einem Stromausfall. Ein fester Tag macht den Zeitpunkt eines Versionswechsels zu einer bewussten Entscheidung.

    image: kestra/kestra:v2.0.1
    pull_policy: missing

Ein Update läuft damit in drei kontrollierten Schritten ab: Backup erstellen, Tag in der Compose-Datei auf die Zielversion ändern, docker compose up -d ausführen. Die Release-Hinweise des Projekts sollten vorher gelesen werden, insbesondere bei einem Wechsel der Hauptversion.

Typische Fehler mit Diagnose und Lösung

Im Testlauf startete der Stack im ersten Anlauf ohne Fehler durch. Die folgenden Punkte stammen teils aus diesem Lauf, teils aus den dabei sichtbar gewordenen Stolperstellen.

Der Pull bricht mit no space left on device ab. Die mit Abstand häufigste Ursache für einen fehlgeschlagenen Erstversuch. Der Download ist dann meist schon weit fortgeschritten, was die Sache ärgerlich macht. Abhilfe: docker image prune -a -f ausführen, mit df -h /var/lib/docker gegenprüfen und erst dann neu starten. Teilweise heruntergeladene Layer werden beim erneuten Versuch wiederverwendet.

Eine Warnung zu Prometheus-Metriken im Log. Beobachtet wurde folgende Meldung:

WARN i.m.p.PrometheusMeterRegistry The meter (MeterId{name='netty.alloc.arena.allocation.count', ...})
registration has failed: Prometheus requires that all meters with the same name have the same set of tag keys.

Ursache ist ein interner Konflikt bei Metrik-Tags im verwendeten Framework. Die Wirkung beschränkt sich auf genau diese eine Metrik, die dann nicht exportiert wird. Der Dienst arbeitet normal weiter. Diese Meldung kann ignoriert werden, sie ist kein Hinweis auf ein Problem.

Die Oberfläche ist nicht erreichbar, obwohl der Container läuft. Zuerst prüfen, ob überhaupt schon jemand lauscht. In den ersten rund 45 Sekunden nach dem Containerstart liefert curl 000, weil der Java-Prozess noch hochfährt. Bleibt es danach dabei, ist meist eine Portkollision oder eine Firewallregel die Ursache.

# Laufende Container und Portzuordnung anzeigen
docker compose ps

# Pruefen, ob der Hostport bereits belegt ist
ss -tulpn | grep 8080

# Letzte Logzeilen des Dienstes ansehen
docker compose logs --tail 50 kestra

Ein Healthcheck gegen die API schlägt dauerhaft fehl. Erwartetes Verhalten, kein Defekt. API-Endpunkte antworten mit 401, siehe der Abschnitt zum Authentifizierungsverhalten. Der Healthcheck muss gegen /ui/ prüfen.

Links in der Oberfläche zeigen auf den falschen Port. Tritt auf, wenn die Portzuordnung geändert wurde, kestra.url aber noch den alten Wert enthält. Beide Stellen müssen übereinstimmen.

Der Container braucht beim Stoppen sehr lange. Das ist so gewollt. stop_grace_period: 6m gibt laufenden Aufgaben Zeit, sich zu beenden. Wer den Stack im Leerlauf stoppt, kann die Wartezeit mit docker compose stop -t 20 verkürzen.

Saubere Deinstallation samt Bind-Mount-Problem

Die Deinstallation enthält eine Stolperfalle, die im Testlauf tatsächlich auftrat und die von der üblichen Compose-Routine nicht abgedeckt wird.

# Container, Netzwerk und benannte Volumes entfernen
cd /opt/kestra && docker compose down -v

# Stack-Verzeichnis loeschen
cd .. && rm -rf /opt/kestra

Danach sind die Container und die benannten Volumes weg, das Arbeitsverzeichnis auf dem Host aber nicht. Der Versuch, es zu entfernen, endet mit:

rm: cannot remove '/tmp/kestra-wd': Operation not permitted

Die Ursache ist eine Kombination aus zwei Eigenschaften der Compose-Datei. Das Verzeichnis /tmp/kestra-wd wird vom Kestra-Container angelegt, und der läuft als root. Auf dem Host gehört das Verzeichnis damit ebenfalls root und lässt sich von einem normalen Benutzer nicht löschen. Weil es ein Bind-Mount ist und kein benanntes Volume, fasst docker compose down -v es nicht an. Es bleibt also stehen, auch wenn sonst alles entfernt wurde.

Es gibt zwei Wege, das aufzulösen. Der direkte mit erhöhten Rechten:

sudo rm -rf /tmp/kestra-wd

Und der Weg ohne sudo, der stattdessen einen kurzlebigen Container nutzt, der ohnehin als root arbeitet. Das ist praktisch, wenn auf dem Host keine Sudo-Rechte zur Verfügung stehen, der Benutzer aber in der Docker-Gruppe ist:

docker run --rm -v /tmp:/t alpine rm -rf /t/kestra-wd

Zum Abschluss die Kontrolle, dass wirklich nichts übrig geblieben ist, und die Rückgewinnung des Plattenplatzes. Das Kestra-Image belegt mit 7,48 GB genug Platz, um dessen Entfernung zu einer spürbaren Verbesserung zu machen.

# Kontrolle: beides muss leer bleiben
docker ps -a | grep kestra
docker volume ls | grep kestra

# Nicht mehr verwendete Images entfernen
docker image prune -a -f

Im Testlauf gab dieser letzte Schritt zusammen mit einem zweiten getesteten Stack 3,5 GB frei, die Belegung der Systempartition sank von 90 auf 66 Prozent. Vorhandene Fremdcontainer auf demselben Host liefen dabei unverändert weiter, docker image prune -a -f entfernt nur Images, die von keinem Container mehr verwendet werden.

Fazit und Empfehlung für die Praxis

Kestra hat sich im Test als unauffälliger Kandidat erwiesen: Der Stack lief im ersten Anlauf durch, war nach rund 48 Sekunden ab Containerstart bedienbar und begnügte sich im Leerlauf mit gut 1,1 GB Arbeitsspeicher. Das ist deutlich weniger Reibung, als die Größe des Projekts vermuten lässt.

Der Preis steht an anderer Stelle: Ein Image von 7,48 GB ist für einen kleinen Server eine Ansage, und der Docker-Socket in der Standardkonfiguration ist eine Entscheidung, die bewusst getroffen werden muss statt unbemerkt übernommen zu werden. Wer diese zwei Punkte vorab klärt, ausreichend Platte einplant, einen festen Versionstag pinnt und vor dem ersten Update einmal den Restore durchspielt, bekommt einen Orchestrator, der eine gewachsene Cronjob-Sammlung ordentlich ablöst.

Passende Anleitungen auf S-EDV

Quellen

KestraDocker ComposeWorkflow-OrchestrierungAutomatisierungPostgreSQLSelfhostingLinux