Zulip mit Docker installieren: Strukturierter Team-Chat mit Kanälen und Themen
Zulip 12.3 auf dem offiziellen docker-zulip-Weg betreiben: Release-Tag klonen, compose.override.yaml, Docker Secrets aus der .env, lokale Ports mit !override, Reverse Proxy, getestetes Backup mit app:restore und Update.
Geprüft am 14.10.2026 · für docker-zulip 12.3-0
WerbelinksMit * markierte Links sind Werbelinks: Bei einem Kauf erhalten wir eine Provision, der Preis bleibt gleich. Als Amazon-Partner verdiene ich an qualifizierten Verkäufen. Mehr dazu

Zulip ist ein Team-Chat mit einem eigenen Ordnungsprinzip: Nachrichten gehören zu einem Kanal und innerhalb des Kanals zu einem Thema (Topic). So lassen sich parallele Diskussionen auch Stunden später noch nachlesen. Diese Anleitung zeigt den offiziellen Docker-Weg des Projekts mit dem Repository docker-zulip und dem Image ghcr.io/zulip/zulip-server:12.3-0: Konfiguration in compose.override.yaml, Geheimnisse als Docker Secrets aus einer .env, Betrieb hinter einem Reverse Proxy, Backup und Update per Git-Tag.
Voraussetzungen
- Server: x86_64 oder ARM64 (aarch64). Laut Zulip-Doku reichen für kleinere Organisationen 1 CPU und 2 GB RAM mit 2 GB Swap, ab 100 Nutzern 2 CPUs und 4 GB RAM. Unter 5 GB RAM verlangt Zulip Swap. Im Test auf einem Host mit 8 GB RAM belegte der Zulip-Container im Leerlauf rund 3 GB, die übrigen Dienste zusammen etwa 330 MB. Planen Sie 4 GB RAM als Untergrenze ein.
- Speicher: mindestens 10 GB frei für eine Organisation mit einigen Dutzend Nutzern, SSD empfohlen.
- Software: Linux mit Docker Engine, Compose-Plugin und
git. - Netz: ein DNS-Name wie
chat.example.com. Zulip akzeptiert keine IP-Adresse alsEXTERNAL_HOST. Zulip verlangt HTTPS, entweder über einen Reverse Proxy oder über das eingebaute Certbot. - Mail: Zugangsdaten für einen SMTP-Server. Ohne Mailversand funktionieren Einladungen und Passwort-Zurücksetzen nicht.
| Dienst | Image | Aufgabe |
|---|---|---|
zulip | ghcr.io/zulip/zulip-server:12.3-0 | Anwendung mit nginx, Volume zulip unter /data |
database | zulip/zulip-postgresql:14 | PostgreSQL 14 mit Zulip-Erweiterungen, Volume postgresql-14 |
memcached | memcached:alpine | Cache mit SASL-Passwort |
rabbitmq | rabbitmq:4.2 | Warteschlangen, Volume rabbitmq |
redis | redis:alpine | Rate-Limits und Sitzungsdaten, mit Passwort |
Wichtig für Bestandsinstallationen: Das frühere Image zulip/docker-zulip auf Docker Hub ist mit dem Ende der 11.x-Reihe eingestellt. Ab 12.x liegt das Image auf GHCR, die Konfiguration gehört in compose.override.yaml, Umgebungsvariablen des alten Images lehnt der Container seit 12.1 beim Start ab.
Schritt 1: Repository auf den Release-Tag klonen
Der Git-Tag des Repositorys ist laut Doku die maßgebliche Version: Er hält compose.yaml, Beispieldatei und Image-Tag zusammen. Das Projekt veröffentlicht keine gleitenden Tags wie latest.
sudo git clone https://github.com/zulip/docker-zulip.git /opt/docker-zulip
cd /opt/docker-zulip
sudo git checkout -B release 12.3-0
sudo cp compose.override.yaml.example compose.override.yaml
Die compose.yaml bearbeiten Sie nicht. Alle eigenen Einstellungen landen in der compose.override.yaml, die Git ignoriert. So bleibt ein späteres Update konfliktfrei.
Verifizieren: git describe --tags zeigt 12.3-0, grep image: compose.yaml enthält ghcr.io/zulip/zulip-server:12.3-0.
Schritt 2: Geheimnisse in der .env hinterlegen
Die Dienste authentifizieren sich untereinander mit Docker Secrets. Die Beispieldatei liest sie aus Umgebungsvariablen der Compose-Umgebung, also aus einer .env im Projektordner. Diese Variablen gelangen nicht direkt in die Container, sondern nur über den secrets-Mechanismus.
cd /opt/docker-zulip
{
for s in POSTGRES MEMCACHED RABBITMQ REDIS; do echo "ZULIP__${s}_PASSWORD=$(openssl rand -hex 24)"; done
echo "ZULIP__SECRET_KEY=$(openssl rand -hex 32)"
echo "ZULIP__EMAIL_PASSWORD=SMTP_PASSWORT"
} | sudo tee .env > /dev/null
sudo chmod 600 .env
Das PostgreSQL-Passwort wird nur beim ersten Start der Datenbank verwendet. Wer es später ändert, muss es laut Doku zusätzlich per ALTER ROLE in der Datenbank setzen, sonst kann sich Zulip nicht mehr anmelden.
Verifizieren: sudo wc -l .env zeigt 6. docker compose config --quiet endet ohne Fehler. Achtung: Auch eine leere .env besteht diese Prüfung, kontrollieren Sie daher die Zeilenzahl.
Schritt 3: compose.override.yaml konfigurieren
Ergänzen Sie in der kopierten Datei im Block services.zulip die Pflichtwerte, die Proxy-Vertrauensstellung, SMTP und die Ports. Der obere Block secrets: aus der Beispieldatei bleibt unverändert.
services:
zulip:
ports: !override
- name: http
target: 80
published: 8082
host_ip: 127.0.0.1
app_protocol: http
environment:
SETTING_EXTERNAL_HOST: "chat.example.com"
SETTING_ZULIP_ADMINISTRATOR: "it@example.com"
TRUST_GATEWAY_IP: True
SETTING_EMAIL_HOST: "smtp.example.com"
SETTING_EMAIL_HOST_USER: "noreply@example.com"
SETTING_EMAIL_PORT: "587"
SETTING_EMAIL_USE_TLS: True
secrets: []
Warum !override: Compose hängt Listen aus der Override-Datei an die Originalliste an. Ohne den Tag würde Docker weiterhin die Ports 25, 80 und 443 des Hosts belegen. Mit !override bleibt nur Port 8082 an 127.0.0.1. Port 25 brauchen Sie nur für das Mail-Gateway, das eingehende Mails in Nachrichten umwandelt.
Ohne CERTIFICATES liefert der Container unverschlüsseltes HTTP auf Port 80 und erwartet einen TLS-Proxy davor. TRUST_GATEWAY_IP: True lässt Zulip der Docker-Gateway-Adresse vertrauen, über die ein Proxy auf demselben Host zugreift. Läuft der Proxy auf einem anderen Rechner, setzen Sie stattdessen LOADBALANCER_IPS mit dessen Adresse.
Verifizieren: docker compose config | grep -A4 'published' zeigt nur host_ip: 127.0.0.1 und published: "8082". docker compose config | grep SETTING_EXTERNAL_HOST zeigt Ihren Namen.
Schritt 4: Erstinitialisierung und Start
cd /opt/docker-zulip
docker compose pull
docker compose run --rm zulip app:init
docker compose up -d --wait
docker compose ps
app:init startet die Abhängigkeiten, prüft die Konfiguration, legt die Datenbank an und endet mit der Zeile === End Initial Configuration Phase ===. Fehlt diese Zeile, lesen Sie die Ausgabe auf Warnungen. up --wait kehrt erst zurück, wenn der Healthcheck des Containers erfolgreich ist.
Im Test dauerte der Download rund 90 Sekunden, app:init weitere 50 Sekunden mit Zeilen wie Applying zerver.0663_realm_enable_guest_user_dm_warning... OK. Nach up --wait war der Container rund 40 Sekunden später healthy.
Verifizieren: docker compose ps zeigt zulip als healthy. curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: chat.example.com' -H 'X-Forwarded-Proto: https' http://127.0.0.1:8082/login/ liefert 200.
Schritt 5: Reverse Proxy mit HTTPS
Zulip verlangt HTTPS. Der Proxy muss X-Forwarded-For und X-Forwarded-Proto setzen und lange Verbindungen für die Ereignis-Schnittstelle zulassen. Ein Nginx-Beispiel auf Basis der Zulip-Doku zu Reverse Proxys:
server {
listen 443 ssl;
http2 on;
server_name chat.example.com;
ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;
client_max_body_size 100m;
location / {
proxy_pass http://127.0.0.1:8082;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 20m;
}
}
Ohne client_max_body_size scheitern laut Zulip-Doku größere Uploads am Proxy. Passen Sie den Wert an Ihr Upload-Limit an. Alternativ übernimmt der Container TLS selbst: CERTIFICATES: certbot holt ein Let's-Encrypt-Zertifikat und erneuert es automatisch. Dann muss Port 443 des Containers öffentlich erreichbar sein und die !override-Liste Port 443 statt 80 enthalten. Grundlagen zu Zertifikaten finden Sie in Nginx als Reverse Proxy mit TLS.
Verifizieren: sudo nginx -t meldet syntax is ok. curl -sI https://chat.example.com/login/ liefert HTTP/2 200.
Schritt 6: Organisation anlegen
Zulip legt Organisationen über einen Einmal-Link an. Das mitgelieferte Skript manage.py im Repository ruft den Verwaltungsbefehl im laufenden Container auf:
cd /opt/docker-zulip
./manage.py generate_realm_creation_link
Please visit the following secure single-use link to register your
new Zulip organization:
https://chat.example.com/new/...
Öffnen Sie den Link im Browser, geben Sie Name und Adresse der Organisation ein und legen Sie das erste Konto an. Es wird Eigentümer der Organisation. Einladungen, Registrierungsregeln und Authentifizierung stellen Sie danach unter Einstellungen, Organisation ein.
Verifizieren: Der Link öffnet ein Formular mit den Feldern für Organisationsname, Adresse und E-Mail. Nach der Bestätigungsmail gelingt die Anmeldung, und im Kanal „general“ lässt sich eine Nachricht mit Thema senden.
Schritt 7: Backup und Update
Laut Doku ist das Volume zulip die Einheit der Sicherung. Es enthält Uploads, Zertifikate, die Datei zulip-secrets.conf und unter /data/backups/ einen Datenbank-Dump. app:backup läuft standardmäßig täglich, vor einem Snapshot aktualisieren Sie den Dump von Hand:
cd /opt/docker-zulip
docker compose exec zulip /sbin/entrypoint.sh app:backup
docker compose run --rm -v zulip:/data -v "$(pwd)":/backup zulip tar czf /backup/backup.tar.gz -C /data .
sudo cp .env compose.override.yaml /sicherer/ort/
Das PostgreSQL-Datenverzeichnis im Volume postgresql-14 ist im laufenden Betrieb nicht konsistent kopierbar, maßgeblich ist der Dump. Die .env mit den Geheimnissen sichern Sie getrennt und geschützt. Wiederhergestellt wird laut Doku mit app:restore <dateiname>, entweder auf einem frischen Host nach dem Entpacken des Volumes oder direkt im laufenden Container.
Im Test meldete app:backup Backup process succeeded., das Archiv enthielt zulip-secrets.conf und den Dump unter backups/. Eine vor dem Backup angelegte Markierungstabelle wurde nach dem Löschen per app:restore vollständig zurückgeholt, der Container startete danach selbst neu und die Anmeldeseite lieferte wieder HTTP 200.
Ein Update wechselt den Git-Tag und damit den Image-Tag. Beim ersten Start migriert der Container die Datenbank:
cd /opt/docker-zulip
git fetch --tags
git checkout -B release NEUER_TAG
diff compose.override.yaml.example compose.override.yaml
docker compose pull
docker compose up -d
Ein Downgrade verlangt laut Doku ein manuelles Zurücksetzen der Migrationen mit manage.py migrate. Planen Sie Updates deshalb mit einem frischen Backup.
Verifizieren: tar tzf backup.tar.gz | grep backups/ listet mindestens eine Dump-Datei. Nach einem Update zeigt docker compose images zulip den neuen Tag und der Container wird wieder healthy.
Typische Fehler
| Symptom | Ursache | Lösung |
|---|---|---|
Bind for 0.0.0.0:80 failed: port is already allocated | ports in der Override-Datei ohne !override, Liste wird angehängt | ports: !override wie in Schritt 3 |
docker compose config meldet keinen Fehler, obwohl die .env leer ist | Leere Variablen gelten als gültige Secrets | Schritt 2 wiederholen, Zeilen der .env zählen |
| Anmeldung der App an PostgreSQL schlägt fehl | ZULIP__POSTGRES_PASSWORD nach dem ersten Start geändert | Altes Passwort zurück oder ALTER ROLE zulip WITH PASSWORD laut Doku |
| Container bricht mit Hinweis auf umbenannte Variable ab | Variablen des alten Images wie DB_HOST oder DISABLE_HTTPS | Nach der Doku „Upgrading from legacy“ umbenennen |
| Falsche Client-IP in Logs, Weiterleitungen auf HTTP | Proxy nicht vertrauenswürdig oder X-Forwarded-Proto fehlt | TRUST_GATEWAY_IP oder LOADBALANCER_IPS setzen, Header im Proxy prüfen |
| Keine Einladungsmails | SMTP fehlt oder falsch | ./manage.py send_test_email it@example.com ausführen und Ausgabe prüfen |
Häufige Fragen
Warum nicht die compose.yaml direkt bearbeiten?
Weil ein Update per git checkout sie ersetzt. Eigene Werte in der ignorierten compose.override.yaml bleiben dabei erhalten.
Gibt es mobile Push-Benachrichtigungen?
Ja, über den Push-Dienst von Zulip. Dafür setzen Sie SETTING_ZULIP_SERVICE_PUSH_NOTIFICATIONS und registrieren den Server mit ./manage.py register_server. Das überträgt Metadaten an den Dienst des Herstellers, prüfen Sie das vorab datenschutzrechtlich.
Kann ich eine vorhandene PostgreSQL-Instanz nutzen?
Ja, die Doku beschreibt unter „Existing services“, wie Sie den mitgelieferten Datenbankdienst mit !reset entfernen und SETTING_REMOTE_POSTGRES_HOST setzen.
Fazit
Der offizielle Docker-Weg von Zulip ist mit 12.x deutlich klarer geworden: fester Release-Tag im Git-Repository, eigene Werte in compose.override.yaml, Geheimnisse als Docker Secrets. Mit lokal gebundenem Port, einem TLS-Proxy, funktionierendem SMTP und einer regelmäßigen Sicherung des Volumes zulip samt .env steht eine gut wartbare Chat-Plattform, deren Themenstruktur gerade in verteilten Teams den Überblick erleichtert.


