Zum Hauptinhalt springen
S-EDV news
← Alle Anleitungen
📘 Anleitung Datenbanken 03.08.2026 · 12 min Lesezeit

Windmill mit Docker installieren: Scripts zu APIs, Workflows und UIs

Windmill verwandelt Python-, TypeScript-, Go- und SQL-Scripts in produktionsreife APIs, Hintergrund-Jobs und interne UIs – alles selbst gehostet. Diese Anleitung zeigt, wie du den vollständigen Docker-Stack in unter 15 Minuten zum Laufen bringst.

Windmill mit Docker installieren. Moderne IT Illustration einer selbstgehosteten Automatisierungsplattform mit Docker Containern, Script Ausführung, API Integration, Workflow Orchestrierung, benutzerdefinierten UIs und Server Infrastruktur für DevOps, Aut KI-generiert

Wer interne Automatisierungen, API-Endpunkte oder einfache Web-UIs bauen will, ohne für Retool, Zapier oder Temporal zu zahlen, sollte Windmill kennen. Die Open-Source-Plattform mit über 16.700 GitHub-Stars nimmt ein Script in Python, TypeScript/Deno, Go, Bash oder SQL entgegen und macht daraus in Minuten einen getakteten Job, einen HTTP-Endpunkt, einen mehrstufigen Workflow oder ein vollständiges internes Formular – alles im Browser konfigurierbar, alles im eigenen Docker-Stack. Der gesamte Zustand liegt ausschließlich in PostgreSQL; kein Redis, kein Message Broker, kein Sonderwissen nötig. Diese Anleitung richtet sich an Entwickler und KMU-Admins, die einen soliden, produktionsnahen Windmill-Stack auf einem Linux-Server oder einer VM hochziehen wollen.

Voraussetzungen

  1. Linux-Host, VM oder NAS mit Docker Engine 24+ und Docker Compose Plugin (Befehl docker compose, nicht das veraltete docker-compose). Falls du Docker noch nicht eingerichtet hast: Docker und Docker Compose auf Linux installieren.
  2. Mindestens 2 vCPUs und 4 GB RAM; für produktiven Betrieb mit mehreren parallelen Jobs empfehlen sich 4+ vCPUs und 8+ GB RAM.
  3. Freie Ports am Host: 80 (HTTP via Caddy) und optional 25 (SMTP-Trigger). Port 80 muss frei sein; wenn bereits ein Webserver läuft, Port im Caddyfile und compose.yaml anpassen (z. B. 8080:80).
  4. Internetzugang für Image-Pull von ghcr.io (GitHub Container Registry).
  5. Mindestens 5 GB freier Speicher für Images und Datenvolumes; produktiv 20+ GB einplanen (Dependency-Cache wächst mit den genutzten Sprachen).
  6. Für HTTPS: eine öffentlich erreichbare Domain und offene Ports 80 + 443. Wie du einen Caddy-Reverse-Proxy mit automatischem HTTPS konfigurierst, erklärt die Anleitung Caddy als Reverse Proxy einrichten.

Architektur auf einen Blick

Bevor du die erste Datei anlegst, lohnt ein kurzer Blick auf den Stack – das macht spätere Konfigurationsentscheidungen nachvollziehbarer:

ServiceImageAufgabe
dbpostgres:16Einziger Zustandsspeicher (Scripts, Jobs, Workflows)
windmill_serverghcr.io/windmill-labs/windmill:mainAPI + Web-Frontend (Port 8000 intern)
windmill_worker × 3ghcr.io/windmill-labs/windmill:mainJob-Ausführung, sandboxed, skalierbar
windmill_worker_nativeghcr.io/windmill-labs/windmill:mainLeichte In-Process-Jobs (HTTP, SQL); benötigt wenig RAM
windmill_extraghcr.io/windmill-labs/windmill-extra:latestLSP (Sprachintelligenz im Editor) + Debugger
caddyghcr.io/windmill-labs/caddy-l4:latestLayer-4-Reverse-Proxy; HTTP auf Port 80, SMTP-Weiterleitung
VolumeInhalt
db_dataPostgreSQL-Daten – alles, was Windmill weiß
worker_dependency_cachepip/npm/Go-Module-Cache; verhindert wiederholtes Installieren
worker_logsGeteilter Log-Speicher für Server und Worker
lsp_cachePython-LSP-Cache für den Web-Editor
caddy_dataTLS-Zertifikate (bei HTTPS-Betrieb)

