Compose: Configuring TLS

Zulip requires that all content be served encrypted using SSL/TLS, over HTTPS. By default, the Zulip Docker container serves unencrypted content on port 80, and expects to be placed behind a proxy; however, there are a number of other deployment options to help get up and running without configuring such a proxy. See Ports exposed by the Docker image for a summary of which ports the container serves which traffic on.

Using a self-signed certificates

Zulip’s Docker deployment supports generating and serving HTTPS traffic with long-lived (10-year) self-signed certificates. These are generally only useful for local development and testing; browsers will require that users click through large warning messages, and Zulip Desktop requires additional configuration to connect. See Zulip’s documentation on using a custom certificate.

  1. Set, in your compose.override.yaml:

    services:
      zulip:
        environment:
          CERTIFICATES: self-signed
    
  2. Restart your Zulip Docker deploy:

    docker compose up zulip
    

Using certbot to acquire Let’s Encrypt certificates

Let’s Encrypt is a free, completely automated CA launched in 2016 to help make HTTPS routine for the entire Web. Zulip offers a simple automation for Certbot, a Let’s Encrypt client, to get SSL certificates from Let’s Encrypt and renew them automatically.

We recommend most Zulip servers use Let’s Encrypt certificates. However, you will need to use something else if your server is not publicly accessible on the Internet, with a publicly-accessible hostname.

Let’s Encrypt Terms of Service

Enabling this configuration will create an account with Let’s Encrypt, using Zulip’s server administrator email address, and will accept their Terms of Service.

  1. Set, in your compose.override.yaml:

    services:
      zulip:
        environment:
          CERTIFICATES: certbot
    
  2. Restart your Zulip Docker deploy:

    docker compose up zulip
    
  3. Let’s Encrypt certificates expire after 90 days. Short expiration periods are good for security, but they also mean that it’s important to automatically renew them to avoid regular maintenance work.

    Zulip configures automatic renewal for you. As a result, a Zulip server configured with Certbot does not require any ongoing work to maintain a current valid SSL certificate.

Manually configuring certificates

It is possible, although not recommended, to obtain and manage TLS certificates for Zulip yourself:

  1. Set, in your compose.override.yaml:

    services:
      zulip:
        environment:
          CERTIFICATES: manual
    
  2. In the zulip Docker volume, place the certificate in certs/manual/zulip.combined-chain.crt, and the key in certs/manual/zulip.key. Because the volume is mounted in the zulip container under /data/, you can copy files into a running Zulip Docker instance via:

    docker compose cp /path/to/zulip.crt zulip:/data/certs/manual/zulip.combined-chain.crt
    docker compose cp /path/to/zulip.key zulip:/data/certs/manual/zulip.key
    

    This copying is only necessary once; since Docker volumes persist their data, the certificate will still be available even if the container is destroyed and recreated.

  3. Restart your Zulip Docker deploy:

    docker compose up zulip
    
  4. You are responsible for keeping the certificates up to date. When you renew the certificate, you must copy the new certificate into the volume, and restart the service:

    docker compose restart zulip
    

Deploying behind a reverse proxy

Before placing Zulip behind a reverse proxy, it needs to be configured to trust the client IP addresses that the proxy reports via the X-Forwarded-For header, and the protocol reported by the X-Forwarded-Proto header. This is important to have accurate IP addresses in server logs, as well as in notification emails which are sent to end users. Zulip doesn’t default to trusting all X-Forwarded-* headers, because doing so would allow clients to spoof any IP address, and claim connections were over a secure connection when they were not; we specify which IP addresses are the Zulip server’s incoming proxies, so we know which X-Forwarded-* headers to trust.

  1. Determine the IP addresses of all reverse proxies you are setting up, as seen from the Zulip container. You can also determine the subnet that the reverse proxies will be on, and specify that (using CIDR notation).

  2. Set the LOADBALANCER_IPS environment variable for the container to the IP address (or CIDR IP address range) of your reverse proxy:

    services:
      zulip:
        environment:
          LOADBALANCER_IPS: 192.168.0.100
    

    Alternately, you can configure Docker Zulip to automatically trust its NAT gateway IP address, using TRUST_GATEWAY_IP:

    services:
      zulip:
        environment:
          TRUST_GATEWAY_IP: True
    
  3. Restart the server for the new settings:

    docker compose up zulip
    
  4. Configure your reverse proxy to send X-Forwarded-For and X-Forwarded-Proto headers. See Zulip’s guide to reverse proxies for details.

  5. If the reverse proxy runs on the same host and uses port 80 or 443 itself, you will need to publish Zulip on different host ports; see Compose: Remapping published ports.

See also