Skip to content

Installing Zulip a Groupware Service

About

Zulip is an open-source team chat platform that combines the real-time speed of Slack with the organization of email. Its standout feature is topic-based threading, which keeps conversations categorized so you can focus on what matters without getting buried in a single, messy chat stream.

It is built for developers and fast-moving teams who need to balance personal messaging, file sharing, and deep discussions in a way that's easy to search and follow later.

Installation

Context

The software is to be installed in a LXC-container (not LXD) behind SSL-proxy on the host of the container which has * acme.sh* installed.

Download & Extract

Download the lastest using the following.

# Change in to temporary directory.
cd $(mktemp -d)
wget https://download.zulip.com/server/zulip-server-latest.tar.gz

Extract the tar file in the current temporary directory.

tar -xf zulip-server-latest.tar.gz

Install

Run the installer as root.

./zulip-server-*/scripts/setup/install --hostname=your.domain.com --email=admin@your.domain.com --self-signed-cert

--self-signed-cert : This option is even required, even when not using SSL at all. --no-init-db : This option instructs the installer to not do any database initialization. This should be used when you already have a Zulip database.
--no-overwrite-settings : This option preserves existing /etc/zulip configuration files.

Configure Zulip for Reverse Proxy

Edit /etc/zulip/settings.py:

# Tell Zulip it's behind a reverse proxy
EXTERNAL_HOST = 'your.domain.com'
ZULIP_ADMINISTRATOR = 'admin@your.domain.com'

# Trust the proxy's forwarded headers
USING_PGROONGA = False  # unless you need it

Edit /etc/zulip/zulip.conf and add:

[application_server]
# Zulip listens on HTTP, proxy handles HTTPS.
http_only = true

[loadbalancer]
#  IP must be the host's IP as seen from the container (typically its bridge interface IP).
ips = 10.0.3.1

Setting http_only = true makes Zulip's nginx listen on port 80 only (no internal TLS), since Apache handles encryption.

Run the next scripts to apply the changes made in the ini-file.

sudo /home/zulip/deployments/current/scripts/zulip-puppet-apply
sudo /home/zulip/deployments/current/scripts/restart-server

Database Initialization

Initialize the Database

sudo -u zulip /home/zulip/deployments/current/scripts/setup/initialize-database

SMTP-Server

Edit the settings file /etc/zulip/settings.py and add the following lines:

EMAIL_HOST = "smtp.example.com"
EMAIL_HOST_USER = "username"
EMAIL_USE_TLS = True
EMAIL_PORT = 587
EMAIL_HOST_PASSWORD = "the-password"

For Strato:

EMAIL_HOST = "smtp.strato.com"
EMAIL_HOST_USER = "zulip@example.com"
EMAIL_USE_TLS = True
EMAIL_PORT = 587

Edit /etc/zulip/zulip-secrets.conf and add:

email_password = your_password_here

Restart Zulip inside the container:

sudo /home/zulip/deployments/current/scripts/restart-server

Test the SMTP-Server:

sudo -u zulip /home/zulip/deployments/current/manage.py send_test_email user@example.com

Host Proxy Configuration

Apache Reverse Proxy

<VirtualHost *:443>
    ServerName your.domain.com

    # ... your existing SSL config ...

    # WebSocket support (critical for Zulip)
    RewriteEngine On
    RewriteCond %{HTTP:Upgrade} websocket [NC]
    RewriteCond %{HTTP:Connection} upgrade [NC]
    RewriteRule ^/?(.*) "ws://127.0.0.1:80/$1" [P,L]

    ProxyPreserveHost On
    ProxyPass / http://127.0.0.1:80/
    ProxyPassReverse / http://127.0.0.1:80/

    # Pass real IP to Zulip
    RequestHeader set X-Forwarded-Proto "https"
    RequestHeader set X-Real-IP %{REMOTE_ADDR}s
</VirtualHost>

Nginx Reverse Proxy

server {
    listen 80;
    server_name your.domain.com;
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl;
    server_name your.domain.com;

    # ... your existing SSL config ...

    # Proxy to Zulip in the LXC container
    location / {
        proxy_pass http://192.168.100.10:80;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;

        # WebSocket support (critical for Zulip)
        proxy_http_version 1.1;
        proxy_set_header Upgrade    $http_upgrade;
        proxy_set_header Connection $connection_upgrade;

        # Timeouts — Zulip uses long-polling
        proxy_read_timeout 1200s;
        proxy_send_timeout 1200s;
    }
}

The long proxy_read_timeout and proxy_send_timeout values are important — Zulip uses long-polling for real-time updates, and default Nginx timeouts of 60s will cause constant unnecessary disconnections.

Required: Upgrade Map Block

This needs to be in your nginx.conf (in the http {} block), not in the vhost:

http {

    # ...existing config...

    map $http_upgrade $connection_upgrade {
        default upgrade;
        ''      close;
    }
}