Schritt 1: Projektordner anlegen

Alle Windmill-Dateien kommen in einen gemeinsamen Ordner. /opt/windmill ist eine bewährte Wahl auf Linux-Servern; auf einem Heimserver tut es auch ~/windmill.

sudo mkdir -p /opt/windmill
cd /opt/windmill

Lade jetzt die drei Dateien herunter, die Windmill offiziell bereitstellt. Das Caddyfile ist besonders wichtig – der caddy-Container startet sonst gar nicht:

curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/docker-compose.yml \
  -o compose.yaml

curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile \
  -o Caddyfile

curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/.env \
  -o .env

Verifizieren: ls -la /opt/windmill sollte drei Dateien zeigen: compose.yaml, Caddyfile und .env. Fehlt eine davon – besonders das Caddyfile –, bricht der Stack beim ersten Start mit einem Bind-Mount-Fehler ab.

Schritt 2: .env mit sicheren Zugangsdaten befüllen

Die heruntergeladene .env enthält Platzhalter-Passwörter. Ändere sie, bevor du den Stack startest – das Standardpasswort changeme ist öffentlich bekannt. Öffne die Datei mit einem Editor:

nano /opt/windmill/.env

Passe mindestens diese drei Werte an. Beide Passwörter müssen übereinstimmen – DATABASE_URL und POSTGRES_PASSWORD teilen sich das gleiche Secret:

# Windmill – lokale Umgebungsvariablen
# WM_IMAGE bestimmt das Image für Server und Worker.
# Für Enterprise Edition auf windmill-ee:main wechseln.
WM_IMAGE=ghcr.io/windmill-labs/windmill:main

# PostgreSQL-Verbindungsstring – Passwort MUSS mit POSTGRES_PASSWORD übereinstimmen
DATABASE_URL=postgres://postgres:DEIN_SICHERES_PASSWORT@db/windmill?sslmode=disable

# Datenbank-Passwort (im db-Service)
POSTGRES_PASSWORD=DEIN_SICHERES_PASSWORT

# Optionale Log-Rotation
LOG_MAX_SIZE=20m
LOG_MAX_FILE=10

Generiere ein sicheres Passwort z. B. mit openssl rand -base64 32 und ersetze DEIN_SICHERES_PASSWORT an beiden Stellen durch identische Werte.

Verifizieren: grep POSTGRES_PASSWORD /opt/windmill/.env und grep DATABASE_URL /opt/windmill/.env – beide Zeilen müssen dasselbe Passwort enthalten. Stimmen sie nicht überein, startet windmill_server mit einem Authentifizierungsfehler gegen die Datenbank.

Schritt 3: compose.yaml prüfen und anpassen

Die heruntergeladene compose.yaml ist direkt lauffähig. Hier die vollständige Referenz – so kannst du sie mit der heruntergeladenen Datei vergleichen oder sie bei Bedarf von Grund auf neu anlegen:

version: "3.7"

x-logging: &default-logging
  driver: "json-file"
  options:
    max-size: "${LOG_MAX_SIZE:-20m}"
    max-file: "${LOG_MAX_FILE:-10}"
    compress: "true"

