Use case · Team chat

Zulip self-hosted on a dedicated server: requirements, install, upkeep

A self-hosted Zulip server needs one machine that Zulip has to itself, running Ubuntu 22.04, 24.04 or 26.04, or Debian 12 or 13, with a minimum of 2 GB RAM, 10 GB of free disk, a DNS name and SMTP credentials. The installer is a release tarball with three flags that matter (--email, --hostname, --certbot); after it come the one-time organization link, outgoing mail, backups with manage.py, upgrades and the push notification registration. This page walks each of them, using the figures in Zulip's production documentation.

See the machines

What the Zulip docs require

The requirements page lists a short set of minimums. Each figure below is the docs' own, with the scope they give it.

ItemFigure in the docsScope
Operating systemUbuntu 22.04, 24.04, 26.04; Debian 12, 13Supported for production; Docker image for other platforms
CPU architecturex86-64 or aarch64All installs
MemoryAt least 2 GB; swap required under 5 GB (2 GB of swap recommended)Fewer than 100 users: 1 CPU and 2 GB with swap is sufficient
Memory and CPU4 GB RAM and 2 CPUs100+ users
Disk10 GB free, roughly a 25 GB disk with the OSA server with dozens of users; SSD recommended
NetworkA hostname in DNS; inbound HTTPS; outbound SMTP, usually 587All installs

For larger organizations the scalability section adds RAM by daily active users: 4 GB from 25, 8 GB from 100, 16 GB for the application plus 16 GB for the database from 400, and 32 GB plus 32 GB from 2,000. The docs call this guidance conservative and suggest a separate database host from hundreds of daily actives, though not as a requirement. Database disk is put at 10 GB per million messages plus 1 GB per thousand users, for installs where most messages have fewer than 100 recipients. CPU is rarely the limit, they say, but they recommend against highly CPU-limited machines for organizations with hundreds of users.

Why a whole machine fits the installer

The installer expects to be the only thing on the system. It installs system packages with apt, nginx, PostgreSQL and Redis among them, and configures them for its own use, and the docs say they cannot support an install that shares the box with other services. A dedicated server is that box: the processors, memory and drives of one physical machine belong to one tenant, and that tenant holds root.

  • Small team: a Moonshot cartridge with NVMe, such as the Xeon E3-1585L v5 (16 to 64 GB) or the 16-core Xeon D-1587 with up to 128 GB, clears the 100-user line with room for swap and history.
  • Hundreds of daily actives: a single- or dual-socket rack server, where memory runs to hundreds of gigabytes and the database has its own disks.
  • Rack servers carry an out-of-band console (HPE iLO, Dell iDRAC or a Supermicro BMC), so a bad firewall rule or a failed OS upgrade is recoverable without a ticket. Most cartridges have power control only.
  • The port is unmetered, and a racked machine is handed over in about 30 minutes once payment is approved.

The same box can be wiped and reinstalled from the control panel, which is useful for a dress rehearsal: install, restore last night's backup, check it, then go live. The Linux dedicated servers page lists the distributions on offer.

Firewall before the installer

Zulip needs inbound 443. Port 80 is optional in the docs, since Zulip only redirects HTTP to HTTPS, but we leave it open because Let's Encrypt's HTTP check arrives there. Port 4369 (epmd, the Erlang service under RabbitMQ) cannot bind only to localhost, and the docs say a firewall should protect it; a default-deny policy does that. IPv6 is added at checkout at no charge, so switch ufw's IPv6 handling on before the first allow rule: ufw only writes IPv6 copies of rules added while IPV6=yes.

sudo sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw --force enable
sudo ufw status verbose

Add 25/tcp only if you turn on Zulip's incoming email integration. Outbound HTTPS must stay open: Zulip uses it for link previews and the push notification service.

Running the installer

  1. Point an A record (and an AAAA record if you took IPv6) for the hostname, such as chat.example.com, at the server. Certbot needs the name to resolve publicly.
  2. On Ubuntu, enable the universe repository. Debian skips this step.
  3. Download the release tarball into a temporary directory and unpack it. The docs publish sha256sums if you want to verify it.
  4. Run the installer as root with your flags. It takes a few minutes and is idempotent: fix the cause of a failure and run it again.
# Ubuntu only (skip on Debian):
sudo add-apt-repository universe
sudo apt update
cd $(mktemp -d)
curl -fLO https://download.zulip.com/server/zulip-server-latest.tar.gz
tar -xf zulip-server-latest.tar.gz
sudo ./zulip-server-*/scripts/setup/install --certbot \
  --email=it-team@example.com --hostname=chat.example.com
FlagWhat it does
--emailA real address for whoever runs the server; users see it as the contact in automated emails and error pages
--hostnameThe domain users type; it becomes EXTERNAL_HOST in /etc/zulip/settings.py
--certbotRequests a Let's Encrypt certificate, then installs a cron job that renews it; requires --hostname and --email
--push-notificationsRegisters for the Mobile Push Notification Service; prompts you to accept its Terms of Service
--self-signed-certFor tests only, or behind a reverse proxy

The one-time organization link

When the installer finishes it prints a secure, one-time-use organization creation link. Opening it walks you through creating the organization and your own account in it. Our advice: open it in the same session, before you announce the hostname, and keep it out of tickets and chat logs. Our reasoning is simple: whoever opens the link first creates the organization and its first account, so that should be you.

