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.

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
- Linux-Host, VM oder NAS mit Docker Engine 24+ und Docker Compose Plugin (Befehl
docker compose, nicht das veraltetedocker-compose). Falls du Docker noch nicht eingerichtet hast: Docker und Docker Compose auf Linux installieren. - Mindestens 2 vCPUs und 4 GB RAM; für produktiven Betrieb mit mehreren parallelen Jobs empfehlen sich 4+ vCPUs und 8+ GB RAM.
- 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). - Internetzugang für Image-Pull von
ghcr.io(GitHub Container Registry). - Mindestens 5 GB freier Speicher für Images und Datenvolumes; produktiv 20+ GB einplanen (Dependency-Cache wächst mit den genutzten Sprachen).
- 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:
| Service | Image | Aufgabe |
|---|---|---|
| db | postgres:16 | Einziger Zustandsspeicher (Scripts, Jobs, Workflows) |
| windmill_server | ghcr.io/windmill-labs/windmill:main | API + Web-Frontend (Port 8000 intern) |
| windmill_worker × 3 | ghcr.io/windmill-labs/windmill:main | Job-Ausführung, sandboxed, skalierbar |
| windmill_worker_native | ghcr.io/windmill-labs/windmill:main | Leichte In-Process-Jobs (HTTP, SQL); benötigt wenig RAM |
| windmill_extra | ghcr.io/windmill-labs/windmill-extra:latest | LSP (Sprachintelligenz im Editor) + Debugger |
| caddy | ghcr.io/windmill-labs/caddy-l4:latest | Layer-4-Reverse-Proxy; HTTP auf Port 80, SMTP-Weiterleitung |
| Volume | Inhalt |
|---|---|
| db_data | PostgreSQL-Daten – alles, was Windmill weiß |
| worker_dependency_cache | pip/npm/Go-Module-Cache; verhindert wiederholtes Installieren |
| worker_logs | Geteilter Log-Speicher für Server und Worker |
| lsp_cache | Python-LSP-Cache für den Web-Editor |
| caddy_data | TLS-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/windmillLade 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 .envVerifizieren: 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/.envPasse 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=10Generiere 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: nullZwei häufige Anpassungen vor dem ersten Start:
- Port 80 bereits belegt: Ändere in
caddyden Eintragportsauf8080:80und passe das Caddyfile entsprechend an. - Windows-Host oder eingeschränkte Umgebung (kein privileged): Bei
windmill_workerprivileged: trueentfernen undFAVOR_UNSHARE_PID=truedurchENABLE_UNSHARE_PID=falseersetzen. Die PID-Namespace-Isolation entfällt dann, die Funktionalität bleibt erhalten. - Mehr Worker: Passe
replicas: 3beimwindmill_workerauf 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 -dPrüfe danach den Status aller Container:
docker compose psErwartete 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 UpDer 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_serverVerifizieren: 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:
- E-Mail:
admin@windmill.dev - 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):
- Base URL: Trage deine öffentliche Domain ein (relevant für E-Mail-Links und OAuth).
- Windmill Hub Sync: Aktivieren, um täglich Community-Scripts und -Flows aus dem öffentlichen Hub zu synchronisieren.
- Worker Groups: Prüfe, dass die Gruppen
defaultundnativekorrekt 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:
- einen HTTP-Endpunkt erstellen (das Script wird zur REST-API),
- einen Cron-Trigger einrichten (z. B. täglich 02:00 Uhr),
- 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:25Stelle 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 caddyVerifizieren: 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 -dDocker 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).sqlEine 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
- 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/Caddyfileund anschließenddocker compose up -d caddy. bind: address already in usefür Port 80: Ein anderer Webserver (nginx, Apache) belegt Port 80. Lösung: Incompose.yamlden caddy-Port auf8080:80ändern und das Caddyfile anpassen, oder den anderen Dienst stoppen.FATAL: password authentication failed for user "postgres":DATABASE_URLundPOSTGRES_PASSWORDin der.enventhalten unterschiedliche Passwörter. Beide angleichen, danndocker compose up -d.- Worker crashen mit
clone(CLONE_NEWPID) operation not permitted:privileged: truefehlt oder ist auf dem Host deaktiviert. Entwederprivileged: truein der compose.yaml ergänzen oderFAVOR_UNSHARE_PID=truedurchENABLE_UNSHARE_PID=falseersetzen undprivilegedweglassen. invalid reference formatbeim Start: Die VariableWM_IMAGEist nicht gesetzt. Prüfe, ob die.env-Datei im selben Verzeichnis wiecompose.yamlliegt und den EintragWM_IMAGE=ghcr.io/windmill-labs/windmill:mainenthält.- Jobs dauern sehr lange (Python/npm): Der
worker_dependency_cache-Volume ist nicht gemountet. Prüfe in der compose.yaml, obworker_dependency_cache:/tmp/windmill/cacheimwindmill_worker-Service eingetragen ist. - Jobs landen nie beim richtigen Worker:
WORKER_GROUPist falsch oder fehlt. Der Standard-Worker brauchtWORKER_GROUP=default, der native WorkerWORKER_GROUP=native. In den Instance Settings unter Workers prüfen, ob beide Gruppen als aktiv erscheinen. - Externe Datenbank (RDS/Azure) schlägt beim Start fehl: Ohne Superuser-Rechte kann Windmill das Schema nicht initialisieren. Lösung:
init-db-as-superuser.sqlaus dem Windmill-Repository als Superuser ausführen und dannGRANT-Rechte fürwindmill_adminundwindmill_usersetzen.
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
- Docker und Docker Compose auf Linux installieren – die Grundlage für alle Docker-Stacks
- MySQL & PostgreSQL Backup automatisieren mit cron – Windmills PostgreSQL-Datenbank sichern
- Caddy als Reverse Proxy einrichten – HTTPS und automatische Zertifikate verstehen
- Temporal mit Docker installieren – die Alternative für komplexe Durable Workflows
- 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).