services:
  db:
    deploy:
      replicas: 1
    image: postgres:16
    shm_size: 1g
    restart: unless-stopped
    volumes:
      - db_data:/var/lib/postgresql/data
    expose:
      - 5432
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
      POSTGRES_DB: windmill
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    logging: *default-logging

  windmill_server:
    image: ${WM_IMAGE}
    pull_policy: always
    deploy:
      replicas: 1
    restart: unless-stopped
    expose:
      - 8000
      - 2525
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - MODE=server
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - worker_logs:/tmp/windmill/logs
    logging: *default-logging

  windmill_worker:
    image: ${WM_IMAGE}
    pull_policy: always
    deploy:
      replicas: 3
      resources:
        limits:
          memory: 2048M
    restart: unless-stopped
    privileged: true
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - MODE=worker
      - WORKER_GROUP=default
      - FAVOR_UNSHARE_PID=true
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - worker_dependency_cache:/tmp/windmill/cache
      - worker_logs:/tmp/windmill/logs
    logging: *default-logging

  windmill_worker_native:
    image: ${WM_IMAGE}
    pull_policy: always
    deploy:
      replicas: 1
      resources:
        limits:
          memory: 2048M
    restart: unless-stopped
    environment:
      - DATABASE_URL=${DATABASE_URL}
      - MODE=worker
      - WORKER_GROUP=native
      - NATIVE_MODE=true
      - SLEEP_QUEUE=200
    depends_on:
      db:
        condition: service_healthy
    volumes:
      - worker_logs:/tmp/windmill/logs
    logging: *default-logging

  windmill_extra:
    image: ghcr.io/windmill-labs/windmill-extra:latest
    pull_policy: always
    restart: unless-stopped
    expose:
      - 3001
      - 3002
      - 3003
    environment:
      - ENABLE_LSP=true
      - ENABLE_MULTIPLAYER=false
      - ENABLE_DEBUGGER=true
      - DEBUGGER_PORT=3003
      - ENABLE_NSJAIL=false
      - REQUIRE_SIGNED_DEBUG_REQUESTS=false
      - WINDMILL_BASE_URL=http://windmill_server:8000
    volumes:
      - lsp_cache:/pyls/.cache
    logging: *default-logging

  caddy:
    image: ghcr.io/windmill-labs/caddy-l4:latest
    restart: unless-stopped
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
    ports:
      - 80:80
      - 25:25
    environment:
      - BASE_URL=":80"
    logging: *default-logging

volumes:
  db_data: null
  worker_dependency_cache: null
  worker_logs: null
  worker_memory: null
  windmill_index: null
  lsp_cache: null
  caddy_data: null

Zwei häufige Anpassungen vor dem ersten Start:

  1. Port 80 bereits belegt: Ändere in caddy den Eintrag ports auf 8080:80 und passe das Caddyfile entsprechend an.
  2. Windows-Host oder eingeschränkte Umgebung (kein privileged): Bei windmill_worker privileged: true entfernen und FAVOR_UNSHARE_PID=true durch ENABLE_UNSHARE_PID=false ersetzen. Die PID-Namespace-Isolation entfällt dann, die Funktionalität bleibt erhalten.
  3. Mehr Worker: Passe replicas: 3 beim windmill_worker auf die Anzahl deiner vCPUs an (Faustregel: 1 Worker pro vCPU).

Verifizieren: docker compose config --quiet im Projektordner – kein Fehler bedeutet, dass compose.yaml und .env syntaktisch korrekt zusammenpassen. Eine Warnung zu WM_IMAGE deutet darauf hin, dass die .env-Datei nicht im selben Verzeichnis liegt oder die Variable fehlt.

Schritt 4: Stack starten und Images pullen

Im Projektordner startest du den kompletten Stack mit einem einzigen Befehl. Beim ersten Start werden alle Images von ghcr.io gezogen – das dauert je nach Internetanbindung 2–5 Minuten.

cd /opt/windmill
docker compose up -d

Prüfe danach den Status aller Container:

docker compose ps

Erwartete Ausgabe (gekürzt):

NAME                          STATUS
windmill-db-1                 Up (healthy)
windmill-windmill_server-1    Up
windmill-windmill_worker-1    Up
windmill-windmill_worker-2    Up
windmill-windmill_worker-3    Up
windmill-windmill_worker_native-1  Up
windmill-windmill_extra-1     Up
windmill-caddy-1              Up

Der db-Container braucht erfahrungsgemäß 10–20 Sekunden, bis der Healthcheck auf healthy springt. Alle anderen Container warten per depends_on darauf, bevor sie starten.

Schau dir die Logs an, falls ein Container nicht startet oder im Status Restarting hängt:

# Alle Services auf einmal
docker compose logs -f

# Nur einen Service (z. B. bei DB-Problemen)
docker compose logs db
docker compose logs windmill_server

Verifizieren: docker compose ps zeigt alle acht Container im Status Up. db muss zusätzlich (healthy) anzeigen. Wenn windmill_server mit FATAL: password authentication failed abstürzt, stimmt das Passwort in DATABASE_URL nicht mit POSTGRES_PASSWORD überein – zurück zu Schritt 2.