If the link is lost, generate a fresh one on the server with su zulip -c '/home/zulip/deployments/current/manage.py generate_realm_creation_link'. The same command creates further organizations, each on its own subdomain.

Outgoing email is not optional

Zulip sends mail to confirm new users' addresses, for message notifications and for password resets, so SMTP has to work before you invite anyone. Fill in the 'Outgoing email (SMTP) settings' section of /etc/zulip/settings.py: EMAIL_HOST, EMAIL_HOST_USER, usually EMAIL_PORT = 587 with EMAIL_USE_TLS = True, and the noreply addresses.

The password does not go in settings.py. Zulip keeps secrets out of that file by policy: add the line email_password = ... to /etc/zulip/zulip-secrets.conf with an editor, never via echo: a password typed into a command lands in the history file and is visible to ps while it runs. Leave the file's restrictive permissions as the installer set them. Then test with su zulip -c '/home/zulip/deployments/current/manage.py send_test_email you@example.com' (two mails should arrive, from the default and the noreply address) and restart with su zulip -c '/home/zulip/deployments/current/scripts/restart-server'. The docs recommend a transactional mail service over an inbox product; the provider must accept mail from every address Zulip sends as.

Backups with manage.py backup

manage.py backup writes one .tar.gz with everything needed to restore the server: the database, /etc/zulip including its secrets, and uploaded files kept on local disk. Without --output the tarball lands in /tmp. It leaves out /var/log/zulip, uploads on the S3 backend and the SSL certificates, which the docs leave out as security-sensitive: a Certbot certificate is simply issued again on the replacement host once the DNS name points at it, and one you supplied yourself is yours to keep a copy of. A restore needs the same base OS, the same Zulip version and the same PostgreSQL version as the backup, then scripts/setup/restore-backup as root.

Because the archive holds the secrets, write it to a directory only the zulip user can read, and log each run to a file. This cron entry is the whole mechanism; copy the archives off the machine yourself, and pruning old ones is yours too.

sudo install -d -m 700 -o zulip -g zulip /var/backups/zulip
echo '30 3 * * * zulip /home/zulip/deployments/current/manage.py backup --output=/var/backups/zulip/zulip-$(date +\%F).tar.gz > /var/backups/zulip/backup-$(date +\%F).log 2>&1' | sudo tee /etc/cron.d/zulip-backup

The docs recommend a disaster recovery test after setting up backups. A reinstall-and-restore on a spare server is that test.

Upgrading Zulip and the OS

Read the upgrade notes for every release between yours and the target, download the new tarball the same way, and run /home/zulip/deployments/current/scripts/upgrade-zulip zulip-server-latest.tar.gz as root. The script runs apt-get upgrade, installs new dependencies, stops Zulip, applies the configuration, runs database migrations and starts the new version; the docs put downtime under 30 seconds unless a release notes an expensive migration. Output goes to /var/log/zulip/upgrade.log. Minor releases roll back by moving the symlinks under /home/zulip/deployments; upgrades do not modify settings.py, so compare it to the new template after a major upgrade.

Operating system upgrades have their own procedure. Going from Ubuntu 22.04 to 24.04, for example, means first bringing Zulip to a release that supports 24.04, taking a backup, and purging rabbitmq-server before do-release-upgrade. Follow the docs' section for your exact pair of releases.

Push notifications, plans and the licence

Apple's and Google's push models do not let a self-hosted server notify the Zulip mobile apps directly, so notifications go through Zulip's Mobile Push Notification Service; with Zulip Server 12.0 or later and a current app they are end-to-end encrypted. Register with --push-notifications at install, or later: set ZULIP_SERVICE_PUSH_NOTIFICATIONS = True in settings.py, restart, and run manage.py register_server as the zulip user. Registered servers send basic metadata, plus usage statistics unless you install with --no-submit-usage-statistics or set ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = False in /etc/zulip/settings.py.

On plans, the docs say one thing: organizations with more than 10 users must move to a paid plan, or the free Community plan if eligible, to use the push service. The server itself is distributed under the Apache 2.0 licence.

Questions

How much memory does a Zulip server need?

The minimum is 2 GB with swap, which the docs call sufficient below 100 users. From 100 users they ask for 4 GB and 2 CPUs. Their scalability guidance goes to 8 GB from 100 daily actives and 16 GB for the application plus 16 GB for the database from 400.

Can Zulip share the server with other applications?

The docs advise against it. The installer pulls in and configures nginx, PostgreSQL and Redis for itself and assumes nothing else is running, and an install on a shared box is outside what they support. Give Zulip its own machine, or use their Docker image.

Do Zulip's mobile apps notify users of a self-hosted server?

Yes, once the server is registered with Zulip's Mobile Push Notification Service. It needs outbound HTTPS. Organizations with more than 10 users need a paid plan, or the free Community plan if they qualify, to use it.

Is self-hosted Zulip free?

The server software is under the Apache 2.0 licence. The plan question applies to the push notification service, which the docs tie to organization size above 10 users.

Can I move an existing chat workspace to Zulip?

The install guide links importers for Slack, Mattermost and Rocket.Chat, among others, and says to stop after installing and follow the import steps instead of creating an organization. If you are still choosing, we also cover Mattermost, Rocket.Chat and a Matrix server.

Give Zulip its own machine

Pick a dedicated server sized for your user count. Root is yours and nothing meters the port.

See the machines

Related on Server Room

Other use cases