From a63fd1b6f9e1e690ae1af84ced735d2633835312 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 11:46:18 -0500 Subject: [PATCH 01/17] Dockerfile: Add a HEALTHCHECK. The parameters of this match the Helm chart's startupProbe/livenessProbe. --- Dockerfile | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Dockerfile b/Dockerfile index 65cb53ef40..7e4bc037aa 100644 --- a/Dockerfile +++ b/Dockerfile @@ -73,5 +73,8 @@ COPY certbot-deploy-hook /sbin/certbot-deploy-hook VOLUME ["$DATA_DIR"] EXPOSE 25 80 443 +HEALTHCHECK --interval=10s --timeout=5s --retries=3 --start-period=300s \ + CMD curl -isfL --insecure http://localhost/health || exit 1 + ENTRYPOINT ["/sbin/entrypoint.sh"] CMD ["app:run"] From 71a4cc4b337c96a9253c4d156b84c9a963b86724 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 11:49:01 -0500 Subject: [PATCH 02/17] compose: Add YAML document header. --- docker-compose.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose.yml b/docker-compose.yml index a652aad46a..e4c5c55f9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,4 @@ +--- services: database: image: "zulip/zulip-postgresql:14" From 52704fd779834d4652788f05e8e4c705e2e26bfd Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 11:49:25 -0500 Subject: [PATCH 03/17] compose: By default, just attach to zulip service. --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index e4c5c55f9f..313e954365 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,6 +12,7 @@ services: POSTGRES_PASSWORD: "REPLACE_WITH_SECURE_POSTGRES_PASSWORD" volumes: - "postgresql-14:/var/lib/postgresql/data:rw" + attach: false memcached: image: "memcached:alpine" restart: unless-stopped @@ -27,6 +28,7 @@ services: SASL_CONF_PATH: "/home/memcache/memcached.conf" MEMCACHED_SASL_PWDB: "/home/memcache/memcached-sasl-db" MEMCACHED_PASSWORD: "REPLACE_WITH_SECURE_MEMCACHED_PASSWORD" + attach: false rabbitmq: image: "rabbitmq:4.1" restart: unless-stopped @@ -35,6 +37,7 @@ services: RABBITMQ_DEFAULT_PASS: "REPLACE_WITH_SECURE_RABBITMQ_PASSWORD" volumes: - "rabbitmq:/var/lib/rabbitmq:rw" + attach: false redis: image: "redis:alpine" restart: unless-stopped @@ -48,6 +51,7 @@ services: REDIS_PASSWORD: "REPLACE_WITH_SECURE_REDIS_PASSWORD" volumes: - "redis:/data:rw" + attach: false zulip: image: "zulip/docker-zulip:11.4-0" restart: unless-stopped From 645c877f001c545f2499eb5f9dba3cf6dcf23a74 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 11:52:44 -0500 Subject: [PATCH 04/17] compose: Use more verbose ports definition. This allows specifying the app_protocol. This doesn't provide any explicit utility at the moment, however. --- docker-compose.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 313e954365..b96c672bd5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -62,9 +62,18 @@ services: ZULIP_GIT_URL: https://github.com/zulip/zulip.git ZULIP_GIT_REF: "11.4" ports: - - "25:25" - - "80:80" - - "443:443" + - name: smtp + target: 25 + published: 25 + app_protocol: smtp + - name: http + target: 80 + published: 80 + app_protocol: http + - name: https + target: 443 + published: 443 + app_protocol: https environment: ## See https://github.com/zulip/docker-zulip#configuration for ## details on this section and how to discover the many From d75ff9c1603907d030b1c09dd0f9e2ce320dfb19 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 13:28:13 -0500 Subject: [PATCH 05/17] compose: Rename to the modern compose.yaml location. --- docker-compose.yml => compose.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docker-compose.yml => compose.yaml (100%) diff --git a/docker-compose.yml b/compose.yaml similarity index 100% rename from docker-compose.yml rename to compose.yaml From 1e909dd25ca2f9d9dbadd8b17e741a5de5ddd5c7 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Wed, 12 Nov 2025 13:15:27 -0500 Subject: [PATCH 06/17] entrypoint: Support SECRETS_something_FILE set to a path. --- entrypoint.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 9af05eb1c2..c27cc07d44 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -276,6 +276,10 @@ secretsConfiguration() { [[ "$key" == SECRETS_*([0-9A-Z_a-z-]) ]] || continue local SECRET_KEY="${key#SECRETS_}" local SECRET_VAR="${!key}" + if [[ "$SECRET_KEY" == *"_FILE" ]]; then + SECRET_VAR="$(cat "$SECRET_VAR")" + SECRET_KEY="${SECRET_KEY%_FILE}" + fi if [ -z "$SECRET_VAR" ]; then echo "Empty secret for key \"$SECRET_KEY\"." fi @@ -416,7 +420,9 @@ initialConfiguration() { waitingForDatabase() { local TIMEOUT=60 echo "Waiting for database server to allow connections ..." - while ! PGPASSWORD="${SECRETS_postgres_password?}" /usr/bin/pg_isready -h "$DB_HOST" -p "$DB_HOST_PORT" -U "$DB_USER" -t 1 >/dev/null 2>&1; do + local PGPASSWORD + PGPASSWORD="$(crudini --get /etc/zulip/zulip-secrets.conf secrets postgres_password)" + while ! PGPASSWORD="$PGPASSWORD" /usr/bin/pg_isready -h "$DB_HOST" -p "$DB_HOST_PORT" -U "$DB_USER" -t 1 >/dev/null 2>&1; do if ! ((TIMEOUT--)); then echo "Could not connect to database server. Exiting." exit 1 From e3a4b47be62868ca14addbd5d2d54bee2644d5ac Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Wed, 12 Nov 2025 13:15:59 -0500 Subject: [PATCH 07/17] entrypoint: Verify that secrets do not contain newlines. --- entrypoint.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/entrypoint.sh b/entrypoint.sh index c27cc07d44..26869c6ebd 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -282,6 +282,9 @@ secretsConfiguration() { fi if [ -z "$SECRET_VAR" ]; then echo "Empty secret for key \"$SECRET_KEY\"." + elif [[ "$SECRET_VAR" =~ $'\n' ]]; then + echo "ERROR: Secret \"$SECRET_KEY\" contains a newline!" + exit 1 fi crudini --set "$DATA_DIR/zulip-secrets.conf" "secrets" "${SECRET_KEY}" "${SECRET_VAR}" done From 08e9b197289662620e5c5ef16cac362b57d7e5eb Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Wed, 12 Nov 2025 15:19:13 -0500 Subject: [PATCH 08/17] compose: Use Docker secrets for secret management. --- compose.yaml | 73 +++++++++++++++++++++++++++++++++++++-------------- entrypoint.sh | 1 + 2 files changed, 54 insertions(+), 20 deletions(-) diff --git a/compose.yaml b/compose.yaml index b96c672bd5..6f923f60ed 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,15 +1,31 @@ --- +secrets: + zulip__postgres_password: + ## Note that you need to do a manual `ALTER ROLE` query if you + ## change this on a system after booting the postgres container + ## the first time on a host. Instructions are available in README.md. + environment: "ZULIP__POSTGRES_PASSWORD" + zulip__memcached_password: + environment: "ZULIP__MEMCACHED_PASSWORD" + zulip__rabbitmq_password: + environment: "ZULIP__RABBITMQ_PASSWORD" + zulip__redis_password: + environment: "ZULIP__REDIS_PASSWORD" + zulip__secret_key: + environment: "ZULIP__SECRET_KEY" + zulip__email_password: + environment: "ZULIP__EMAIL_PASSWORD" + services: database: image: "zulip/zulip-postgresql:14" restart: unless-stopped + secrets: + - zulip__postgres_password environment: POSTGRES_DB: "zulip" POSTGRES_USER: "zulip" - ## Note that you need to do a manual `ALTER ROLE` query if you - ## change this on a system after booting the postgres container - ## the first time on a host. Instructions are available in README.md. - POSTGRES_PASSWORD: "REPLACE_WITH_SECURE_POSTGRES_PASSWORD" + POSTGRES_PASSWORD_FILE: /run/secrets/zulip__postgres_password volumes: - "postgresql-14:/var/lib/postgresql/data:rw" attach: false @@ -21,20 +37,32 @@ services: - "-euc" - | echo 'mech_list: plain' > "$$SASL_CONF_PATH" - echo "zulip@$$HOSTNAME:$$MEMCACHED_PASSWORD" > "$$MEMCACHED_SASL_PWDB" - echo "zulip@localhost:$$MEMCACHED_PASSWORD" >> "$$MEMCACHED_SASL_PWDB" + echo "zulip@$$HOSTNAME:$$(cat $$MEMCACHED_PASSWORD_FILE)" > "$$MEMCACHED_SASL_PWDB" + echo "zulip@localhost:$$(cat $$MEMCACHED_PASSWORD_FILE)" >> "$$MEMCACHED_SASL_PWDB" exec memcached -S + secrets: + - zulip__memcached_password environment: SASL_CONF_PATH: "/home/memcache/memcached.conf" MEMCACHED_SASL_PWDB: "/home/memcache/memcached-sasl-db" - MEMCACHED_PASSWORD: "REPLACE_WITH_SECURE_MEMCACHED_PASSWORD" + MEMCACHED_PASSWORD_FILE: /run/secrets/zulip__memcached_password attach: false rabbitmq: image: "rabbitmq:4.1" restart: unless-stopped + command: + - "sh" + - "-euc" + - | + export RABBITMQ_DEFAULT_PASS="$$(cat $$RABBITMQ_PASSWORD_FILE)" + echo 'default_user = $$(RABBITMQ_DEFAULT_USER)' >> /etc/rabbitmq/rabbitmq.conf + echo 'default_pass = $$(RABBITMQ_DEFAULT_PASS)' >> /etc/rabbitmq/rabbitmq.conf + exec docker-entrypoint.sh rabbitmq-server + secrets: + - zulip__rabbitmq_password environment: RABBITMQ_DEFAULT_USER: "zulip" - RABBITMQ_DEFAULT_PASS: "REPLACE_WITH_SECURE_RABBITMQ_PASSWORD" + RABBITMQ_PASSWORD_FILE: /run/secrets/zulip__rabbitmq_password volumes: - "rabbitmq:/var/lib/rabbitmq:rw" attach: false @@ -44,11 +72,11 @@ services: command: - "sh" - "-euc" - - | - echo "requirepass '$$REDIS_PASSWORD'" > /etc/redis.conf - exec redis-server /etc/redis.conf + - '/usr/local/bin/docker-entrypoint.sh --requirepass "$$(cat $$REDIS_PASSWORD_FILE)"' + secrets: + - zulip__redis_password environment: - REDIS_PASSWORD: "REPLACE_WITH_SECURE_REDIS_PASSWORD" + REDIS_PASSWORD_FILE: /run/secrets/zulip__redis_password volumes: - "redis:/data:rw" attach: false @@ -74,6 +102,13 @@ services: target: 443 published: 443 app_protocol: https + secrets: + - zulip__postgres_password + - zulip__memcached_password + - zulip__rabbitmq_password + - zulip__redis_password + - zulip__secret_key + - zulip__email_password environment: ## See https://github.com/zulip/docker-zulip#configuration for ## details on this section and how to discover the many @@ -85,14 +120,12 @@ services: SETTING_MEMCACHED_LOCATION: "memcached:11211" SETTING_RABBITMQ_HOST: "rabbitmq" SETTING_REDIS_HOST: "redis" - SECRETS_email_password: "123456789" - ## These should match RABBITMQ_DEFAULT_PASS, POSTGRES_PASSWORD, - ## MEMCACHED_PASSWORD, and REDIS_PASSWORD above. - SECRETS_rabbitmq_password: "REPLACE_WITH_SECURE_RABBITMQ_PASSWORD" - SECRETS_postgres_password: "REPLACE_WITH_SECURE_POSTGRES_PASSWORD" - SECRETS_memcached_password: "REPLACE_WITH_SECURE_MEMCACHED_PASSWORD" - SECRETS_redis_password: "REPLACE_WITH_SECURE_REDIS_PASSWORD" - SECRETS_secret_key: "REPLACE_WITH_SECURE_SECRET_KEY" + SECRETS_postgres_password_FILE: /run/secrets/zulip__postgres_password + SECRETS_memcached_password_FILE: /run/secrets/zulip__memcached_password + SECRETS_rabbitmq_password_FILE: /run/secrets/zulip__rabbitmq_password + SECRETS_redis_password_FILE: /run/secrets/zulip__redis_password + SECRETS_secret_key_FILE: /run/secrets/zulip__secret_key + SECRETS_email_password_FILE: /run/secrets/zulip__email_password SETTING_EXTERNAL_HOST: "localhost.localdomain" SETTING_ZULIP_ADMINISTRATOR: "admin@example.com" SETTING_EMAIL_HOST: "" # e.g. smtp.example.com diff --git a/entrypoint.sh b/entrypoint.sh index 26869c6ebd..8030d31e93 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -286,6 +286,7 @@ secretsConfiguration() { echo "ERROR: Secret \"$SECRET_KEY\" contains a newline!" exit 1 fi + echo "Setting $SECRET_KEY from environment variable $key" crudini --set "$DATA_DIR/zulip-secrets.conf" "secrets" "${SECRET_KEY}" "${SECRET_VAR}" done echo "Zulip secrets configuration succeeded." From 49b83e35022529f626aeeccb1240ea9f1c7ef04a Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Wed, 12 Nov 2025 15:19:46 -0500 Subject: [PATCH 09/17] entrypoint: Map all zulip__ secrets to zulip-secrets.conf. --- compose.yaml | 6 ------ entrypoint.sh | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/compose.yaml b/compose.yaml index 6f923f60ed..31c22d47d9 100644 --- a/compose.yaml +++ b/compose.yaml @@ -120,12 +120,6 @@ services: SETTING_MEMCACHED_LOCATION: "memcached:11211" SETTING_RABBITMQ_HOST: "rabbitmq" SETTING_REDIS_HOST: "redis" - SECRETS_postgres_password_FILE: /run/secrets/zulip__postgres_password - SECRETS_memcached_password_FILE: /run/secrets/zulip__memcached_password - SECRETS_rabbitmq_password_FILE: /run/secrets/zulip__rabbitmq_password - SECRETS_redis_password_FILE: /run/secrets/zulip__redis_password - SECRETS_secret_key_FILE: /run/secrets/zulip__secret_key - SECRETS_email_password_FILE: /run/secrets/zulip__email_password SETTING_EXTERNAL_HOST: "localhost.localdomain" SETTING_ZULIP_ADMINISTRATOR: "admin@example.com" SETTING_EMAIL_HOST: "" # e.g. smtp.example.com diff --git a/entrypoint.sh b/entrypoint.sh index 8030d31e93..90ba7d4533 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -289,6 +289,24 @@ secretsConfiguration() { echo "Setting $SECRET_KEY from environment variable $key" crudini --set "$DATA_DIR/zulip-secrets.conf" "secrets" "${SECRET_KEY}" "${SECRET_VAR}" done + # Secrets detected in /run/secrets/ override those via env vars + shopt -s nullglob + local secrets_path + for secrets_path in /run/secrets/zulip__*; do + local secrets_filename + secrets_filename="$(basename "$secrets_path")" + local SECRET_KEY="${secrets_filename#zulip__}" + local SECRET_VAR + SECRET_VAR="$(cat "$secrets_path")" + if [ -z "$SECRET_VAR" ]; then + echo "Empty secret for key \"$SECRET_KEY\"." + elif [[ "$SECRET_VAR" =~ $'\n' ]]; then + echo "ERROR: Secret \"$SECRET_KEY\" contains a newline!" + exit 1 + fi + echo "Setting $SECRET_KEY from secret in $secrets_path" + crudini --set "$DATA_DIR/zulip-secrets.conf" "secrets" "${SECRET_KEY}" "${SECRET_VAR}" + done echo "Zulip secrets configuration succeeded." } databaseConfiguration() { From 70683867517c9cdb19c1e17b2a730585ec2fa870 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 13:58:44 -0500 Subject: [PATCH 10/17] entrypoint: Only match on types we generate. --- entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 90ba7d4533..99c9ebb224 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -137,7 +137,7 @@ setConfigurationValue() { literal) VALUE="$1" ;; - bool | boolean | int | integer | array) + bool | integer | array) VALUE="$KEY = $2" ;; string | *) From 1f91cbd907b1ab8c0fc7f0ea03dd034f27bc173c Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 13:59:59 -0500 Subject: [PATCH 11/17] entrypoint: Warn on unknown type when adding setting. --- entrypoint.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/entrypoint.sh b/entrypoint.sh index 99c9ebb224..dfb16e24b6 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -140,7 +140,11 @@ setConfigurationValue() { bool | integer | array) VALUE="$KEY = $2" ;; - string | *) + string) + VALUE="$KEY = '${2//\'/\'}'" + ;; + *) + echo "WARNING: Unknown type '$TYPE' for '$KEY' -- treating as string." >&2 VALUE="$KEY = '${2//\'/\'}'" ;; esac From ca4c8063fda62e71291604349b950e8071d40f0a Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 14:00:42 -0500 Subject: [PATCH 12/17] entrypoint: Canonicalize boolean values when writing settings. --- entrypoint.sh | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index dfb16e24b6..50f28be150 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -9,11 +9,12 @@ set -u shopt -s extglob normalize_bool() { - # Returns either "True" or "False" + # Returns either "True" or "False", or possibly "None" if a third argument is given local varname="$1" local raw_value="${!varname:-}" local value="${raw_value,,}" # Convert to lowercase local default="${2:-False}" + local allow_none="${3:-}" case "$value" in true | enable | enabled | yes | y | 1 | on) @@ -26,8 +27,12 @@ normalize_bool() { echo "$default" ;; *) - echo "WARNING: Invalid boolean ('$raw_value') for '$varname'; defaulting to $default" >&2 - echo "$default" + if [ -n "$allow_none" ] && [ "$value" = "none" ]; then + echo "None" + else + echo "WARNING: Invalid boolean ('$raw_value') for '$varname'; defaulting to $default" >&2 + echo "$default" + fi ;; esac } @@ -137,7 +142,16 @@ setConfigurationValue() { literal) VALUE="$1" ;; - bool | integer | array) + bool) + # Note that if any settings were explicitly set as type + # "bool" (which none are at current), this would provide a + # slightly confusing error message with "PROVIDED_SETTING" + # in it, rather than the actual setting name. + # shellcheck disable=SC2034 + local PROVIDED_SETTING="$2" + VALUE="$KEY = $(normalize_bool PROVIDED_SETTING False allow_none)" + ;; + integer | array) VALUE="$KEY = $2" ;; string) From f7e4a22f2dd06f2c4c090f53762c14bcc8ecb9a9 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 14:37:26 -0500 Subject: [PATCH 13/17] compose: Pull local settings into compose.override.yaml. --- compose.override.yaml | 89 +++++++++++++++++++++++++++++++++++++++++++ compose.yaml | 63 +----------------------------- 2 files changed, 90 insertions(+), 62 deletions(-) create mode 100644 compose.override.yaml diff --git a/compose.override.yaml b/compose.override.yaml new file mode 100644 index 0000000000..b9c2abbdda --- /dev/null +++ b/compose.override.yaml @@ -0,0 +1,89 @@ +--- +secrets: + ## You can either set these secrets in a file named .env in this directory, or + ## change these from "environment:" to "file:" with a path to store them on + ## disk. Be sure that files, if you use them, do not contain trailing + ## newlines! + ## + ## You may also add additional secrets here, prefixed with zulip__, (and also + ## in the "secrets:" block inside the "zulip" service, below) in order to + ## propagate them into Zulip's /etc/zulip/zulip-secrets.conf + ## + ## --> https://docs.docker.com/reference/compose-file/secrets/ + ## https://docs.docker.com/compose/how-tos/use-secrets/ + zulip__postgres_password: + ## Note that you need to do a manual `ALTER ROLE` query if you + ## change this on a system after booting the postgres container + ## the first time on a host. Instructions are available in README.md. + environment: "ZULIP__POSTGRES_PASSWORD" + zulip__memcached_password: + environment: "ZULIP__MEMCACHED_PASSWORD" + zulip__rabbitmq_password: + environment: "ZULIP__RABBITMQ_PASSWORD" + zulip__redis_password: + environment: "ZULIP__REDIS_PASSWORD" + zulip__secret_key: + environment: "ZULIP__SECRET_KEY" + zulip__email_password: + environment: "ZULIP__EMAIL_PASSWORD" + +services: + zulip: + build: + args: + ## If you want to build zulip from a different repo/branch, you can + ## change these and run: + ## + ## docker compose build zulip + ZULIP_GIT_URL: https://github.com/zulip/zulip.git + ZULIP_GIT_REF: "11.4" + environment: + ## See https://github.com/zulip/docker-zulip#configuration for + ## details on this section and how to discover the many + ## additional settings that are supported here. + DB_HOST: "database" + DB_HOST_PORT: "5432" + DB_USER: "zulip" + SSL_CERTIFICATE_GENERATION: "self-signed" + SETTING_MEMCACHED_LOCATION: "memcached:11211" + SETTING_RABBITMQ_HOST: "rabbitmq" + SETTING_REDIS_HOST: "redis" + SETTING_EXTERNAL_HOST: "localhost.localdomain" + SETTING_ZULIP_ADMINISTRATOR: "admin@example.com" + SETTING_EMAIL_HOST: "" # e.g. smtp.example.com + SETTING_EMAIL_HOST_USER: "noreply@example.com" + SETTING_EMAIL_PORT: "587" + ## It seems that the email server needs to use ssl or tls and can't be used without it + SETTING_EMAIL_USE_SSL: "False" + SETTING_EMAIL_USE_TLS: "True" + ## Uncomment to enable the incoming email gateway. You will need to + ## ensure that email to emaildomain.example.com is routed to this host + ## (e.g. via MX record) + # SETTING_EMAIL_GATEWAY_PATTERN: "%s@emaildomain.example.com" + ZULIP_AUTH_BACKENDS: "EmailAuthBackend" + ## Uncomment this when configuring the mobile push notifications service + # SETTING_ZULIP_SERVICE_PUSH_NOTIFICATIONS: "True" + # SETTING_ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS: "True" + + ## If you're using a reverse proxy, you'll want to provide the + ## comma-separated set of IP addresses (or CIDR ranges) to trust here. + # LOADBALANCER_IPS: "" + + ## By default, files uploaded by users and profile pictures are + ## stored directly on the Zulip server. You can configure files + ## to be stored in Amazon S3 or a compatible data store + ## here. See docs at: + ## + ## https://zulip.readthedocs.io/en/latest/production/upload-backends.html + ## + ## If you want to use the S3 backend, you must set + ## SETTING_LOCAL_UPLOADS_DIR to None as well as configuring the + ## other fields. + # SETTING_LOCAL_UPLOADS_DIR: "None" + # SETTING_S3_AUTH_UPLOADS_BUCKET: "" + # SETTING_S3_AVATAR_BUCKET: "" + # SETTING_S3_ENDPOINT_URL: "None" + # SETTING_S3_REGION: "None" + secrets: + ## Add any additional zulip__ secrets that you defined above. + [] diff --git a/compose.yaml b/compose.yaml index 31c22d47d9..d5be196377 100644 --- a/compose.yaml +++ b/compose.yaml @@ -1,21 +1,4 @@ --- -secrets: - zulip__postgres_password: - ## Note that you need to do a manual `ALTER ROLE` query if you - ## change this on a system after booting the postgres container - ## the first time on a host. Instructions are available in README.md. - environment: "ZULIP__POSTGRES_PASSWORD" - zulip__memcached_password: - environment: "ZULIP__MEMCACHED_PASSWORD" - zulip__rabbitmq_password: - environment: "ZULIP__RABBITMQ_PASSWORD" - zulip__redis_password: - environment: "ZULIP__REDIS_PASSWORD" - zulip__secret_key: - environment: "ZULIP__SECRET_KEY" - zulip__email_password: - environment: "ZULIP__EMAIL_PASSWORD" - services: database: image: "zulip/zulip-postgresql:14" @@ -85,10 +68,6 @@ services: restart: unless-stopped build: context: . - args: - ## Change these if you want to build zulip from a different repo/branch - ZULIP_GIT_URL: https://github.com/zulip/zulip.git - ZULIP_GIT_REF: "11.4" ports: - name: smtp target: 25 @@ -110,52 +89,12 @@ services: - zulip__secret_key - zulip__email_password environment: - ## See https://github.com/zulip/docker-zulip#configuration for - ## details on this section and how to discover the many - ## additional settings that are supported here. + # Default hostnames; configure your application in compose.override.yaml DB_HOST: "database" DB_HOST_PORT: "5432" - DB_USER: "zulip" - SSL_CERTIFICATE_GENERATION: "self-signed" SETTING_MEMCACHED_LOCATION: "memcached:11211" SETTING_RABBITMQ_HOST: "rabbitmq" SETTING_REDIS_HOST: "redis" - SETTING_EXTERNAL_HOST: "localhost.localdomain" - SETTING_ZULIP_ADMINISTRATOR: "admin@example.com" - SETTING_EMAIL_HOST: "" # e.g. smtp.example.com - SETTING_EMAIL_HOST_USER: "noreply@example.com" - SETTING_EMAIL_PORT: "587" - ## It seems that the email server needs to use ssl or tls and can't be used without it - SETTING_EMAIL_USE_SSL: "False" - SETTING_EMAIL_USE_TLS: "True" - ## Uncomment to enable the incoming email gateway. You will need to - ## ensure that email to emaildomain.example.com is routed to this host - ## (e.g. via MX record) - # SETTING_EMAIL_GATEWAY_PATTERN: "%s@emaildomain.example.com" - ZULIP_AUTH_BACKENDS: "EmailAuthBackend" - ## Uncomment this when configuring the mobile push notifications service - # SETTING_ZULIP_SERVICE_PUSH_NOTIFICATIONS: "True" - # SETTING_ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS: "True" - - ## If you're using a reverse proxy, you'll want to provide the - ## comma-separated set of IP addresses (or CIDR ranges) to trust here. - # LOADBALANCER_IPS: "" - - ## By default, files uploaded by users and profile pictures are - ## stored directly on the Zulip server. You can configure files - ## to be stored in Amazon S3 or a compatible data store - ## here. See docs at: - ## - ## https://zulip.readthedocs.io/en/latest/production/upload-backends.html - ## - ## If you want to use the S3 backend, you must set - ## SETTING_LOCAL_UPLOADS_DIR to None as well as configuring the - ## other fields. - # SETTING_LOCAL_UPLOADS_DIR: "None" - # SETTING_S3_AUTH_UPLOADS_BUCKET: "" - # SETTING_S3_AVATAR_BUCKET: "" - # SETTING_S3_ENDPOINT_URL: "None" - # SETTING_S3_REGION: "None" volumes: - "zulip:/data:rw" ulimits: From 575f95dc83dc8dbe21d8385668a09be261520227 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 15:16:29 -0500 Subject: [PATCH 14/17] gitignore: Ignore any .env local secrets. --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index dca4d1c90c..7eabd1dc8d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ .idea/ *.tmproj +# Secrets +/.env + # dev files docker-compose-dev.yml kubernetes/*-dev.yml From 629885b7840b3196e88bcfa3238f3aeccc35d816 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 14:45:53 -0500 Subject: [PATCH 15/17] compose.override: Improve inline documentation. --- compose.override.yaml | 85 +++++++++++++++++++++++++++++++------------ 1 file changed, 62 insertions(+), 23 deletions(-) diff --git a/compose.override.yaml b/compose.override.yaml index b9c2abbdda..bdc16bf547 100644 --- a/compose.override.yaml +++ b/compose.override.yaml @@ -41,49 +41,88 @@ services: ## See https://github.com/zulip/docker-zulip#configuration for ## details on this section and how to discover the many ## additional settings that are supported here. - DB_HOST: "database" - DB_HOST_PORT: "5432" - DB_USER: "zulip" - SSL_CERTIFICATE_GENERATION: "self-signed" - SETTING_MEMCACHED_LOCATION: "memcached:11211" - SETTING_RABBITMQ_HOST: "rabbitmq" - SETTING_REDIS_HOST: "redis" + ## + ## The following two settings are required: SETTING_EXTERNAL_HOST: "localhost.localdomain" SETTING_ZULIP_ADMINISTRATOR: "admin@example.com" - SETTING_EMAIL_HOST: "" # e.g. smtp.example.com - SETTING_EMAIL_HOST_USER: "noreply@example.com" - SETTING_EMAIL_PORT: "587" - ## It seems that the email server needs to use ssl or tls and can't be used without it - SETTING_EMAIL_USE_SSL: "False" - SETTING_EMAIL_USE_TLS: "True" + + ## Most deploys do SSL termination outside of the container; Zulip + ## automatically generates a self-signed certificate to use on port443. + ## Uncomment this to set up an auto-renewed Lets Encrypt certificate + ## inside the container -- this requires that SETTING_EXTERNAL_HOST be + ## accessible from the public network. + ## + # SSL_CERTIFICATE_GENERATION: "certbot" + + ## By default, port 80 redirects to port 443, as is suitable for exposing + ## publicly. To handle traffic directly on port 80 (if doing SSL + ## termination in an outer reverse proxy), uncomment this. + ## + # DISABLE_HTTPS: True + + ## If you're using a reverse proxy, you will also need to provide the + ## comma-separated set of IP addresses (or CIDR ranges) to trust here. + ## + ## --> https://zulip.readthedocs.io/en/stable/production/reverse-proxies.html + ## + # LOADBALANCER_IPS: "10.0.0.0/8" + + ## Outgoing email settings + ## + ## --> https://zulip.readthedocs.io/en/stable/production/email.html + ## + # SETTING_EMAIL_HOST: "smtp.example.com" + # SETTING_EMAIL_HOST_USER: "noreply@example.com" + # SETTING_EMAIL_PORT: "587" + # SETTING_EMAIL_USE_SSL: False + # SETTING_EMAIL_USE_TLS: True + ## Uncomment to enable the incoming email gateway. You will need to ## ensure that email to emaildomain.example.com is routed to this host ## (e.g. via MX record) + ## + ## --> https://zulip.readthedocs.io/en/stable/production/email-gateway.html + ## # SETTING_EMAIL_GATEWAY_PATTERN: "%s@emaildomain.example.com" - ZULIP_AUTH_BACKENDS: "EmailAuthBackend" - ## Uncomment this when configuring the mobile push notifications service - # SETTING_ZULIP_SERVICE_PUSH_NOTIFICATIONS: "True" - # SETTING_ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS: "True" - ## If you're using a reverse proxy, you'll want to provide the - ## comma-separated set of IP addresses (or CIDR ranges) to trust here. - # LOADBALANCER_IPS: "" + ## A comma-separated list of authentication backends to enable. Note that + ## this ZULIP_AUTH_BACKENDS takes the place of + ## SETTINGS_AUTHENTICATION_BACKENDS. This defaults to just + ## EmailAuthBackend. + ## + ## --> https://zulip.readthedocs.io/en/stable/production/authentication-methods.html + ## + # ZULIP_AUTH_BACKENDS: "EmailAuthBackend,GoogleAuthBackend" + + ## Uncomment this when configuring the mobile push notifications service. + ## After setting these, you will need to register the server: + ## + ## docker compose exec -it -u zulip zulip /home/zulip/deployments/current/manage.py register_server + ## + ## --> https://zulip.readthedocs.io/en/stable/production/mobile-push-notifications.html + ## + # SETTING_ZULIP_SERVICE_PUSH_NOTIFICATIONS: True + # SETTING_ZULIP_SERVICE_SUBMIT_USAGE_STATISTICS: True ## By default, files uploaded by users and profile pictures are ## stored directly on the Zulip server. You can configure files ## to be stored in Amazon S3 or a compatible data store - ## here. See docs at: - ## - ## https://zulip.readthedocs.io/en/latest/production/upload-backends.html + ## here. ## ## If you want to use the S3 backend, you must set ## SETTING_LOCAL_UPLOADS_DIR to None as well as configuring the ## other fields. + ## + ## --> https://zulip.readthedocs.io/en/latest/production/upload-backends.html + ## # SETTING_LOCAL_UPLOADS_DIR: "None" # SETTING_S3_AUTH_UPLOADS_BUCKET: "" # SETTING_S3_AVATAR_BUCKET: "" # SETTING_S3_ENDPOINT_URL: "None" # SETTING_S3_REGION: "None" + + ## For a complete list of possible settings, see: + ## --> https://github.com/zulip/zulip/blob/11.4/zproject/prod_settings_template.py secrets: ## Add any additional zulip__ secrets that you defined above. [] From a69de691f9c6ae47470fc5cbc8a6c47f1b9bf037 Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Thu, 13 Nov 2025 15:15:47 -0500 Subject: [PATCH 16/17] github: Add a docker-compose test. --- .github/workflows/dockerfile.yaml | 58 +++++++++++++++++++++++++++++++ ci/compose.override.yaml | 21 +++++++++++ ci/env | 6 ++++ 3 files changed, 85 insertions(+) create mode 100644 ci/compose.override.yaml create mode 100644 ci/env diff --git a/.github/workflows/dockerfile.yaml b/.github/workflows/dockerfile.yaml index 9669ef5bd6..8f694c3882 100644 --- a/.github/workflows/dockerfile.yaml +++ b/.github/workflows/dockerfile.yaml @@ -154,3 +154,61 @@ jobs: pod=$(kubectl get pods -n "$namespace" -l app.kubernetes.io/name=zulip --output name) kubectl -n "$namespace" logs "$pod" kubectl -n "$namespace" exec "$pod" -c zulip -- cat /var/log/zulip/errors.log + + docker-compose-test: + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: + - build + env: + GITHUB_CI_IMAGE: ghcr.io/${{ github.repository }}:pr-${{ github.event.pull_request.number }} + steps: + - name: Checkout code + uses: actions/checkout@v5 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Verify Docker Compose config validation + run: | + docker compose \ + -f compose.yaml \ + -f ci/compose.override.yaml \ + --env-file ci/env \ + config + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Start Docker Compose services + run: | + docker compose \ + -f compose.yaml \ + -f ci/compose.override.yaml \ + --env-file ci/env \ + up -d --no-build + + - name: Wait for services to be healthy + run: | + echo "Waiting for zulip service to be healthy..." + timeout 300 bash -c \ + 'until docker inspect --format "{{.State.Health.Status}}" $(docker compose ps -q zulip) | grep -q healthy; do sleep 5; done' + + - name: Verify all services are running + run: | + docker compose ps + # Check that no services are in a failed state + if docker compose ps | grep -E "(Exit|Restarting)"; then + exit 1 + fi + + - name: Check service logs for critical errors + if: success() || failure() + continue-on-error: true + run: | + docker compose ps + docker compose logs zulip diff --git a/ci/compose.override.yaml b/ci/compose.override.yaml new file mode 100644 index 0000000000..90aff181d0 --- /dev/null +++ b/ci/compose.override.yaml @@ -0,0 +1,21 @@ +--- +secrets: + zulip__postgres_password: + environment: "ZULIP__POSTGRES_PASSWORD" + zulip__memcached_password: + environment: "ZULIP__MEMCACHED_PASSWORD" + zulip__rabbitmq_password: + environment: "ZULIP__RABBITMQ_PASSWORD" + zulip__redis_password: + environment: "ZULIP__REDIS_PASSWORD" + zulip__secret_key: + environment: "ZULIP__SECRET_KEY" + zulip__email_password: + environment: "ZULIP__EMAIL_PASSWORD" + +services: + zulip: + image: "${GITHUB_CI_IMAGE:?error}" + environment: + SETTING_EXTERNAL_HOST: "zulip.example.net" + SETTING_ZULIP_ADMINISTRATOR: "admin@example.net" diff --git a/ci/env b/ci/env new file mode 100644 index 0000000000..32d0df73e4 --- /dev/null +++ b/ci/env @@ -0,0 +1,6 @@ +ZULIP__POSTGRES_PASSWORD=postgres_password +ZULIP__MEMCACHED_PASSWORD=memcached_password +ZULIP__RABBITMQ_PASSWORD=rabbitmq_password +ZULIP__REDIS_PASSWORD=redis_password +ZULIP__SECRET_KEY=django_secret_key +ZULIP__EMAIL_PASSWORD=outgoing_email_password From 00c74a3564561e4a855adc786041a336da2b6dab Mon Sep 17 00:00:00 2001 From: Alex Vandiver Date: Fri, 14 Nov 2025 12:22:52 -0500 Subject: [PATCH 17/17] entrypoint: Switch to manage.py check, from checkconfig. The latter is Zulip-specific, and being removed in `main`. --- entrypoint.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 50f28be150..8207f3eeae 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -397,7 +397,7 @@ zulipConfiguration() { fi setConfigurationValue "$setting_key" "$setting_var" "$type" done - if ! su zulip -c "/home/zulip/deployments/current/manage.py checkconfig"; then + if ! su zulip -c "/home/zulip/deployments/current/manage.py check"; then echo "Error in the Zulip configuration. Exiting." exit 1 fi @@ -448,7 +448,7 @@ initialConfiguration() { ls -l /etc/zulip/ exit 1 fi - if ! su zulip -c "/home/zulip/deployments/current/manage.py checkconfig"; then + if ! su zulip -c "/home/zulip/deployments/current/manage.py check"; then echo "Error in the Zulip configuration. Exiting." exit 1 fi