Schritt 5: Windmill im Browser öffnen und Ersteinrichtung

Öffne http://<SERVER-IP> oder http://localhost (Port 80) im Browser. Du siehst den Windmill-Login.

Melde dich mit den Standard-Zugangsdaten an:

  1. E-Mail: admin@windmill.dev
  2. Passwort: changeme

Windmill leitet dich direkt zum Ändern des Admin-Passworts weiter. Erledige das als Erstes, bevor du den Stack weiter konfigurierst. Danach siehst du den leeren Workspace und kannst sofort ein erstes Script anlegen.

Wichtige Ersteinstellungen in den Instance Settings (Zahnrad-Symbol oben rechts):

  1. Base URL: Trage deine öffentliche Domain ein (relevant für E-Mail-Links und OAuth).
  2. Windmill Hub Sync: Aktivieren, um täglich Community-Scripts und -Flows aus dem öffentlichen Hub zu synchronisieren.
  3. Worker Groups: Prüfe, dass die Gruppen default und native korrekt registriert sind.

Verifizieren: curl -I http://localhost sollte HTTP/1.1 200 OK oder einen 302-Redirect auf /login zurückgeben. Im Browser: nach dem Login siehst du das Windmill-Dashboard mit dem leeren Workspace. Falls der Browser eine Verbindungsablehnung meldet, prüfe mit docker compose ps, ob der caddy-Container wirklich läuft.

Schritt 6: Erstes Script anlegen und ausführen

Windmill ist am einfachsten über ein kurzes Beispiel-Script greifbar. Klicke im Dashboard auf + New Script, wähle Python und füge diesen Code ein:

def main(name: str = "Welt"):
    return f"Hallo, {name}!"

Speichere das Script und klicke auf Run. Windmill startet einen Worker-Job, führt den Code in einer isolierten Sandbox aus und zeigt das Ergebnis in der Job-Ansicht an. Die Ausführungszeit, Logs und das Return-Value erscheinen direkt im Browser.

Vom gleichen Script aus kannst du mit einem Klick:

  1. einen HTTP-Endpunkt erstellen (das Script wird zur REST-API),
  2. einen Cron-Trigger einrichten (z. B. täglich 02:00 Uhr),
  3. das Script in einen Flow einbauen (mehrere Scripts sequenziell oder parallel).

Verifizieren: In der Job-Ansicht (Runs im linken Menü) erscheint der gerade ausgeführte Job mit Status Success und dem Return-Value "Hallo, Welt!". Ein Status Failed mit Fehlermeldung zum fehlenden Interpreter deutet darauf hin, dass der Worker-Container noch nicht vollständig gestartet ist – kurz warten und erneut ausführen.

Schritt 7: HTTPS und externe Erreichbarkeit (optional)

Für den produktiven Einsatz mit einer öffentlichen Domain aktivierst du automatisches HTTPS über Caddy. Ändere in der compose.yaml die Umgebungsvariable des Caddy-Containers:

  caddy:
    environment:
      - BASE_URL="https://windmill.deine-domain.tld"

Außerdem müssen Port 443 in den ports des caddy-Services ergänzt und in der Firewall freigegeben werden:

    ports:
      - 80:80
      - 443:443
      - 25:25

Stelle sicher, dass deine Domain per A-Record auf die Server-IP zeigt und Port 80 + 443 erreichbar sind. Caddy holt das Let's-Encrypt-Zertifikat dann automatisch beim nächsten Start. Wie du generell einen Reverse Proxy richtig absicherst, erklärt die Anleitung VPS absichern und härten.

Wende die Änderung an:

docker compose up -d caddy

Verifizieren: curl -I https://windmill.deine-domain.tld gibt HTTP/2 200 zurück. Im Caddy-Log (docker compose logs caddy) siehst du certificate obtained successfully. Falls Caddy mit ACME challenge failed abbricht, ist Port 80 oder 443 noch nicht erreichbar.

Schritt 8: Updates und Datensicherung

Windmill erscheint mehrmals wöchentlich mit neuen Releases. Updates sind unkompliziert und datensicher:

