Zulip: Self-Host a Team Chat That Scales Beyond Slack
Slack and Teams are the default for team communication, but both have friction: Slack's free tier deletes message history after 90 days, and Teams bundles into Microsoft's ecosystem whether you need it or not. Self-hosted alternatives like Mattermost are available but feel like Slack clones.
Photo by Marc Mintel on Unsplash
Zulip takes a different approach. It's built around topics — every message belongs to a stream (channel) and a topic (thread). This model scales better than Slack's channels as team and conversation volume grows. Used by Dropbox, MongoDB, Akamai, and by open-source communities like Python, Rust, and PostgreSQL, Zulip has proven itself for real organizations.
Why Zulip's Model Is Different
In Slack, a busy channel is a wall of messages. You either read it chronologically or miss context. Zulip adds a mandatory topic to every message, creating persistent threads within each stream. When you open a stream, you see a list of recent topics — each a separate conversation thread.
The benefits:
- Catchup is faster: You can read topics you care about and mark others as read
- Context is preserved: A question about the database schema is findable later because it's in
#backend > database-schema, not lost in#dev - Async-friendly: Because threads are persistent, conversations can span hours or days without losing context
- Searchable: Find "what did we decide about API versioning" by searching across topic names
For distributed or async teams, this model is significantly better than Slack's flat channels.
Requirements
Zulip's server is a Django application. The recommended deployment uses Docker, but Zulip also provides a production installer for Ubuntu/Debian.
Minimum: 2 CPU cores, 2 GB RAM, 20 GB disk Recommended: 4 cores, 4 GB RAM for teams over 20 users
Docker Deployment
Zulip provides official Docker images:
mkdir zulip && cd zulip
# Create configuration
cat > docker-compose.yml << 'EOF'
version: "3"
services:
database:
image: "zulip/zulip-postgresql:14"
environment:
POSTGRES_DB: "zulip"
POSTGRES_USER: "zulip"
POSTGRES_PASSWORD: "zulip-db-password"
volumes:
- ./postgresql/data:/var/lib/postgresql/data:rw
memcached:
image: "memcached:alpine"
rabbitmq:
image: "rabbitmq:3.7.7"
environment:
RABBITMQ_DEFAULT_USER: "zulip"
RABBITMQ_DEFAULT_PASS: "zulip-mq-password"
redis:
image: "redis:alpine"
volumes:
- ./redis/data:/data:rw
zulip:
image: "zulip/docker-zulip:9-latest"
ports:
- "80:80"
- "443:443"
environment:
SETTING_EXTERNAL_HOST: "chat.yourdomain.com"
SETTING_ZULIP_ADMINISTRATOR: "[email protected]"
SETTING_EMAIL_HOST: "smtp.gmail.com"
SETTING_EMAIL_HOST_USER: "[email protected]"
SETTING_EMAIL_PORT: "587"
SETTING_EMAIL_USE_TLS: "True"
ZULIP_AUTH_BACKENDS: "EmailAuthBackend"
SECRETS_email_password: "your-app-password"
SECRETS_rabbitmq_password: "zulip-mq-password"
SECRETS_postgres_password: "zulip-db-password"
SECRETS_memcached_password: ""
SECRETS_redis_password: ""
SECRETS_secret_key: "your-secret-key-here"
SSL_CERTIFICATE_GENERATION: "self-signed"
volumes:
- ./zulip:/data:rw
depends_on:
- database
- memcached
- rabbitmq
- redis
EOF
docker compose up -d
The first startup takes several minutes while Zulip initializes the database. Check progress with docker compose logs -f zulip.
Like what you're reading? Subscribe to Self-Hosted Weekly — free weekly guides in your inbox.
SSL with Certbot
For production, replace the self-signed certificate. Zulip can manage Let's Encrypt automatically:
# In docker-compose.yml, change:
SSL_CERTIFICATE_GENERATION: "certbot"
Or use an external reverse proxy (Nginx/Caddy) and set SSL_CERTIFICATE_GENERATION: "no" with LOADBALANCER_IPS pointing to your proxy.
First-Time Configuration
After the containers start, navigate to https://chat.yourdomain.com and complete the setup wizard:
- Register your admin account with the email set in
SETTING_ZULIP_ADMINISTRATOR - Name your organization
- Choose your plan (self-hosted is the Community/Enterprise plan)
- Optionally import users from LDAP/SAML or invite them by email
Streams and Topics Setup
Streams (channels) in Zulip have a few properties:
- Public: All organization members can join and read
- Private: Invite-only, members must be added explicitly
- Web-public: Readable without login (for open-source communities)
Create an initial set of streams to get your team started:
#general — company-wide announcements
#engineering — technical discussions
#product — product decisions and roadmap
#random — off-topic conversation
#ops — infrastructure and deployment
Within each stream, topics emerge naturally from conversations. You can also pin important topics and set stream notifications.
Integrations
Zulip has 90+ integrations via webhooks. Common ones:
GitHub: Post PR reviews, CI status, and issue events to a Zulip stream. Configure under Settings → Integrations → GitHub.
PagerDuty / Alertmanager: Send alerts to #ops. Useful for on-call teams.
Sentry / Bugsnag: Error notifications with stack traces.
CI/CD (GitHub Actions, Jenkins, GitLab CI): Build status and deployment notifications.
For custom integrations, Zulip's outgoing webhook format is simple:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"type": "stream", "to": "engineering", "topic": "deployments", "content": "🚀 prod deployed: v1.2.3"}' \
https://chat.yourdomain.com/api/v1/messages \
-u "[email protected]:bot-api-key"
Mobile Apps
Zulip has native iOS and Android apps. When logging in, enter your Zulip server URL instead of using the default zulip.com option. The apps support push notifications, which requires registering with Zulip's notification service:
# In docker-compose.yml:
SETTING_PUSH_NOTIFICATION_BOUNCER_URL: "https://push.zulipchat.com"
This sends notification metadata (not message content) through Zulip's push service — necessary because APNs and FCM require server certificates that aren't practical for self-hosted instances.
User Management and Authentication
Email/password: Default authentication. Users register or are invited by email.
LDAP/Active Directory: Connect to your corporate directory for SSO. Configure in SETTINGS_AUTH_LDAP_* environment variables.
SAML: Integrate with Okta, Google Workspace, Azure AD. Requires configuring SAML metadata.
Social auth: GitHub, Google OAuth.
Multiple backends can be enabled simultaneously — users can log in with either email or Google OAuth, for example.
Message History and Search
Unlike Slack's free tier, self-hosted Zulip keeps all messages forever. The PostgreSQL database holds message history, and Zulip's search indexes it with full-text search.
Useful search operators:
stream:engineering topic:deploy— messages in a specific stream+topicsender:[email protected]— messages from a userhas:attachment— messages with file attachmentsbefore:2026-01-01— historical messages
Backup and Recovery
# Stop Zulip
docker compose stop zulip
# Backup the database
docker exec zulip_database_1 pg_dump -U zulip zulip > zulip-backup.sql
# Backup file storage (uploads, avatars)
tar czf zulip-data.tar.gz ./zulip/
# Restart
docker compose start zulip
For automated backups, Zulip's Docker image includes a zulip/backup command that exports everything to a tarball.
Zulip vs Mattermost
| Zulip | Mattermost | |
|---|---|---|
| Conversation model | Streams + Topics | Channels (like Slack) |
| Self-hosted | Yes | Yes |
| Mobile apps | Yes | Yes |
| Resource usage | Medium | Lower |
| Best for | Async teams, open source communities | Teams wanting Slack feel |
Both are solid. Mattermost feels more familiar to Slack users. Zulip requires adjusting to the topic model but pays off for teams that generate high message volume.
Who Uses Self-Hosted Zulip
Zulip's own open-source community uses it — the Python, Rust, PostgreSQL, and Lean math communities all run public Zulip instances. If you browse rust-lang.zulipchat.com or python.zulipchat.com, you can see the topic model in action at scale: hundreds of streams, each with focused conversations that stay findable months later.
For a company, the self-hosted option means: unlimited users, unlimited history, your data stays on your servers, and you pay $0 in per-seat fees.
