Zulip mit Docker installieren: Strukturierter Team-Chat mit Streams und Topics
Zulip löst Chat-Chaos durch sein einzigartiges Konzept: Jede Nachricht gehört zu einem Stream und einem Topic. Diese Anleitung zeigt die vollständige Installation des Zulip-12.0-Docker-Stacks mit sicherem Secrets-Management und Schritt-für-Schritt-Verifikation.

Team-Chats wie Slack oder Microsoft Teams haben ein strukturelles Problem: Hunderte Nachrichten in einem Kanal, Konversationen laufen durcheinander, und wer nicht live dabei ist, verliert den Überblick dauerhaft. Zulip löst dieses Problem durch ein einzigartiges Messaging-Modell – jede Nachricht gehört zu einem Stream (dem Kanal) und einem Topic (dem Thread-Thema). Das bedeutet: Auch wer drei Tage offline war, kann gezielt den Stand eines Themas nachlesen, ohne durch hunderte irrelevante Nachrichten zu scrollen. Mit über 25.000 GitHub-Stars ist Zulip die durchdachteste selbst gehostete Slack-Alternative für Entwicklerteams, Open-Source-Projekte und KMU, die volle Datenkontrolle wollen. Diese Anleitung führt dich durch die vollständige Installation des Zulip-12.0-Docker-Stacks auf einem beliebigen Linux-Host – plattformneutral, mit sicherem Secrets-Management und Verifikation nach jedem Schritt.
Voraussetzungen
- Docker Engine (aktuell, nicht Rootless) und Docker Compose Plugin v2 installiert – falls noch nicht vorhanden, hilft die Anleitung Docker und Docker Compose auf Linux installieren.
- Linux-Host, VM oder NAS mit Docker-Unterstützung (x86_64 oder ARM64)
- Mindestens 2 GB RAM (4 GB empfohlen); mindestens 10 GB freier Festplattenspeicher, produktiv 50+ GB
- Freie Ports: 80 (HTTP, Pflicht), 443 (HTTPS, optional), 25 (SMTP-Eingang, optional)
- Eine gültige Domain oder Subdomain, die auf den Server zeigt (für
SETTING_EXTERNAL_HOST) - SMTP-Zugangsdaten eines Mail-Providers (für ausgehende Mails – zwingend erforderlich)
- Git zum Klonen des offiziellen Repositories
- Python 3 zum sicheren Erzeugen des Secret Keys
- Für HTTPS: Reverse Proxy (z. B. Caddy oder Nginx Proxy Manager) – siehe Abschnitt weiter unten
Schritt 1: Zulip-Repository klonen und Projektordner vorbereiten
Das offizielle docker-zulip-Repository auf GitHub enthält die gepflegte compose.yaml sowie alle nötigen Konfigurationsvorlagen. Klone es in ein geeignetes Verzeichnis – /opt/zulip empfiehlt sich für Server-Dienste:
git clone https://github.com/zulip/docker-zulip.git /opt/zulip
cd /opt/zulipDas Repository enthält bereits eine compose.yaml für den vollständigen Stack. Die eigene Konfiguration pflegst du ausschließlich in einer .env-Datei und bei Bedarf in einer compose.override.yaml – so bleiben deine Anpassungen bei einem git pull erhalten.
Verifizieren: Prüfe, ob die Dateien korrekt vorhanden sind:
ls /opt/zulip/compose.yaml
# Erwartete Ausgabe: /opt/zulip/compose.yamlSchritt 2: .env-Datei mit Secrets befüllen
Zulip 12.0 nutzt Docker Secrets statt Klartext-Umgebungsvariablen. Die Secrets werden aus der .env-Datei in den Container propagiert – aber nicht direkt als Umgebungsvariablen, sondern über den Secrets-Mechanismus der compose.yaml. Erstelle die Datei /opt/zulip/.env:
# /opt/zulip/.env – Pflichtfelder, NICHT in die Versionskontrolle einchecken!
# Hostname unter dem Zulip erreichbar ist (kein http://, kein trailing slash)
SETTING_EXTERNAL_HOST=zulip.example.com
# E-Mail-Adresse für Fehler- und Support-Mails
SETTING_ZULIP_ADMINISTRATOR=admin@example.com
# SMTP-Konfiguration (für ausgehende Mails, z. B. Einladungen)
SETTING_EMAIL_HOST=smtp.example.com
SETTING_EMAIL_HOST_USER=noreply@example.com
SETTING_EMAIL_PORT=587
SETTING_EMAIL_USE_TLS=True
# Passwörter für interne Dienste (zufällig, niemals wiederverwenden)
ZULIP__POSTGRES_PASSWORD=sicheres_zufaelliges_passwort_hier
ZULIP__MEMCACHED_PASSWORD=sicheres_zufaelliges_passwort_hier
ZULIP__RABBITMQ_PASSWORD=sicheres_zufaelliges_passwort_hier
ZULIP__REDIS_PASSWORD=sicheres_zufaelliges_passwort_hier
# Django Secret Key: mindestens 50 zufällige Zeichen (einmalig generieren!)
ZULIP__SECRET_KEY=hier_deinen_generierten_key_eintragen
# SMTP-Passwort deines Mail-Providers
ZULIP__EMAIL_PASSWORD=dein_smtp_passwortDen ZULIP__SECRET_KEY erzeugst du einmalig sicher mit Python – dieser darf nach dem ersten Start niemals mehr geändert werden, da sonst alle Sessions und Verschlüsselungen ungültig werden:
python3 -c "import secrets; print(secrets.token_hex(32))"
# Beispielausgabe: a1b2c3d4e5f6... (64 hexadezimale Zeichen)Trage den generierten Wert als ZULIP__SECRET_KEY in die .env-Datei ein. Sichere die Datei anschließend mit restriktiven Rechten:
chmod 600 /opt/zulip/.envWichtig: Das neue Secrets-Format verwendet einen doppelten Unterstrich (ZULIP__SECRET_KEY). Ältere Anleitungen im Internet zeigen noch das veraltete Format (ZULIP_SECRET_KEY mit einfachem Unterstrich) – das funktioniert mit dem neuen Image nicht.
Verifizieren: Stelle sicher, dass alle sechs Pflicht-Secrets gesetzt sind:
grep -c "^ZULIP__" /opt/zulip/.env
# Erwartete Ausgabe: 6 (alle sechs ZULIP__-Variablen vorhanden)Schritt 3: compose.yaml verstehen
Die folgende compose.yaml beschreibt den vollständigen Zulip-Stack mit allen fünf Diensten. Sie entspricht dem Stand des offiziellen Repositories für Version 12.0 – mit explizitem Image-Tag (kein latest!) und Docker-Secrets-Integration. Das Image kommt von der GitHub Container Registry, nicht von Docker Hub:
services:
database:
image: postgres:14
restart: unless-stopped
environment:
POSTGRES_DB: zulip
POSTGRES_USER: zulip
POSTGRES_PASSWORD_FILE: /run/secrets/zulip__postgres_password
secrets:
- zulip__postgres_password
volumes:
- postgresql-14:/var/lib/postgresql/data
memcached:
image: memcached:alpine
restart: unless-stopped
command: -S -u memcached
environment:
MEMCACHED_PASSWORD_FILE: /run/secrets/zulip__memcached_password
secrets:
- zulip__memcached_password
rabbitmq:
image: rabbitmq:4.2
restart: unless-stopped
environment:
RABBITMQ_DEFAULT_USER: zulip
RABBITMQ_DEFAULT_PASS_FILE: /run/secrets/zulip__rabbitmq_password
secrets:
- zulip__rabbitmq_password
volumes:
- rabbitmq:/var/lib/rabbitmq
redis:
image: redis:alpine
restart: unless-stopped
command: sh -c 'redis-server --requirepass "$$(cat /run/secrets/zulip__redis_password)"'
secrets:
- zulip__redis_password
volumes:
- redis:/data
zulip:
image: ghcr.io/zulip/zulip-server:12.0-1
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "25:25"
environment:
SETTING_EXTERNAL_HOST: "${SETTING_EXTERNAL_HOST}"
SETTING_ZULIP_ADMINISTRATOR: "${SETTING_ZULIP_ADMINISTRATOR}"
SETTING_EMAIL_HOST: "${SETTING_EMAIL_HOST}"
SETTING_EMAIL_HOST_USER: "${SETTING_EMAIL_HOST_USER}"
SETTING_EMAIL_PORT: "${SETTING_EMAIL_PORT:-587}"
SETTING_EMAIL_USE_TLS: "${SETTING_EMAIL_USE_TLS:-True}"
secrets:
- zulip__postgres_password
- zulip__memcached_password
- zulip__rabbitmq_password
- zulip__redis_password
- zulip__secret_key
- zulip__email_password
volumes:
- zulip:/data
ulimits:
nofile:
soft: 1000000
hard: 1048576
depends_on:
- database
- memcached
- rabbitmq
- redis
secrets:
zulip__postgres_password:
environment: "ZULIP__POSTGRES_PASSWORD"
zulip__memcached_password:
environment: "ZULIP__MEMCACHED_PASSWORD"
zulip__rabbitmq_password:
environment: "ZULIP__RABBITMQ_PASSWORD"
zulip__redis_password:
environment: "ZULIP__REDIS_PASSWORD"
zulip__secret_key:
environment: "ZULIP__SECRET_KEY"
zulip__email_password:
environment: "ZULIP__EMAIL_PASSWORD"
volumes:
postgresql-14:
rabbitmq:
redis:
zulip:Die wichtigsten technischen Eckdaten des Stacks auf einen Blick:
| Komponente | Image | Zweck |
|---|---|---|
| Zulip App | ghcr.io/zulip/zulip-server:12.0-1 | Hauptanwendung (amd64 + arm64) |
| Datenbank | postgres:14 | Persistenter Datenspeicher |
| Cache | memcached:alpine | Session- und Objekt-Cache |
| Message Queue | rabbitmq:4.2 | Asynchrone Aufgabenverarbeitung |
| Key-Value-Store | redis:alpine | Rate-Limiting, Presence |
| Port | Protokoll | Zweck |
|---|---|---|
| 80 | HTTP | Web-Zugang (Standard, HTTP-only) |
| 443 | HTTPS | Verschlüsselter Zugang (nach TLS-Konfiguration) |
| 25 | SMTP | Eingehende E-Mails (optional) |
| Volume | Pfad im Container | Inhalt |
|---|---|---|
| postgresql-14 | /var/lib/postgresql/data | Datenbankdaten (persistent) |
| rabbitmq | /var/lib/rabbitmq | Queue-Daten (persistent) |
| redis | /data | Cache-Daten (persistent) |
| zulip | /data | Uploads, Logs, App-Konfiguration |
Falls du Port 25 nicht für eingehende E-Mails benötigst, entferne die entsprechende Zeile aus dem ports-Abschnitt – das verhindert Konflikte mit einem lokalen MTA wie Postfix.
Verifizieren: Syntax der Compose-Datei prüfen:
cd /opt/zulip && docker compose config --quiet
# Keine Ausgabe = keine SyntaxfehlerSchritt 4: Images laden
Lade alle Container-Images vorab herunter – das spart Zeit beim späteren Start und zeigt frühzeitig, ob die GitHub Container Registry erreichbar ist (ca. 1 GB gesamt):
cd /opt/zulip && docker compose pullVerifizieren:
docker compose images
# Alle fünf Images sollten mit konkreten Tags aufgelistet seinSchritt 5: Datenbank initialisieren (Pflichtschritt)
Dieser Schritt wird in vielen älteren Anleitungen vergessen und ist der häufigste Grund für einen fehlschlagenden ersten Start: Bevor der eigentliche Stack hochfährt, muss die Datenbank initialisiert werden. Der app:init-Befehl richtet das Datenbankschema ein, legt Standarddaten an und bereitet die Zulip-Konfiguration vor:
cd /opt/zulip && docker compose run --rm zulip app:initDieser Befehl startet kurzzeitig die Abhängigkeiten (PostgreSQL, Memcached, RabbitMQ, Redis) und führt dann alle Migrationen aus. Das kann mehrere Minuten dauern. Warte, bis der Befehl erfolgreich abgeschlossen ist – du erkennst das an einer Ausgabe ohne Fehlermeldungen und einem regulären Exit-Code.
Verifizieren:
echo "Exit-Code: $?"
# Erwartete Ausgabe: Exit-Code: 0Schritt 6: Stack starten
Jetzt startest du den vollständigen Stack im Hintergrund und wartest, bis der Zulip-Container als bereit gilt:
cd /opt/zulip && docker compose up zulip --waitDas --wait-Flag blockiert, bis der Zulip-Container seinen Health-Check bestanden hat. Alternativ startest du mit docker compose up -d und beobachtest die Logs live:
docker compose logs -f zulipVerifizieren: Alle Container müssen laufen, der Zulip-Container ohne Fehler:
cd /opt/zulip && docker compose ps
# Erwartete Ausgabe (alle Container mit Status "running" oder "Up"):
# NAME IMAGE STATUS
# zulip ghcr.io/zulip/zulip-server:12.0-1 Up
# database postgres:14 Up
# memcached memcached:alpine Up
# rabbitmq rabbitmq:4.2 Up
# redis redis:alpine Up
docker compose logs zulip --tail=20
# Keine CRITICAL/ERROR-MeldungenHTTP-Erreichbarkeit testen:
curl -sI http://localhost/ | head -5
# Erwartete Ausgabe: HTTP/1.1 200 OK oder HTTP/1.1 302 FoundSchritt 7: Erste Organisation einrichten
Zulip-Instanzen sind in „Realms" (Organisationen) aufgeteilt. Nach dem ersten Start gibt es noch keine – du generierst einen einmaligen Einrichtungslink über das Management-Kommando:
cd /opt/zulip && docker compose exec zulip ./manage.py generate_realm_creation_linkDer Befehl gibt eine URL aus, z. B. http://zulip.example.com/new/abc123def456. Öffne diesen Link im Browser, um deine erste Organisation anzulegen, den Administrator-Account zu erstellen und Zulip grundlegend zu konfigurieren (Organisationsname, Standard-Streams usw.).
Wichtig: Der Link ist einmalig gültig. Erstelle ihn erst dann, wenn du direkt danach mit der Einrichtung beginnst.
Verifizieren: Nach dem Aufruf des Links im Browser sollte das Zulip-Registrierungsformular erscheinen. Nach Abschluss der Einrichtung loggst du dich mit dem Admin-Account ein und siehst die Zulip-Oberfläche mit dem Streams-Panel auf der linken Seite.
Schritt 8: HTTPS einrichten (empfohlen)
HTTP-only ist der Standard-Default von Zulip 12.0 – für den produktiven Betrieb solltest du HTTPS aktivieren. Die eleganteste Lösung für selbst gehostete Stacks ist ein vorgelagerter Reverse Proxy, der TLS terminiert. Dafür eignet sich z. B. Caddy als Reverse Proxy mit automatischem HTTPS oder der Nginx Proxy Manager.
Beim Einsatz eines Reverse Proxys:
- Entferne Port
443:443aus demports-Abschnitt in einercompose.override.yaml– der Proxy übernimmt TLS. - Lass Port
80:80weiter gemappt, damit der Proxy intern kommunizieren kann. SETTING_EXTERNAL_HOSTmuss auf die öffentliche Domain zeigen (ohnehttp://-Präfix).- Setze den Proxy so, dass er die Header
X-Forwarded-ForundX-Forwarded-Protokorrekt weitergibt.
Verifizieren:
curl -sI https://zulip.example.com/ | head -5
# Erwartete Ausgabe: HTTP/2 200 (oder 302 zur Login-Seite)Schritt 9: Updates und Backups
Für Updates trägst du den neuen Image-Tag in die compose.yaml ein (z. B. 12.1-1), sicherst vorher die Volumes und führst dann das Upgrade durch. Verwende niemals den Tag :latest in der Produktivumgebung – pin immer auf einen expliziten Tag. Detaillierte Hinweise zu Container-Update-Strategien findest du in der Anleitung Docker-Container automatisch aktualisieren nach dem Watchtower-Aus.
# Volumes sichern (Beispiel)
docker run --rm -v zulip:/data -v $(pwd)/backup:/backup alpine \
tar czf /backup/zulip-data.tar.gz /data
# Neues Image laden und Datenbank-Upgrade ausführen
cd /opt/zulip && docker compose pull
docker compose run --rm zulip app:upgrade
docker compose up zulip --waitDie Named Volumes (postgresql-14, zulip, redis, rabbitmq) bleiben bei einem docker compose down erhalten – erst docker compose down --volumes löscht alle Daten unwiderruflich. Mehr zur Backup-Strategie: 3-2-1-Backup-Strategie umsetzen.
Verifizieren:
cd /opt/zulip && docker compose ps
docker compose logs zulip --tail=10
# Kein "migration error", kein "CRITICAL"Troubleshooting / Typische Fehler
- „Permission denied" / Container startet nicht: Du verwendest Rootless Docker oder uDocker. Zulip setzt das
ulimit nofileauf 1.048.576 – das erfordert Root. Wechsle zu Standard-Docker als Root-User. - „zulip__secret_key secret not set": Die
.env-Datei fehlt oder derZULIP__SECRET_KEYist nicht gesetzt. Prüfe mitgrep ZULIP__SECRET_KEY /opt/zulip/.env. Der Wert muss mindestens 50 Zeichen lang sein. - „Could not connect to database": Der
app:init-Schritt (Schritt 5) wurde übersprungen. Führedocker compose run --rm zulip app:initaus, bevor du den Stack startest. - „SMTP authentication failed" / E-Mail-Fehler: Überprüfe
SETTING_EMAIL_HOST,SETTING_EMAIL_HOST_USER,ZULIP__EMAIL_PASSWORDund den Port (587 mit TLS ist Standard für moderne Provider). Einige Provider erfordern App-Passwörter statt des Account-Passworts. - „ERR_NAME_NOT_RESOLVED" im Browser:
SETTING_EXTERNAL_HOSTstimmt nicht mit dem tatsächlichen DNS-Namen überein. Der Wert darf keinhttp://-Präfix und keinen Trailing Slash enthalten – nur den nackten Hostnamen, z. B.zulip.example.com. - Organisation kann nicht erstellt werden / Realm-Link fehlt: Führe nach dem Start
docker compose exec zulip ./manage.py generate_realm_creation_linkaus und öffne die ausgegebene URL im Browser. - „Out of memory" / Container wird gekillt (OOMKilled): Weniger als 2 GB RAM verfügbar. PostgreSQL, RabbitMQ, Memcached, Redis und die Zulip-App brauchen zusammen mindestens 2 GB. Mit einem VPS unter 2 GB läuft Zulip nicht stabil.
- Port 25 bereits belegt: Ein lokaler MTA (Postfix, Exim) belegt Port 25. Entferne
"25:25"aus demports-Abschnitt, wenn du keine eingehenden E-Mails in Streams benötigst. - Upgrade schlägt fehl / Datenverlust: Image-Tag stand auf
latestund hat ein Major-Upgrade eingespielt. Immer expliziten Tag verwenden und vor jedem Upgrade Volumes sichern.
Häufige Fragen
Was ist der Unterschied zwischen Streams und Topics?
Streams sind Kanäle – vergleichbar mit Slack-Channels, etwa #entwicklung oder #support. Topics sind Threads innerhalb eines Streams, z. B. „Bug #1234 – Login schlägt fehl" oder „Sprint 42 – Planung". Jede Nachricht gehört zu genau einem Stream und einem Topic. Das ist Zulips Kernvorteil: Konversationen bleiben auch Tage später gezielt auffindbar, ohne durch irrelevante Nachrichten zu scrollen.
Kann ich Zulip hinter einem Reverse Proxy betreiben?
Ja – und das ist für den produktiven Betrieb empfohlen. Setze SETTING_EXTERNAL_HOST auf die öffentliche Domain, entferne Port 443:443 aus der compose.yaml (der Proxy terminiert TLS) und stelle sicher, dass der Proxy die X-Forwarded-Proto- und X-Forwarded-For-Header korrekt weitergibt. Port 80 muss weiter in Zulip erreichbar sein.
Wie erstelle ich nach der Installation weitere Admins?
Nach der Erst-Einrichtung über den Realm-Creation-Link kannst du weitere Administratoren über die Zulip-Weboberfläche ernennen: Einstellungen → Nutzer und Gruppen → Nutzer → Nutzer bearbeiten → Rolle: Administrator. Alternativ per Kommandozeile: docker compose exec zulip ./manage.py promote_new_full_member.
Werden Daten bei „docker compose down" gelöscht?
Nein – solange du nicht das Flag --volumes anhängst. Die Named Volumes postgresql-14, zulip, redis und rabbitmq bleiben erhalten. Mit docker compose down --volumes werden alle Daten unwiderruflich gelöscht.
Unterstützt Zulip SSO, LDAP oder SAML?
Ja. Über die Umgebungsvariable SETTING_AUTHENTICATION_BACKENDS lassen sich LDAP, SAML, GitHub-OAuth, Google-OAuth und weitere Backends aktivieren. Die Konfiguration erfolgt per SETTING_-Variablen in der compose.override.yaml. Details dazu findest du in der offiziellen Zulip-Dokumentation unter Authentication Backends.
Warum gibt es jetzt zwei verschiedene Images?
Das alte Image zulip/docker-zulip auf Docker Hub ist ein Legacy-Image und erhält ab Version 12.x keine Updates mehr. Das aktuelle Image heißt ghcr.io/zulip/zulip-server:12.0-1 und liegt auf der GitHub Container Registry. Es hat auch eine andere Struktur für Umgebungsvariablen und Secrets (doppelter Unterstrich: ZULIP__SECRET_KEY). Ältere Anleitungen im Internet, die noch das Docker-Hub-Image beschreiben, sind veraltet und nicht kompatibel.
Wie sichere ich Zulip sinnvoll ab?
Die vier Named Volumes enthalten alle persistenten Daten. Ein gutes Backup-Konzept sichert diese regelmäßig extern. Zulip bietet zusätzlich ein eigenes Export-Format (./manage.py export), das alle Nachrichten, Nutzer und Anhänge in ein portables Archiv packt.
Fazit
Zulip ist kein Leichtgewicht – sechs Container, Mindest-RAM von 2 GB, ein obligatorischer Init-Schritt vor dem ersten Start und ein Secrets-Management, das sich von früheren Versionen grundlegend unterscheidet. Wer diese Einstiegshürde überwindet, bekommt dafür die strukturierteste selbst gehostete Team-Chat-Plattform, die aktuell verfügbar ist. Das Streams-Topics-Konzept ist kein optionales Feature, sondern der Kern der Plattform – und genau das macht Zulip für Teams ab etwa fünf Personen zu einer ernsthaften Alternative zu Slack und Microsoft Teams: Diskussionen bleiben auffindbar, auch wenn man nicht live dabei war. Für Entwicklerteams, Open-Source-Projekte und KMU, die volle Datenkontrolle wollen und bereit sind, etwas mehr Setup-Aufwand zu investieren, ist Zulip eine klare Empfehlung.
Wer den Stack weiter absichern möchte, findet in der Anleitung Docker Compose absichern: Secrets, Healthchecks, Non-Root und Read-Only wertvolle ergänzende Maßnahmen.
Weiterführende Anleitungen und Quellen
- Docker und Docker Compose auf Linux installieren (Ubuntu/Debian): die Self-Hosting-Grundlage
- Caddy als Reverse Proxy einrichten: Anfänger-Anleitung mit automatischem HTTPS
- Docker Compose absichern: Secrets, Healthchecks, Non-Root und Read-Only für den Produktivbetrieb
- Docker-Container automatisch aktualisieren nach dem Watchtower-Aus: Diun, WUD und Renovate im Vergleich
- 3-2-1-Backup-Strategie umsetzen: Anleitung mit Restic, USB-Disk und S3-Cloud
Offizielle Quellen: Zulip Docker – Offizielle Dokumentation | GitHub: zulip/docker-zulip