Zulip
Overview
Zulip is an open source team collaboration and chat application. It combines the immediacy of real-time chat with the threaded conversation model of email. Every message belongs to a topic within a channel, so you can follow many conversations at once and catch up efficiently without missing anything.
The SecureAnyCloud image installs Zulip Server 12.0 from the official upstream production installer, which provisions and manages the entire single-server stack. The image stores the PostgreSQL message database on a dedicated Azure data disk, and it creates a fresh organization with a unique owner password on the first boot of every VM.
What you get:
- Zulip Server 12.0 served over HTTPS on port 443 by Zulip's own nginx with a self-signed certificate, and an HTTP-to-HTTPS redirect on port 80
- The full single-server stack that the installer manages: PostgreSQL with full-text search, Redis, RabbitMQ, memcached, and the Django and Tornado application servers under supervisor
- A dedicated Azure data disk at
/var/lib/postgresqlfor the PostgreSQL message database - A fresh organization (realm) and owner account with a unique password generated on first boot
nginx.serviceandsupervisor.serviceas systemd units, enabled and active- 24/7 SecureAnyCloud support
Prerequisites
You need an active Azure subscription, an SSH key pair, and a VNet with a subnet in the target region.
Standard_B4ms (4 vCPU / 16 GiB RAM) is a good starting point for the multi-service Zulip stack.
Configure the network security group (NSG) to allow inbound 22/tcp from your management network, 443/tcp for the Zulip web interface, and 80/tcp for the HTTP-to-HTTPS redirect.
Step 1 - Deploy from the Azure Marketplace
Sign in to the Azure Portal, choose Create a resource, search the Marketplace for Zulip by SecureAnyCloud, and select Create.
On the Basics tab, pick your subscription, resource group, region and size. Under Administrator account, choose SSH public key and paste your key. Under Inbound port rules, allow SSH (22), HTTP (80) and HTTPS (443).
Review the dedicated data disk on the Disks tab, then select Review + create and Create.
Step 2 - Deploy from the Azure CLI
# Accept the Marketplace terms once per subscription (required before the first CLI deploy)
az vm image terms accept --urn sac1647283583153:zulip-ubuntu-24-04:default:latest
az vm create \
--resource-group <your-rg> \
--name zulip \
--image sac1647283583153:zulip-ubuntu-24-04:default:latest \
--size Standard_B4ms \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name zulip --port 443 --priority 1010
az vm open-port --resource-group <your-rg> --name zulip --port 80 --priority 1020Step 3 - Connect to your VM
ssh azureuser@<vm-public-ip>Step 4 - Confirm the Zulip stack is running
On first boot, Zulip sets the server hostname to your instance address, restarts the stack, creates a fresh organization and owner account, and writes the credentials to a root-only file. Confirm that both services are active:
systemctl is-active nginx supervisorBoth services report active.
Next, confirm that the supervisor-managed Zulip processes are running and that the health endpoint answers:
sudo supervisorctl status zulip-django zulip-tornado curl -sk -o /dev/null -w 'HTTP %{http_code}\n' https://127.0.0.1/healthThe health endpoint returns HTTP 200.
Step 5 - Retrieve the organization owner password
The organization owner account is unique to your VM and is generated on first boot. Read the credentials file as root:
sudo cat /root/zulip-credentials.txtThe file contains the owner email, a unique password and the https://<vm-public-ip>/ URL. After your first sign-in, change the email and password in your personal settings.
Step 6 - Sign in to Zulip
Browse to https://<vm-public-ip>/ and accept the self-signed certificate warning. Then sign in with the owner email and the password from the credentials file.
Step 7 - Explore your organization
After you sign in, you land in your new organization. The left sidebar lists channels and their topics, and the main pane shows the message view.
Every message belongs to a topic within a channel, so a single channel can carry many parallel conversations that stay separate. Select a topic to read just that thread.
Step 8 - Organization settings
Open the organization settings from the gear menu. The owner account can manage users, channels, permissions, authentication methods and the organization profile here. Invite your team, create channels and configure your organization.
Data on the dedicated data disk
The PostgreSQL cluster data directory lives on a dedicated Azure data disk mounted at /var/lib/postgresql. This keeps the Zulip message database, which is the heaviest and most valuable per-deployment state, on a managed disk. You can snapshot, resize and back up that disk independently of the OS disk.
df -h /var/lib/postgresql | tail -1 sudo -u postgres psql -tAc 'SHOW data_directory;'The data directory reports a path under /var/lib/postgresql.
Replacing the self-signed certificate
The image ships a self-signed certificate so that the appliance is reachable over HTTPS immediately. For production, replace it with a trusted certificate. Once you have a DNS name that points at the VM, set Zulip's EXTERNAL_HOST to that name and use Zulip's built-in Certbot integration:
sudo sed -i 's/^EXTERNAL_HOST = .*/EXTERNAL_HOST = "your-domain.example.com"/' /etc/zulip/settings.py
sudo /home/zulip/deployments/current/scripts/setup/setup-certbot your-domain.example.com --email admin@your-domain.example.com
sudo /home/zulip/deployments/current/scripts/restart-serversetup-certbot obtains a Let's Encrypt certificate, installs it into Zulip's nginx and sets up automatic renewal.
Maintenance
Zulip ships new releases periodically. To upgrade, follow the official Zulip upgrade procedure with the management scripts that ship with the deployment:
sudo /home/zulip/deployments/current/scripts/upgrade-zulip-from-git mainThe OS continues to receive unattended security updates. The PostgreSQL message database persists on the dedicated data disk across reboots and upgrades.
Support
SecureAnyCloud maintains this image and provides 24/7 support. For help with this image, contact sales@secureanycloud.com.
Zulip is open source software licensed under the Apache License 2.0. "Zulip" is a trademark of Kandra Labs, Inc. SecureAnyCloud is not affiliated with or endorsed by Kandra Labs.
Deploy on Azure
Launch Zulip on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com
Overview
This guide covers the deployment and configuration of Zulip on AWS, using the SecureAnyCloud AWS Marketplace AMI. Zulip is an open source team chat application built around threaded conversations. Messages live in channels and, inside a channel, in named topics, so several discussions run in parallel without interleaving.
That structure makes Zulip readable asynchronously. People catch up topic by topic instead of scrolling a single firehose.
A production Zulip server is not one process. It is a PostgreSQL database with a full-text search extension, a RabbitMQ message broker, a Redis instance, a memcached cache, a Django application server, a Tornado real-time push server, a pool of queue workers and an nginx front door that terminates TLS. This image installs all of it with the vendor's own production installer, from the official upstream release tarball verified against a pinned SHA-256 digest, so what you run is unmodified upstream Zulip.
No secret in this image is shared between instances. Zulip mints its secrets when it is installed: the Django signing key, the avatar salt, the shared secret, the camo key, and the RabbitMQ, memcached and Redis passwords. An image that simply captured that state would give every customer the same Django signing key, which means a session token minted on one deployment would be valid on all of them.
The build strips all of these secrets before capture, together with the TLS private key, the RabbitMQ distribution cookie and the memcached authentication database. The image regenerates them on the first boot of your instance, before nginx is allowed to start.
What you get:
- Zulip Server 12.1, installed from the official upstream production tarball (
zulip-server-12.1.tar.gz, SHA-256320a3a41…f229, verified byte-identical fromdownload.zulip.comand the GitHub release asset) - PostgreSQL 18 with the pgroonga extension for full-text search, on its own dedicated EBS volume
- RabbitMQ, Redis and memcached, all bound to loopback only
- The Django application server, the Tornado real-time push server and the queue workers, under supervisor
- nginx terminating TLS on port 443, with plain HTTP on port 80 redirecting to HTTPS
- A per-instance self-signed TLS certificate, minted on first boot for that instance's own address
- An organization and owner account created on first boot, with a 24-character password generated on the instance and written to a root-only file
- Invitation-only registration, with invitations restricted to administrators
- Two dedicated EBS volumes: the database at
/var/lib/postgresqland the whole application tier (deployments, virtual environment and uploaded files) at/home/zulip - The verbatim upstream Apache-2.0 license text baked into the image at
/usr/local/share/sac-zulip/ - Nothing in this image runs in a container. Every component is a native package managed by systemd and supervisor
Connecting to your instance
Connect over SSH on port 22 using the key pair you selected at launch. The login user depends on the operating system variant you launched:
- AMI variant Zulip on Ubuntu 24.04: operating system Ubuntu 24.04 LTS, SSH login user ubuntu
ssh -i /path/to/your-key.pem ubuntu@<public-ip>Confirm the platform is running
Every unit should report active. zulip-firstboot.service is the one-shot unit that generated this instance's secrets, certificate and organization.
systemctl is-active postgresql.service redis-server.service rabbitmq-server.service memcached.service supervisor.service nginx.service zulip-firstboot.serviceactive active active active active active activeOnly SSH and the two nginx ports are reachable from the network. The database, the message broker and the caches are bound to loopback, so nothing outside the instance can reach them.
sudo ss -tlnH | awk '{split($4,a,":"); p=a[length(a)]; if (p=="22"||p=="80"||p=="443"||p=="5432"||p=="6379"||p=="5672"||p=="11211") print $4}' | sort -u0.0.0.0:22 0.0.0.0:443 0.0.0.0:80 127.0.0.1:11211 127.0.0.1:5432 127.0.0.1:5672 127.0.0.1:6379 [::1]:6379 [::]:22 [::]:443 [::]:80The application processes run under supervisor:
sudo supervisorctl status | awk '{printf "%-52s %s\n", $1, $2}' | head -8go-camo RUNNING process-fts-updates RUNNING smokescreen RUNNING zulip-django RUNNING zulip-katex RUNNING zulip-tornado RUNNING zulip-tus RUNNING zulip-workers:zulip_events_deferred_email_senders RUNNINGZulip's own health endpoint answers over TLS, and the deployment reports the installed version:
curl -sk -o /dev/null -w 'https://127.0.0.1/health -> HTTP %{http_code}\n' https://127.0.0.1/health grep -oE "ZULIP_VERSION = \"[0-9.]+\"" /home/zulip/deployments/current/version.pyhttps://127.0.0.1/health -> HTTP 200 ZULIP_VERSION = "12.1"Retrieve your organization owner credentials
This instance's first boot generated the owner account and its password and wrote them to a root-only file. Read the whole file with sudo cat /root/zulip-credentials.txt. It contains the sign-in URL, the owner email address and the password.
The block below shows the file's shape and permissions without printing the password:
grep -E '^zulip\.(url|admin\.email)=' /root/zulip-credentials.txt awk -F= '/^zulip\.admin\.pass=/{print "zulip.admin.pass (" length($2) " characters, unique to this instance)"}' /root/zulip-credentials.txt stat -c '%n mode %a owner %U' /root/zulip-credentials.txtzulip.url=https://<public-ip>/ zulip.admin.email=admin@<public-ip>.nip.io zulip.admin.pass (24 characters, unique to this instance) /root/zulip-credentials.txt mode 600 owner rootThe owner email address uses the instance's own address, so a working login exists before you configure a domain name or outbound email. You can change it at any time from Personal settings > Account & privacy.
Sign in
Browse to https://<public-ip>/ and accept the browser warning about the self-signed certificate. A later section covers replacing it with a certificate from a trusted authority. Sign in with the owner email address and password from the credentials file.
The line under the sign-in box reads "You need to be invited to join this organization." This image ships invitation-only, so a Zulip server on a public address does not accept anonymous signup.
On your first sign-in, Zulip shows a one-time welcome screen that confirms your email address. Select Continue. Zulip then offers a short video tour, which you can skip.
Channels and topics
The Inbox view is Zulip's home screen. It lists every channel that has unread messages and, under each one, the individual topics within it, so you can see at a glance which conversations moved.
Open a topic to see just that conversation. This is the core difference from a flat chat tool. A release discussion, an incident and an onboarding question can all be live in #general at once without interleaving, and you can resolve, mute, link to or move each one independently.
To create a channel, select CREATE A CHANNEL at the bottom of the left sidebar. To start a conversation, select Start new conversation, name the topic, and send.
Organization administration
Open the gear menu in the top right and choose Organization settings. The Users section lists members and their roles, and lets you invite people, change roles and deactivate accounts. Organization permissions controls who can invite, who can create channels, and the message editing and retention policies.
Your account is the organization Owner. Create at least one more owner before you rely on this server, so that the loss of one account does not lock you out.
Verify every secret is unique to your instance
Zulip's secrets live in /etc/zulip/zulip-secrets.conf. This image ships that file empty and regenerates the whole set on first boot, along with the TLS private key, the RabbitMQ distribution cookie and the memcached authentication database.
The helper below prints a SHA-256 fingerprint of each value, never the value itself. Use it to confirm that the set exists. If you launch a second instance from the same AMI, use it to confirm that no fingerprint matches:
sudo /usr/local/sbin/zulip-secret-fingerprints.sh | awk '{printf "%-40s %s...\n", $1, substr($2,1,16)}'avatar_salt db2e0de26246bfb3... camo_key a740231a88e4ae54... memcached_password e104df7659318487... rabbitmq_password adb135d8d142b1e5... redis_password 734978561ebc43d6... secret_key 44b05b2693683847... shared_secret 20363002338fd7e0... zulip_org_id 8392c4ef54ae5dbd... zulip_org_key 281d48b050486b71... file:/etc/ssl/private/zulip.key 89e405fb27c095f4... file:/var/lib/rabbitmq/.erlang.cookie c2cf7860848ac179... file:/etc/sasl2/memcached-sasldb2 b97478c716f9ea1f...Zulip's authentication backend records sign-in attempts, so you can audit them:
sudo awk '/Authentication attempt/{n++} END{print n+0 " sign-in attempts recorded in /var/log/zulip/auth.log"}' /var/log/zulip/auth.log37 sign-in attempts recorded in /var/log/zulip/auth.logStorage layout
The database and the application tier sit on their own EBS volumes, mounted by filesystem UUID so that the layout reproduces on every instance. You can grow either volume with the EC2 console and resize2fs without rebuilding the server.
findmnt -no TARGET,SOURCE,SIZE,USED /var/lib/postgresql
findmnt -no TARGET,SOURCE,SIZE,USED /home/zulip
findmnt -no TARGET,SOURCE,SIZE,USED //var/lib/postgresql /dev/nvme1n1 39.1G 73.2M /home/zulip /dev/nvme2n1 39.1G 4.8G / /dev/nvme0n1p1 37.7G 5.5G/var/lib/postgresql holds the PostgreSQL cluster, which contains every channel, topic, message and user. /home/zulip holds the versioned deployment tree, the Python virtual environment, the compiled static assets and LOCAL_UPLOADS_DIR, where Zulip stores every uploaded file, image and avatar.
Use your own domain name and a trusted certificate
The instance answers at its own IP address with a self-signed certificate. For production, point a DNS name at the instance and install a certificate signed by a trusted authority. Do this first after launch, because Zulip embeds EXTERNAL_HOST in message links, uploaded file URLs and email footers.
- Create an A record for your chosen name (for example chat.your-company.example) that points at the instance's public IP. Allocate an Elastic IP so that the address does not change.
- Edit /etc/zulip/settings.py and set EXTERNAL_HOST = "chat.your-company.example".
- Obtain a certificate. Zulip ships the vendor's Certbot helper: sudo /home/zulip/deployments/current/scripts/setup/setup-certbot --email you@your-company.example chat.your-company.example (port 80 must be reachable from the internet for the HTTP-01 challenge). To install a certificate you already hold, place the full chain at /etc/ssl/certs/zulip.combined-chain.crt and the private key at /etc/ssl/private/zulip.key.
- Apply the change: sudo -u zulip /home/zulip/deployments/current/scripts/restart-server, then sudo systemctl reload nginx.
- Reload https://chat.your-company.example/ and confirm that the certificate is trusted.
Configure outbound email
Outbound email is not configured on this image, and it does not work until you configure it. Zulip uses email for invitations, password resets and message notifications.
Two things stand in the way on a fresh EC2 instance. AWS blocks outbound TCP port 25 by default, and a new instance has no reverse DNS record or sending reputation, so mail that it sends directly is rejected or filed as spam. Relay through a service that is already trusted instead.
Your owner account does not depend on email. Its password was generated on the instance and written to /root/zulip-credentials.txt, so you can sign in and complete the configuration below first.
- In Amazon SES, verify your sending domain (or a single sender address) and create SMTP credentials. A new SES account is in the sandbox and can send only to verified addresses until you request production access.
- Edit /etc/zulip/settings.py and set EMAIL_HOST = "email-smtp.<region>.amazonaws.com", EMAIL_HOST_USER = "<your SES SMTP username>", EMAIL_PORT = 587, EMAIL_USE_TLS = True and NOREPLY_EMAIL_ADDRESS = "noreply@your-company.example".
- Add the SMTP password as a secret: append smtp_password = <your SES SMTP password> to /etc/zulip/zulip-secrets.conf (root only, mode 640). Do not put the password in settings.py.
- Apply the change and test it: sudo -u zulip /home/zulip/deployments/current/scripts/restart-server, then sudo -u zulip /home/zulip/deployments/current/manage.py send_test_email you@your-company.example.
- Any authenticated SMTP relay works equally well. SES is simply the least effort from EC2.
Until you configure email, add people with Invite links copied from the Users screen rather than emailed invitations. Reset passwords from the command line with sudo -u zulip /home/zulip/deployments/current/manage.py change_password <email>.
Back up your organization
Zulip ships its own backup command, which captures the database, the uploaded files and the configuration in one archive. Write backups to a volume other than the instance, and copy them to S3.
- Full backup: sudo -u zulip /home/zulip/deployments/current/manage.py backup --output /home/zulip/zulip-backup.tar.gz
- Copy to S3: aws s3 cp /home/zulip/zulip-backup.tar.gz s3://your-bucket/zulip/
- Restore: sudo /home/zulip/deployments/current/scripts/setup/restore-backup /path/to/zulip-backup.tar.gz
- Export one organization (portable, for migration): sudo -u zulip /home/zulip/deployments/current/manage.py export -r ''
An EBS snapshot of both data volumes, taken together, is a good complement. It is not a substitute for a Zulip backup archive, which is what the restore tooling consumes.
Upgrade Zulip
Upgrade Zulip in place with the vendor's own upgrade script, which handles the database migrations and restarts the stack. Take a backup first.
- Check the running version: grep ZULIP_VERSION /home/zulip/deployments/current/version.py
- Upgrade to the latest release: sudo /home/zulip/deployments/current/scripts/upgrade-zulip-from-git main, or download a release tarball and run sudo /home/zulip/deployments/current/scripts/upgrade-zulip /path/to/zulip-server-X.Y.tar.gz
- Apply operating system security updates: sudo apt-get update && sudo apt-get -y upgrade
Troubleshooting
- The browser warns about the certificate. This is expected until you install a trusted certificate, because the image ships a per-instance self-signed one. Follow the domain name section above.
- The sign-in page loads but the app does not. Run sudo supervisorctl status. zulip-django and zulip-tornado must both be RUNNING. Restart with sudo -u zulip /home/zulip/deployments/current/scripts/restart-server.
- "Invalid HTTP_HOST header" appears, or links point at the wrong address. EXTERNAL_HOST in /etc/zulip/settings.py does not match the address you are browsing to. Correct it and restart the server.
- Invitations or password-reset emails never arrive. Outbound email is not configured. See the email section above, and test with manage.py send_test_email.
- Messages send but nothing appears in real time. Check zulip-tornado under supervisor and look at /var/log/zulip/tornado.log.
- Uploads fail. Check free space on /home/zulip with df -h /home/zulip, and grow the volume if needed.
- Where are the logs? They are in /var/log/zulip/: server.log for the application, auth.log for sign-in attempts, errors.log for tracebacks, and tornado.log and workers.log for the real-time and queue tiers.
License
Zulip Server is licensed under the Apache License 2.0. The image contains the verbatim license text, the upstream NOTICE, the BSD-3-Clause notice for the vendored confirmation package, and the source provenance of the exact release shipped:
ls /usr/local/share/sac-zulip/ head -2 /usr/local/share/sac-zulip/LICENSELICENSE LICENSE.confirmation-bsd3.txt NOTICE SOURCE.txt Apache LicenseZulip Cloud and Zulip's paid self-hosted plans are the vendor's separate commercial offerings and are not included with this image.
SecureAnyCloud is not affiliated with or endorsed by Zulip or Kandra Labs. This guide uses the Zulip name and logo to identify the open source software shipped here.
Support
Every SecureAnyCloud image is backed by 24/7 technical support by email and live chat. SecureAnyCloud aims to respond within one business day.
- Email: sales@secureanycloud.com
- Support: https://secureanycloud.com/support
Deploy on AWS
Launch Zulip on AWS with 24/7 support from SecureAnyCloud.
View on Marketplace
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com