cd /opt/windmill
docker compose pull
docker compose up -d

Docker Compose zieht neue Images, stoppt betroffene Container und startet sie mit dem neuen Image neu. Laufende Jobs erhalten bis zu 30 Sekunden, um abzuschließen (Graceful Shutdown). Die PostgreSQL-Volumes bleiben unangetastet.

Wichtig: Verwende beim Update niemals docker compose down --volumes. Dieses Flag löscht den db_data-Volume und damit die gesamte Datenbank unwiederbringlich.

Für ein Datenbank-Backup nutzt du pg_dump direkt gegen den laufenden Container:

docker exec windmill-db-1 pg_dump -U postgres windmill \
  > /opt/windmill/backups/windmill_$(date +%Y%m%d).sql

Eine vollständige Backup-Strategie, die diesen Dump automatisch rotiert und in die Cloud schiebt, beschreibt die Anleitung MySQL & PostgreSQL Backup automatisieren.

Verifizieren: Nach dem Update zeigt docker compose ps alle Container im Status Up. In den Windmill-Instance-Settings unter About siehst du die neue Versionsnummer. Die SQL-Dump-Datei sollte mehrere MB groß sein und mit -- PostgreSQL-Kommentaren beginnen.

Troubleshooting / Typische Fehler

  1. caddy startet nicht / no such file or directory: ./Caddyfile: Das Caddyfile fehlt im Projektordner. Lösung: curl -fsSL https://raw.githubusercontent.com/windmill-labs/windmill/main/Caddyfile -o /opt/windmill/Caddyfile und anschließend docker compose up -d caddy.
  2. bind: address already in use für Port 80: Ein anderer Webserver (nginx, Apache) belegt Port 80. Lösung: In compose.yaml den caddy-Port auf 8080:80 ändern und das Caddyfile anpassen, oder den anderen Dienst stoppen.
  3. FATAL: password authentication failed for user "postgres": DATABASE_URL und POSTGRES_PASSWORD in der .env enthalten unterschiedliche Passwörter. Beide angleichen, dann docker compose up -d.
  4. Worker crashen mit clone(CLONE_NEWPID) operation not permitted: privileged: true fehlt oder ist auf dem Host deaktiviert. Entweder privileged: true in der compose.yaml ergänzen oder FAVOR_UNSHARE_PID=true durch ENABLE_UNSHARE_PID=false ersetzen und privileged weglassen.
  5. invalid reference format beim Start: Die Variable WM_IMAGE ist nicht gesetzt. Prüfe, ob die .env-Datei im selben Verzeichnis wie compose.yaml liegt und den Eintrag WM_IMAGE=ghcr.io/windmill-labs/windmill:main enthält.
  6. Jobs dauern sehr lange (Python/npm): Der worker_dependency_cache-Volume ist nicht gemountet. Prüfe in der compose.yaml, ob worker_dependency_cache:/tmp/windmill/cache im windmill_worker-Service eingetragen ist.
  7. Jobs landen nie beim richtigen Worker: WORKER_GROUP ist falsch oder fehlt. Der Standard-Worker braucht WORKER_GROUP=default, der native Worker WORKER_GROUP=native. In den Instance Settings unter Workers prüfen, ob beide Gruppen als aktiv erscheinen.
  8. Externe Datenbank (RDS/Azure) schlägt beim Start fehl: Ohne Superuser-Rechte kann Windmill das Schema nicht initialisieren. Lösung: init-db-as-superuser.sql aus dem Windmill-Repository als Superuser ausführen und dann GRANT-Rechte für windmill_admin und windmill_user setzen.

Häufige Fragen

Wie viele Worker-Container brauche ich?

Die Faustregel lautet: 1 Standard-Worker pro vCPU, je 1–2 GB RAM. Ein Server mit 4 vCPUs bekommt also replicas: 4 beim windmill_worker. Zusätzlich 1–2 native Worker für leichte HTTP- und SQL-Jobs – die benötigen nur rund 0,1 CPU und 128 MB RAM. Anpassen reicht: deploy.replicas im compose.yaml ändern, dann docker compose up -d.

Kann ich Windmill ohne Caddy betreiben?

