#!/usr/bin/env bash set -e usage() { # A subset of this documentation also appears in docs/production/install.md cat <<'EOF' Usage: install --hostname=zulip.example.com --email=zulip-admin@example.com [options...] install --help Options: --hostname=zulip.example.com The user-accessible domain name for this Zulip server, i.e., what users will type in their web browser. Required, unless --no-init-db is set and --certbot is not. --email=zulip-admin@example.com The email address of the person or team who should get support and error emails from this Zulip server. Required, unless --no-init-db is set and --certbot is not. --certbot Obtains a free SSL certificate for the server using Certbot, https://certbot.eff.org/ Recommended. Conflicts with --self-signed-cert. --self-signed-cert Generate a self-signed SSL certificate for the server. This isn’t suitable for production use, but may be convenient for testing. Conflicts with --certbot. --cacert=/path/to/ca.pem Set the CA which used to establish TLS to all public internet sites during the install process; used when this command is run once in a highly-controlled environment to produce an image which is used elsewhere. Uncommon. --postgresql-database-name=zulip Sets the PostgreSQL database name. --postgresql-database-user=zulip Sets the PostgreSQL database user. --postgresql-version=16 Sets the version of PostgreSQL that will be installed. --postgresql-missing-dictionaries Set postgresql.missing_dictionaries, which alters the initial database. Use with cloud managed databases like RDS. Conflicts with --no-overwrite-settings. --no-init-db Does not do any database initialization; use when you already have a Zulip database. --no-overwrite-settings Preserve existing `/etc/zulip` configuration files. --no-dist-upgrade Skip the initial `apt-get dist-upgrade`. EOF } system_requirements_failure() { set +x echo >&2 cat >&2 cat <&2 For more information, see: https://zulip.readthedocs.io/en/latest/production/requirements.html EOF exit 1 } # Shell option parsing. Over time, we'll want to move some of the # environment variables below into this self-documenting system. args="$(getopt -o '' --long help,hostname:,email:,certbot,self-signed-cert,cacert:,postgresql-database-name:,postgresql-database-user:,postgresql-version:,postgresql-missing-dictionaries,no-init-db,no-overwrite-settings,no-dist-upgrade -n "$0" -- "$@")" eval "set -- $args" while true; do case "$1" in --help) usage exit 0 ;; --hostname) EXTERNAL_HOST="$2" shift shift ;; --email) ZULIP_ADMINISTRATOR="$2" shift shift ;; --certbot) USE_CERTBOT=1 shift ;; --cacert) export CUSTOM_CA_CERTIFICATES="$2" shift shift ;; --self-signed-cert) SELF_SIGNED_CERT=1 shift ;; --postgresql-database-name) POSTGRESQL_DATABASE_NAME="$2" shift shift ;; --postgresql-database-user) POSTGRESQL_DATABASE_USER="$2" shift shift ;; --postgresql-version) POSTGRESQL_VERSION="$2" shift shift ;; --postgresql-missing-dictionaries) POSTGRESQL_MISSING_DICTIONARIES=1 shift ;; --no-init-db) NO_INIT_DB=1 shift ;; --no-overwrite-settings) NO_OVERWRITE_SETTINGS=1 shift ;; --no-dist-upgrade) NO_DIST_UPGRADE=1 shift ;; --) shift break ;; esac done if [ "$#" -gt 0 ]; then usage >&2 exit 1 fi ## Options from environment variables. # # Specify options for apt. read -r -a APT_OPTIONS <<<"${APT_OPTIONS:-}" # Install additional packages. read -r -a ADDITIONAL_PACKAGES <<<"${ADDITIONAL_PACKAGES:-}" # Comma-separated list of Puppet manifests to install. The default is # zulip::profile::standalone for an all-in-one system or # zulip::profile::docker for Docker. Use # e.g. zulip::profile::app_frontend for a Zulip frontend server. PUPPET_CLASSES="${PUPPET_CLASSES:-zulip::profile::standalone}" VIRTUALENV_NEEDED="${VIRTUALENV_NEEDED:-yes}" POSTGRESQL_VERSION="${POSTGRESQL_VERSION:-16}" if [ -n "$SELF_SIGNED_CERT" ] && [ -n "$USE_CERTBOT" ]; then set +x echo "error: --self-signed-cert and --certbot are incompatible" >&2 echo >&2 usage >&2 exit 1 fi if [ -n "$POSTGRESQL_MISSING_DICTIONARIES" ] && [ -n "$NO_OVERWRITE_SETTINGS" ]; then set +x echo "error: --postgresql-missing-dictionaries and --no-overwrite-settings are incompatible" >&2 echo >&2 usage >&2 exit 1 fi if [ -z "$EXTERNAL_HOST" ] || [ -z "$ZULIP_ADMINISTRATOR" ]; then if [ -n "$USE_CERTBOT" ] || [ -z "$NO_INIT_DB" ]; then usage >&2 exit 1 fi fi if [ "$EXTERNAL_HOST" = zulip.example.com ] \ || [ "$ZULIP_ADMINISTRATOR" = zulip-admin@example.com ]; then # These example values are specifically checked for and would fail # later; see check_config in zerver/lib/management.py. echo 'error: The example hostname and email must be replaced with real values.' >&2 echo >&2 usage >&2 exit 1 fi case "$POSTGRESQL_VERSION" in [0-9] | [0-9].* | 1[01] | 1[01].*) echo "error: PostgreSQL 12 or newer is required." >&2 exit 1 ;; esac # Do set -x after option parsing is complete set -x ZULIP_PATH="$(readlink -f "$(dirname "$0")"/../..)" # Force a known locale. Some packages on PyPI fail to install in some locales. export LC_ALL="C.UTF-8" export LANG="C.UTF-8" export LANGUAGE="C.UTF-8" # Force a known path; this fixes problems on Debian where `su` from # non-root may not adjust `$PATH` to root's. export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" # Check for a supported OS release. if [ -f /etc/os-release ]; then os_info="$( . /etc/os-release printf '%s\n' "$ID" "$ID_LIKE" "$VERSION_ID" "$VERSION_CODENAME" )" { read -r os_id read -r os_id_like read -r os_version_id read -r os_version_codename || true } <<<"$os_info" case " $os_id $os_id_like " in *' debian '*) package_system="apt" ;; *' rhel '*) package_system="yum" ;; esac fi case "$os_id $os_version_id" in 'debian 12' | 'ubuntu 22.04' | 'ubuntu 24.04') ;; *) system_requirements_failure <&2 echo "Insufficient RAM. Zulip requires at least 2GB of RAM." >&2 echo >&2 echo -e '\033[0m' >&2 exit 1 fi # Do package update, e.g. do `apt-get update` on Debian if [ "$package_system" = apt ]; then # setup-apt-repo does an `apt-get update` "$ZULIP_PATH"/scripts/lib/setup-apt-repo elif [ "$package_system" = yum ]; then "$ZULIP_PATH"/scripts/lib/setup-yum-repo fi # Check early for missing SSL certificates if [ "$PUPPET_CLASSES" = "zulip::profile::standalone" ] && [ -z "$USE_CERTBOT""$SELF_SIGNED_CERT" ] && { ! [ -e "/etc/ssl/private/zulip.key" ] || ! [ -e "/etc/ssl/certs/zulip.combined-chain.crt" ]; }; then set +x cat <&2 echo "Installing packages failed; is network working and (on Ubuntu) the universe repository enabled?" >&2 echo >&2 echo -e '\033[0m' >&2 exit 1 fi elif [ "$package_system" = yum ]; then if ! yum install -y \ python3 python3-pyyaml puppet git curl jq crudini \ "${ADDITIONAL_PACKAGES[@]}"; then set +x echo -e '\033[0;31m' >&2 echo "Installing packages failed; is network working?" >&2 echo >&2 echo -e '\033[0m' >&2 exit 1 fi fi # We generate a self-signed cert even with certbot, so we can use the # webroot authenticator, which requires nginx be set up with a # certificate. if [ -n "$SELF_SIGNED_CERT" ] || [ -n "$USE_CERTBOT" ]; then "$ZULIP_PATH"/scripts/setup/generate-self-signed-cert \ --exists-ok "${EXTERNAL_HOST:-$(hostname)}" fi # Create and activate a virtualenv if [ "$VIRTUALENV_NEEDED" = "yes" ]; then "$ZULIP_PATH"/scripts/lib/create-production-venv "$ZULIP_PATH" fi # Generate /etc/zulip/zulip.conf . mkdir -p /etc/zulip has_class() { grep -qx "$1" /var/lib/puppet/classes.txt } # puppet apply --noop fails unless the user that it _would_ chown # files to exists; https://tickets.puppetlabs.com/browse/PUP-3907 # # The home directory here should match what's declared in base.pp. id -u zulip &>/dev/null || useradd -m zulip --home-dir /home/zulip if [ -n "$NO_OVERWRITE_SETTINGS" ] && [ -e "/etc/zulip/zulip.conf" ]; then "$ZULIP_PATH"/scripts/zulip-puppet-apply --noop \ --write-catalog-summary \ --classfile=/var/lib/puppet/classes.txt else # Write out more than we need, and remove sections that are not # applicable to the classes that are actually necessary. cat </etc/zulip/zulip.conf [machine] puppet_classes = $PUPPET_CLASSES deploy_type = production [postgresql] version = $POSTGRESQL_VERSION EOF if [ -n "$POSTGRESQL_MISSING_DICTIONARIES" ]; then crudini --set /etc/zulip/zulip.conf postgresql missing_dictionaries true fi "$ZULIP_PATH"/scripts/zulip-puppet-apply --noop \ --write-catalog-summary \ --classfile=/var/lib/puppet/classes.txt # We only need the PostgreSQL version setting on database hosts; but # we don't know if this is a database host until we have the catalog summary. if ! has_class "zulip::postgresql_common" || [ "$package_system" != apt ]; then crudini --del /etc/zulip/zulip.conf postgresql fi if [ -n "$POSTGRESQL_DATABASE_NAME" ]; then crudini --set /etc/zulip/zulip.conf postgresql database_name "$POSTGRESQL_DATABASE_NAME" fi if [ -n "$POSTGRESQL_DATABASE_USER" ]; then crudini --set /etc/zulip/zulip.conf postgresql database_user "$POSTGRESQL_DATABASE_USER" fi fi if has_class "zulip::app_frontend_base"; then "$ZULIP_PATH"/scripts/lib/install-node if [ -z "$NO_OVERWRITE_SETTINGS" ] || ! [ -e "/etc/zulip/settings.py" ]; then cp -a "$ZULIP_PATH"/zproject/prod_settings_template.py /etc/zulip/settings.py if [ -n "$EXTERNAL_HOST" ]; then sed -i "s/^EXTERNAL_HOST =.*/EXTERNAL_HOST = '$EXTERNAL_HOST'/" /etc/zulip/settings.py fi if [ -n "$ZULIP_ADMINISTRATOR" ]; then sed -i "s/^ZULIP_ADMINISTRATOR =.*/ZULIP_ADMINISTRATOR = '$ZULIP_ADMINISTRATOR'/" /etc/zulip/settings.py fi fi ln -nsf /etc/zulip/settings.py "$ZULIP_PATH"/zproject/prod_settings.py "$ZULIP_PATH"/scripts/setup/generate_secrets.py --production fi "$ZULIP_PATH"/scripts/zulip-puppet-apply -f if [ "$package_system" = apt ]; then apt-get -y --with-new-pkgs upgrade elif [ "$package_system" = yum ]; then # No action is required because `yum update` already does upgrade. : fi if [ -n "$USE_CERTBOT" ]; then "$ZULIP_PATH"/scripts/setup/setup-certbot \ "$EXTERNAL_HOST" --email "$ZULIP_ADMINISTRATOR" fi if has_class "zulip::nginx" && ! has_class "zulip::profile::docker"; then # Check nginx was configured properly now that we've installed it. # Most common failure mode is certs not having been installed. if ! nginx -t; then ( set +x cat </dev/null; then set +x cat <