#!/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 or --puppet-classes 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 or --puppet-classes 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. --postgresql-database-name=zulip Sets the PostgreSQL database name. --postgresql-database-user=zulip Sets the PostgreSQL database user. --postgresql-version=17 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. --puppet-classes Comma-separated list of Puppet classes to install; defaults to 'zulip:::profile::standalone'. Implies --no-init-db. --no-overwrite-settings Preserve existing `/etc/zulip` configuration files. --no-dist-upgrade Skip the initial `apt-get dist-upgrade`. --push-notifications With this option, the Zulip installer registers your server for the Mobile Push Notification Service, and sets up the initial default configuration. You will be immediately prompted to agree to the Terms of Service, and your server will be registered at the end of the installation process. --no-push-notifications Disable push notifications registration (the default if neither flag is provided). --no-submit-usage-statistics If you enable push notifications, by default your server will submit basic metadata (required for billing and for determining free plan eligibility), as well as aggregate usage statistics. You can disable submitting usage statistics by passing this flag. If push notifications are not enabled, data won't be submitted, so this flag is redundant. --agree-to-terms-of-service If you enable push notifications, you can pass this flag to indicate that you have read and agree to the Zulip Terms of Service: . This skips the Terms of Service prompt, allowing for running the installer with --push-notifications in scripts without requiring user input. 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,puppet-classes:,no-overwrite-settings,no-dist-upgrade,push-notifications,no-push-notifications,no-submit-usage-statistics,agree-to-terms-of-service -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 ;; --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 ;; --puppet-classes) PUPPET_CLASSES="$2" NO_INIT_DB=1 shift shift ;; --no-overwrite-settings) NO_OVERWRITE_SETTINGS=1 shift ;; --no-dist-upgrade) NO_DIST_UPGRADE=1 shift ;; --push-notifications) if [ -n "$NO_PUSH_NOTIFICATIONS" ]; then echo "error: --push-notifications and --no-push-notifications are incompatible." >&2 exit 1 fi PUSH_NOTIFICATIONS=1 shift ;; --no-push-notifications) if [ -n "$PUSH_NOTIFICATIONS" ]; then echo "error: --push-notifications and --no-push-notifications are incompatible." >&2 exit 1 fi NO_PUSH_NOTIFICATIONS=1 shift ;; --no-submit-usage-statistics) NO_SUBMIT_USAGE_STATISTICS=1 shift ;; --agree-to-terms-of-service) AGREE_TO_TERMS_OF_SERVICE_FLAG=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}" POSTGRESQL_VERSION="${POSTGRESQL_VERSION:-17}" 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[0-3] | 1[0-3].*) echo "error: PostgreSQL 14 or newer is required." >&2 exit 1 ;; esac if [ -z "$PUSH_NOTIFICATIONS" ] && [ -z "$NO_PUSH_NOTIFICATIONS" ]; then # Unless specified, we default to --no-push-notifications NO_PUSH_NOTIFICATIONS=1 fi # We set these to Python-style True/False string values, for easy # insertion into the settings.py file. if [ -n "$PUSH_NOTIFICATIONS" ]; then SERVICE_MOBILE_PUSH="True" # --push-notifications also enables SUBMIT_USAGE_STATISTICS, unless the user # explicitly opted out by passing --no-submit-usage-statistics. if [ -n "$NO_SUBMIT_USAGE_STATISTICS" ]; then SERVICE_SUBMIT_USAGE_STATISTICS="False" else SERVICE_SUBMIT_USAGE_STATISTICS="True" fi else SERVICE_MOBILE_PUSH="False" # SUBMIT_USAGE_STATISTICS without PUSH_NOTIFICATIONS is an unusual # configuration that we don't need to offer in the installer. SERVICE_SUBMIT_USAGE_STATISTICS="False" fi # ToS acceptance needs to be ensured if --push-notifications is passed. if [ -n "$PUSH_NOTIFICATIONS" ]; then # If the user provided the --agree-to-terms-of-service flag, we can just proceed. if [ -n "$AGREE_TO_TERMS_OF_SERVICE_FLAG" ]; then PUSH_NOTIFICATIONS_SERVICE_TOS_AGREED=1 echo "Push notifications will be enabled, as you agreed to the Zulip Terms of Service" echo "by passing the --agree-to-terms-of-service flag." sleep 2 else # If user asked for push notifications, prompt for ToS acceptance. echo echo "You chose to register your server for the Mobile Push Notifications Service." echo "Doing so will share basic metadata with the service's maintainers, including:" echo echo "* The server's configured hostname: $EXTERNAL_HOST" echo "* The server's configured contact email address: $ZULIP_ADMINISTRATOR" echo "* Basic metadata about each organization hosted by the server; see:" echo " " if [ -z "$NO_SUBMIT_USAGE_STATISTICS" ]; then echo "* The server's usage statistics; see:" echo " " fi echo echo "For details on why a centralized push notification service is necessary, see:" echo " " echo echo "Use of this service is governed by the Zulip Terms of Service:" echo " " echo read -r -p "Do you want to agree to the Zulip Terms of Service and proceed? [Y/n] " tos_prompt echo # Normalize the user’s response to lowercase. case "${tos_prompt,,}" in "" | y | yes) echo "Great! Push notifications will be enabled; continuing with installation..." PUSH_NOTIFICATIONS_SERVICE_TOS_AGREED=1 sleep 2 ;; *) echo "In order to enable push notifications, you must agree to the Terms of Service." echo "If you do not want to enable push notifications, run the command without the --push-notifications flag." exit 1 ;; esac fi fi # 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 if ! "$ZULIP_PATH/scripts/lib/supported-os"; then 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 # Anything under 5GB, we recommended allocating 2GB of swap. Error # out if there's no swap. swap_kb=$(grep SwapTotal /proc/meminfo | awk '{print $2}') if [ "$mem_kb" -lt 5000000 ] && [ "$swap_kb" -eq 0 ]; then set +x echo -e '\033[0;31m' >&2 echo "No swap allocated; when running with < 5GB of RAM, we recommend at least 2GB of swap." >&2 echo "https://www.digitalocean.com/community/tutorials/how-to-add-swap-space-on-ubuntu-22-04" >&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 # 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, # or hosts which talk directly to the database (e.g. application # hosts); but we don't know if this is a database host until we # have the catalog summary. if (! has_class "zulip::postgresql_common" && ! has_class "zulip::postgresql_client") || [ "$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 # Frontend deploys use /home/zulip/deployments; without this, the # install directory is also only readable by root. mkdir -p /home/zulip/deployments deploy_path=$("$ZULIP_PATH"/scripts/lib/zulip_tools.py make_deploy_path) mv "$ZULIP_PATH" "$deploy_path" ln -nsf /home/zulip/deployments/next "$ZULIP_PATH" ln -nsf "$deploy_path" /home/zulip/deployments/next # Create and activate a virtualenv "$deploy_path"/scripts/lib/create-production-venv "$deploy_path" "$deploy_path"/scripts/lib/install-node if [ -z "$NO_OVERWRITE_SETTINGS" ] || ! [ -e "/etc/zulip/settings.py" ]; then cp -a "$deploy_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 # Set SERVICE settings based on what the user provided. if grep -q -E "^#?\s*ZULIP_SERVICE_PUSH_NOTIFICATIONS = " /etc/zulip/settings.py; then sed -i -E "s/^#?\s*ZULIP_SERVICE_PUSH_NOTIFICATIONS = .*/ZULIP_SERVICE_PUSH_NOTIFICATIONS = $SERVICE_MOBILE_PUSH/" /etc/zulip/settings.py else echo "ZULIP_SERVICE_PUSH_NOTIFICATIONS = $SERVICE_MOBILE_PUSH" >>/etc/zulip/settings.py fi if grep -q -E "^#?\s*ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = " /etc/zulip/settings.py; then sed -i -E "s/^#?\s*ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = .*/ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = $SERVICE_SUBMIT_USAGE_STATISTICS/" /etc/zulip/settings.py else echo "ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS = $SERVICE_SUBMIT_USAGE_STATISTICS" >>/etc/zulip/settings.py fi fi ln -nsf /etc/zulip/settings.py "$deploy_path"/zproject/prod_settings.py "$deploy_path"/scripts/setup/generate_secrets.py --production else deploy_path="$ZULIP_PATH" fi "$deploy_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 "$deploy_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 <