Ja. Entferne den caddy-Service aus der compose.yaml und konfiguriere deinen vorhandenen Nginx, Traefik oder NPM so, dass windmill_server:8000 als Upstream dient. Für Traefik gibt es im Windmill-Repository ein Beispiel-Caddyfile-Äquivalent. Port 80 entfällt dann aus dem compose.yaml. Die SMTP-Weiterleitung (Port 25 → 2525) musst du in diesem Fall selbst in deinem Reverse Proxy abbilden, sofern du E-Mail-Trigger nutzen willst.

Wie aktiviere ich HTTPS mit einer eigenen Domain?

Setze BASE_URL im caddy-Environment auf https://deine-domain.tld, ergänze Port 443 in den ports, stelle sicher, dass DNS und Firewall korrekt sind, und starte caddy neu. Caddy holt das Let's-Encrypt-Zertifikat automatisch. Eigene Zertifikate trägst du per tls /certs/cert.pem /certs/key.pem im Caddyfile ein.

Wie aktiviere ich die Enterprise Edition?

Ändere in der .env den Wert WM_IMAGE auf ghcr.io/windmill-labs/windmill-ee:main und starte mit docker compose up -d neu. Den EE-Lizenzschlüssel trägst du nicht als Umgebungsvariable ein, sondern in den Instance Settings der Windmill-UI unter License. Kollaborative Bearbeitung (Multiplayer) aktivierst du zusätzlich per ENABLE_MULTIPLAYER=true im windmill_extra-Service.

Wie binde ich Scripts aus einem Git-Repository ein?

Windmill unterstützt Git-Sync: In den Instance Settings unter Git Sync kannst du ein Repository verbinden. Scripts und Flows werden dann bidirektional synchronisiert – Änderungen in der UI landen als Commits im Repository, und Pushes ins Repository aktualisieren die Scripts in Windmill. Das ermöglicht vollständige GitOps-Workflows mit Versionskontrolle außerhalb der UI.

Wie funktioniert die Volltext-Suche?

Der windmill_indexer-Service ist standardmäßig deaktiviert (replicas: 0). Für die Volltext-Suche in Scripts und Job-Logs setzt du replicas: 1 beim Indexer-Service und startest neu. Der Service ist auch in der Community Edition verfügbar und nutzt das Volume windmill_index.

Fazit

Windmill ist einer der durchdachtesten Self-Hosting-Stacks, die es derzeit für Workflow-Automatisierung gibt. Der einzige Zustandsspeicher ist PostgreSQL – kein Redis, kein Kafka, keine verteilten Queues – und trotzdem lassen sich Scripts in fünf Sprachen, Cron-Jobs, HTTP-Trigger, mehrstufige Workflows und interne UIs realisieren. Die Trennung von Server und Worker ermöglicht echtes horizontales Skalieren, und der LSP-gestützte Web-Editor macht den Unterschied zu klassischen Cron-Lösungen deutlich spürbar.

Wer heute mit drei Standard-Workern startet, kann morgen auf zehn skalieren, ohne die Architektur anzufassen. Wer PostgreSQL schon als Datenbank für andere Dienste betreibt, kann sogar die eingebaute DB weglassen und Windmill gegen eine externe Instanz zeigen – zum Beispiel gegen die gleiche PostgreSQL-Instanz, die bereits regelmäßig gesichert wird. Die Enterprise Edition mit Multiplayer-Bearbeitung und erweiterten Audit-Logs bleibt optional; die Community Edition ist für die meisten KMU-Anwendungsfälle vollständig ausreichend.

Weiterführende Anleitungen und Quellen

  1. Docker und Docker Compose auf Linux installieren – die Grundlage für alle Docker-Stacks
  2. MySQL & PostgreSQL Backup automatisieren mit cron – Windmills PostgreSQL-Datenbank sichern
  3. Caddy als Reverse Proxy einrichten – HTTPS und automatische Zertifikate verstehen
  4. Temporal mit Docker installieren – die Alternative für komplexe Durable Workflows
  5. n8n mit Docker: KI-Workflows und Automatisierung self-hosted – Low-Code-Alternative zu Windmill

Offizielle Quellen: Windmill Self-Hosting-Dokumentation und das windmill-labs/windmill-Repository auf GitHub (compose.yaml, .env, Caddyfile).