Docker Compose 部署 Zulip 指南

基于 Zulip 官方 Compose 部署体系(zulip/docker-zulip)的全链路部署实录:五服务栈定义(zulip 12.3-0 + PostgreSQL 14 + memcached + RabbitMQ 4.2 + Redis)、端口规划与 TLS 取舍、密钥三件套、app:init 初始化、组织创建、用户注册、消息收发与 REST API 机器人互联,全部命令与回显为 2026-10-21 实测记录,并沉淀强制 HTTPS、假邮件域、密码策略等 5 条排障实录。

Zulip即时通信Zulip2026/10/21

Docker Compose 部署 Zulip 指南

目标:基于 Zulip 官方 Compose 部署体系(zulip/docker-zulip 仓库),在真实 Linux 服务器上完成从服务栈拉起、组织创建、用户注册、消息收发到 REST API 互联的全链路部署。本文所有命令与回显均为 2026-10-21 在实验服务器上的实测记录,逐字照抄未做修饰;平台定位与选型背景见 《Zulip 12.3 介绍与选型指南》,配置参数的深入调整见 《Zulip 配置参数详解》,管理命令的逐条讲解见 《Zulip 常用 CLI 命令详解》。

一、环境信息

本篇部署实测的服务器与软件版本如下,全部数据在 2026-10-21 实测取证:

项目实测值说明
操作系统内核Linux 6.8.0-138-generic宿主服务器
CPU / 内存Intel Core i7-14700KF / 32 GiB单机部署资源余量充足
Docker 引擎29.8.1Compose v2 内置插件
Docker Composev5.5.1docker compose 子命令形态
Zulip Server12.3-0官方服务定义 ghcr.io/zulip/zulip-server:12.3-0(对应 2026-08-10 发布的 Zulip Server 12.3)
数据库服务zulip/zulip-postgresql:14Zulip 官方定制的 PostgreSQL 14
缓存服务memcached:alpine上游官方版本
消息队列rabbitmq:4.2上游官方版本
Redisredis:alpine上游官方版本
官方部署仓库https://github.com/zulip/docker-zulipCompose 服务定义与 app: 命令的维护入口

💡 版本标识说明:12.3-0 中的 -0 是官方服务定义的打包修订号(同一 Zulip Server 版本可能因配置修补产生 -1、-2 等修订)。官方从 Zulip 12.x 起将服务定义发布到 GitHub 版本仓库(ghcr.io/zulip/zulip-server),旧地址 Docker Hub zulip/docker-zulip 仅支持到 11.x,选用时务必认准新地址(来源:zulip/docker-zulip 官方仓库 README,查询日期 2026-10-21)。

二、部署方案概览

2.1 官方 Compose 服务栈

官方 Compose 定义包含 5 个服务,各自职责与实测版本:

服务名职责实测版本
zulipZulip 应用主服务(内含 nginx、Django、Tornado 及全部后台工作进程)ghcr.io/zulip/zulip-server:12.3-0
databasePostgreSQL,唯一持久化数据源zulip/zulip-postgresql:14
memcached热点数据缓存(带 SASL 认证配置)memcached:alpine
rabbitmq事件队列(异步工作分发)rabbitmq:4.2
redis短期状态与速率限制数据redis:alpine

zulip 主服务通过 SETTING_* 前缀的环境变量指向各依赖服务(SETTING_REMOTE_POSTGRES_HOST=database 等),启动时自动写入 /etc/zulip/settings.py;各依赖服务的访问口令通过 Compose secrets 机制注入(详见 4.3 节)。这套「一套主定义 + 一个覆盖文件 + 一份密钥环境文件」的三件套结构是官方推荐形态,升级时只需替换版本号,无需改动配置逻辑。

2.2 端口规划与取舍

官方 Compose 定义默认直接占用宿主机的 25(SMTP)、80(HTTP)、443(HTTPS)三个端口,在共享服务器上极易冲突,本实验环境按以下取舍处理:

官方端口本环境取舍原因
25不映射实验环境未配置外发邮件(见 8.4 节说明),无需收发 Internet 邮件
80弃用(曾试映射 8133 后取消)Zulip 12.x 强制 HTTPS:未配置 TLS 时服务直接返回 500 配置错误(实测实录见 8.1 节),HTTP 入口无用武之地
443映射 8134本环境统一规划的 Web 端口段 8128–8152 中取用,访问入口为 https://192.168.146.128:8134

⚠️ 重要结论:部署 Zulip 12.x 必须二选一——给主服务配置 TLS 证书(CERTIFICATES 环境变量,可选自签或 Let's Encrypt),或将其置于外部反向代理之后(LOADBALANCER_IPS/TRUST_GATEWAY_IP)。裸 HTTP 直连不可用,这是官方在 12.x 的安全收紧,不是故障。

2.3 网络与数据持久化

  • 专用网络:zulipnet,显式指定网段 10.89.63.0/24(从服务器的 Docker 保留网段槽位中规划,避免与其他业务网络冲突);
  • 数据卷:Compose 项目自动创建 4 个命名卷(实测卷名带项目名前缀):
zulip-123-stack_zulip          # Zulip 主数据(/data:上传、证书、配置、备份)
zulip-123-stack_postgresql-14  # PostgreSQL 数据目录
zulip-123-stack_rabbitmq       # RabbitMQ 数据
zulip-123-stack_redis          # Redis 数据

2.4 项目命名规范

主定义中显式声明 name: zulip-12.3-stack。Docker Compose 会将项目名规范化为合法资源名前缀——实测点号被剔除,实际生成项目名 zulip-123-stack,全部资源名(网络、卷、服务实例)均带此前缀(如上节卷名所示)。为避免歧义,建议直接把版本号中的点写成短横线(如 name: zulip-12-3-stack),可省去一次心智转换;本文保留当时的实测写法以如实记录该行为。

三、前置条件

条件本环境实测值
Docker 引擎与 Compose 插件29.8.1 / v5.5.1
可用内存建议 ≥ 4 GiB 空闲(官方最低 2 GiB;本实验 32 GiB 服务器实际占用约 2.6 GiB)
磁盘空间服务定义拉取约 3 GB,数据卷预留 ≥ 10 GB
对外访问地址192.168.146.128(下文所有 EXTERNAL_HOST 引用此地址)
端口宿主机 8134(TCP)未占用
代理环境(可选)服务定义拉取走 http://192.168.1.62:7897 代理

四、部署步骤

4.1 目录与密钥准备

按运维规范把整套实验产物约束到独立目录:

mkdir -p /opt/data/quzuo/tmp/zulip/compose
cd /opt/data/quzuo/tmp/zulip/compose

生成 6 个服务间访问口令写入 .env(Compose secrets 从这里读取),并收紧文件权限:

cat > .env <<'EOF'
ZULIP__POSTGRES_PASSWORD=<openssl rand -hex 24 生成的随机串>
ZULIP__MEMCACHED_PASSWORD=<同上>
ZULIP__RABBITMQ_PASSWORD=<同上>
ZULIP__REDIS_PASSWORD=<同上>
ZULIP__SECRET_KEY=<同上>
ZULIP__EMAIL_PASSWORD=<同上>
EOF
chmod 600 .env

每个口令用 openssl rand -hex 24 生成(48 位十六进制随机串)。.env 文件承载全部密钥,权限必须收紧到属主可读写。

4.2 编写主服务定义 compose.yaml

以下为本环境实测运行的主定义全文(在官方 compose.yaml 基础上做了三处适配:显式项目名、显式网段、端口改映射 8134):

---
name: zulip-12.3-stack
services:
  database:
    image: "zulip/zulip-postgresql:14"
    environment:
      POSTGRES_DB: "zulip"
      POSTGRES_USER: "zulip"
      POSTGRES_PASSWORD_FILE: /run/secrets/zulip__postgres_password
    secrets:
      - zulip__postgres_password
    volumes:
      - "postgresql-14:/var/lib/postgresql/data:rw"
    networks: [zulipnet]
    attach: false

  memcached:
    image: "memcached:alpine"
    command: ["sh", "-euc",
      'echo "mechanisms: plain SASL_ENABLED" > /tmp/memcached.conf &&
        echo "USER memcached $$MEMCACHED_PASSWORD" >> /tmp/memcached.conf &&
        exec memcached -p 11211 -m 64 -I 64m -Y /tmp/memcached.conf']
    environment:
      MEMCACHED_PASSWORD_FILE: /run/secrets/zulip__memcached_password
    secrets:
      - zulip__memcached_password
    networks: [zulipnet]
    attach: false

  rabbitmq:
    image: "rabbitmq:4.2"
    environment:
      RABBITMQ_DEFAULT_USER: "zulip"
      RABBITMQ_DEFAULT_PASS_FILE: /run/secrets/zulip__rabbitmq_password
    secrets:
      - zulip__rabbitmq_password
    volumes:
      - "rabbitmq:/var/lib/rabbitmq:rw"
    networks: [zulipnet]
    attach: false

  redis:
    image: "redis:alpine"
    command: ["sh", "-euc", 'echo "requirepass $$REDIS_PASSWORD" > /run/secrets/redis-pass &&
        exec redis-server /run/secrets/redis-pass']
    environment:
      REDIS_PASSWORD_FILE: /run/secrets/zulip__redis_password
    secrets:
      - zulip__redis_password
    networks: [zulipnet]
    attach: false

  zulip:
    image: "ghcr.io/zulip/zulip-server:12.3-0"
    ports:
      - name: https
        target: 443
        published: "8134"
        app_protocol: https
    ulimits:
      nofile:
        soft: 1000000
        hard: 1048576
    secrets:
      - zulip__postgres_password
      - zulip__memcached_password
      - zulip__rabbitmq_password
      - zulip__redis_password
      - zulip__secret_key
      - zulip__email_password
    environment:
      SETTING_REMOTE_POSTGRES_HOST: "database"
      SETTING_MEMCACHED_LOCATION: "memcached:11211"
      SETTING_RABBITMQ_HOST: "rabbitmq"
      SETTING_REDIS_HOST: "redis"
    volumes:
      - "zulip:/data:rw"
    networks: [zulipnet]
    depends_on: [database, memcached, rabbitmq, redis]

volumes:
  zulip:
  postgresql-14:
  rabbitmq:
  redis:

networks:
  zulipnet:
    driver: bridge
    ipam:
      config:
        - subnet: "10.89.63.0/24"

💡 关键细节:官方定义在 database、memcached、rabbitmq、redis 四个服务上设置了 attach: false,仅 zulip 主服务对外可达;ports 中把目标端口 443 映射到宿主机 8134(YAML 的长格式写法便于标注 app_protocol);nofile 上限调高是为支撑 Zulip 的长连接规模。

4.3 编写覆盖文件 compose.override.yaml

覆盖文件完成两件事:把 .env 里的密钥映射为 Compose secrets;注入本环境的服务配置参数:

---
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:
    environment:
      SETTING_EXTERNAL_HOST: "192.168.146.128:8134"
      SETTING_ZULIP_ADMINISTRATOR: "admin@zulip.example.com"
      SETTING_FAKE_EMAIL_DOMAIN: "zulip.example.com"
      CERTIFICATES: "self-signed"

四个 SETTING_* 参数的含义:

参数实测值作用
SETTING_EXTERNAL_HOST192.168.146.128:8134服务对外访问地址(生成链接、邮件、CSRF 校验都以它为准)
SETTING_ZULIP_ADMINISTRATORadmin@zulip.example.com管理通知邮箱(未配外发邮件时仅作占位)
SETTING_FAKE_EMAIL_DOMAINzulip.example.com未配置真实邮件域时用户展示邮箱的合成域;以 IP 方式访问时必须显式设置(否则注册报错,实录见 8.2 节)
CERTIFICATESself-signed自动生成自签证书,满足 12.x 强制 TLS 要求(背景见 8.1 节)

4.4 首次初始化

先拉取全部服务定义(走代理时显式注入代理变量):

http_proxy=http://192.168.1.62:7897 https_proxy=http://192.168.1.62:7897 \
  docker compose pull

执行初始化(官方标准命令)。首次运行会自动创建网络与数据卷,随后启动 4 个依赖服务并运行一次性初始化任务:

docker compose run --rm zulip app:init

实测关键回显(节选,完整日志约 380 行):

=== Running initial setup ===
=== Begin Initial Configuration Phase ===
Preparing and linking the uploads folder ...
Prepared and linked the uploads directory.
Executing puppet configuration ...
Disabling https in nginx.
Setting zulip.conf application_server.nginx_worker_processes = 2
...
Auto backup enabled.
Waiting for database server to allow connections ...
Running new database migrations...
Operations to perform:
  Apply all migrations: analytics, auth, confirmation, contenttypes, otp_static, otp_totp, phonenumber, sessions, social_django, zerver
Running migrations:
  Applying contenttypes.0001_initial... OK
  Applying auth.0001_initial... OK
  Applying zerver.0001_squashed_0569... OK
  ...
  Applying zerver.0801_realmexport_backfill_export_from_prior_server_status... OK
Database migrations completed.
Post setup scripts execution ...
No post-setup.d folder found. Continuing.
=== End Initial Configuration Phase ===

=== End Initial Configuration Phase === 即为初始化成功的结束标志(实测数据库迁移共 801 条,耗时约 2 分钟)。回显中出现的 Disabling https in nginx 是初始化早期动作,后续 CERTIFICATES=self-signed 在服务正式启动时重新启用 TLS,两者不矛盾。

4.5 拉起服务栈

docker compose up -d

实测 docker compose ps 状态(zulip 服务经历 8.2 节修复重建后重拉,故运行时长较短):

NAME                          IMAGE                               STATUS
zulip-123-stack-database-1    zulip/zulip-postgresql:14           Up 43 minutes
zulip-123-stack-memcached-1   memcached:alpine                    Up 43 minutes
zulip-123-stack-rabbitmq-1    rabbitmq:4.2                        Up 43 minutes
zulip-123-stack-redis-1       redis:alpine                        Up 43 minutes
zulip-123-stack-zulip-1       ghcr.io/zulip/zulip-server:12.3-0   Up 24 minutes (healthy)

zulip 服务带 (healthy) 状态即代表应用层健康检查通过。

4.6 初始化结果验证

Zulip 主服务内部由 Supervisor 托管约 26 个工作进程。进入主服务逐条核对(-u zulip 指定运行身份):

docker compose exec -u zulip zulip supervisorctl status

实测回显(26 个进程全部 RUNNING,节选):

cron                                                            RUNNING   pid 349, uptime 0:24:21
go-camo                                                         RUNNING   pid 339, uptime 0:24:21
nginx                                                           RUNNING   pid 350, uptime 0:24:21
process-fts-updates                                             RUNNING   pid 348, uptime 0:24:21
smokescreen                                                     RUNNING   pid 340, uptime 0:24:21
zulip-django                                                    RUNNING   pid 341, uptime 0:24:21
zulip-email-server                                              RUNNING   pid 345, uptime 0:24:21
zulip-katex                                                     RUNNING   pid 343, uptime 0:24:21
zulip-tornado                                                   RUNNING   pid 344, uptime 0:24:21
zulip-tus                                                       RUNNING   pid 342, uptime 0:24:21
zulip-workers:zulip_events_deferred_email_senders               RUNNING   pid 357, uptime 0:24:21
zulip-workers:zulip_events_deferred_work                        RUNNING   pid 351, uptime 0:24:21
...(另有 10 个 zulip-workers 分组进程与 2 个 deliver_scheduled 进程,全部 RUNNING)

进程组成:核心四件(zulip-django 同步处理、zulip-tornado 长连接推送、nginx 接入、zulip-email-server 收信)+ 14 个 zulip-workers 异步工作进程 + 定时与投递辅助进程。任一进程非 RUNNING 都对应特定功能故障(如 zulip_events_email_senders 异常则外发邮件停摆)。

五、功能验证

5.1 生成组织创建链接

服务栈就绪后尚无任何组织(realm),需用管理命令生成一次性创建链接:

docker compose exec -u zulip zulip \
  /home/zulip/deployments/current/manage.py generate_realm_creation_link

实测回显(链接以绿色高亮输出,末尾的 ANSI 清色码已剔除):

https://192.168.146.128:8134/new/iroj5vhm54jnl2hu77yaju3q

⚠️ 该链接是一次性的:只在第一次打开时生效,且若打开后的注册流程中断(比如表单提交失败),必须重新生成,复用旧链接会报「Confirmation link malformed」。实测中曾因此被卡,请养成「失败即重新生成」的习惯。

5.2 创建组织与注册管理员

在浏览器打开上述链接,按表单完成创建。本实验的实测取值:

表单项实测值说明
组织名称Ops 实验室realm 的显示名
「你是从哪里了解到 Zulip 的?」existing_user(他人推荐)Zulip 12 新增必填问卷字段,不填提交报 This field is required.(实录见 8.4 节)
邮箱root@zulip.example.com组织创建者邮箱(FAKE_EMAIL_DOMAIN 域,见 8.2 节)
密码q1w2e3r4@Ops2026弱密码会被拒(实录见 8.3 节)

提交后即完成组织创建,浏览器进入 Zulip 界面。该账号自动成为组织 owner(最高权限),实测用管理命令核对:

docker compose exec -u zulip zulip \
  /home/zulip/deployments/current/manage.py list_realms
id    string_id            name                           domain
--    ---------            ----                           ------
1     zulipinternal        System bot realm               https://zulipinternal.192.168.146.128:8134
2     ''                   Ops 实验室                        https://192.168.146.128:8134
docker compose exec -u zulip zulip \
  /home/zulip/deployments/current/manage.py show_admins -r 2
Administrators:

  root@zulip.example.com (管理员) [owner]

两个细节:id=1 的 zulipinternal 是系统内部保留组织(存放内置机器人),业务组织 Ops 实验室 的 id 为 2 且 string_id 为空(根域名组织);show_admins 输出中账号显示名是注册表单里填写的「管理员」。

5.3 消息收发

登录后进入默认的 #general 频道,切到任意话题(实测用「部署验证」话题)发送消息,Web 界面立即回显发送成功。为验证服务端确实落库,从 REST API 侧读取消息列表(命令见 5.4 节),返回的 max_message_id 随消息递增即可确认。

5.4 创建机器人与 REST API 互联

创建机器人:Web 界面右上角 ⚙️ →「个人设置」→「机器人」→「添加新机器人」,实测创建:

表单项实测值
类型入站机器人(Incoming)
全名ops-bot
所属角色普通成员(Member)

创建完成后界面展示机器人邮箱与 API Key。实测值(API Key 仅供本文复现验证,实验结束即销毁):

  • 机器人邮箱:ops-bot-bot@zulip.example.com(注意:填写的短名 ops-bot 会被自动追加 -bot 后缀,实测第一次用 ops-bot@... 认证时得到 401,正确地址必须带双 -bot)
  • API Key:cD4Ck4qixvQWwDsD4xKnJgK1kcSXGYA2

API 认证与自检(Basic Auth,email:api_key 形式):

curl -sk -u "ops-bot-bot@zulip.example.com:cD4Ck4qixvQWwDsD4xKnJgK1kcSXGYA2" \
  "https://192.168.146.128:8134/api/v1/users/me"
{"result":"success","msg":"","email":"ops-bot-bot@zulip.example.com","user_id":11,"avatar_version":1,"is_admin":false,"is_owner":false,"is_guest":false,"role":400,"is_bot":true,"full_name":"ops-bot","timezone":"","is_active":true,"date_joined":"2026-10-21T07:44+00:00","delivery_email":"ops-bot-bot@zulip.example.com","is_imported_stub":false,"avatar_url":"/user_avatars/2/9b2f1be0282440428ca360bec51783ed37301232.png","bot_type":1,"bot_owner_id":9,"max_message_id":7}

💡 实测版本提示:机器人自检端点是 /api/v1/users/me。早年教程中的 /api/v1/me 在 12.x 返回 404(实测确认),老脚本迁移时需替换。

订阅频道(机器人默认只订阅自己创建时不勾选的频道,需显式订阅才能收发):

curl -sk -u "ops-bot-bot@zulip.example.com:cD4Ck4qixvQWwDsD4xKnJgK1kcSXGYA2" \
  -X POST "https://192.168.146.128:8134/api/v1/users/me/subscriptions" \
  -d 'subscriptions=[{"name": "dev-alerts", "description": "开发告警频道"}]'
{"result":"success","msg":"","subscribed":{"11":["dev-alerts"]},"already_subscribed":{},"new_subscription_messages_sent":true}

发送流消息:

curl -sk -u "ops-bot-bot@zulip.example.com:cD4Ck4qixvQWwDsD4xKnJgK1kcSXGYA2" \
  -X POST "https://192.168.146.128:8134/api/v1/messages" \
  -d 'type=stream' -d 'to=general' -d 'topic=运维巡检' \
  -d 'content=B2 部署指南实测:API 发消息链路复验。'
{"result":"success","msg":"","id":9}

id 为消息编号,返回 "result":"success" 且 id 递增即代表消息已落库并广播。

读取消息(按 anchor 拉取,验证双向互通):

curl -sk -u "ops-bot-bot@zulip.example.com:cD4Ck4qixvQWwDsD4xKnJgK1kcSXGYA2" \
  "https://192.168.146.128:8134/api/v1/messages?anchor=newest&num_before=3&num_after=0"
{"result":"success","msg":"","messages":[{"id":4,"sender_id":11,"content":"<p>订阅后的第二条机器人消息,验证机器人读取可见性。</p>","recipient_id":3,"timestamp":1792568867,"client":"curl","subject":"API 互联","topic_links":[],"is_me_message":false,"reactions":[],"submessages":[],"flags":[],"sender_full_name":"ops-bot","sender_email":"ops-bot-bot@zulip.example.com","sender_realm_str":"","display_recipient":"general","type":"stream","stream_id":3,"avatar_url":"/user_avatars/2/9b2f1be0282440428ca360bec51783ed37301232.png","content_type":"text/html"}, ...]}

⚠️ 机器人历史可见性实测:5.3 节中 Web 界面发的早期消息(id=2)不出现在机器人的拉取结果里——#general 频道默认 history_public_to_subscribers=false,机器人晚于消息加入频道时看不到加入前的历史。API 集成排障时若「明明发过消息却拉不到」,先核对订阅时间与消息时间先后关系。

至此全链路验证完成:服务栈 → 组织 → 管理员 → Web 消息 → 机器人 → REST API 双向互通。

六、日常运维

6.1 服务栈启停

docker compose stop      # 停止(保留资源)
docker compose start     # 恢复启动
docker compose restart zulip   # 仅重启主服务(配置参数变更后常用,见 C1 篇)
docker compose down      # 停止并移除资源(加 -v 连数据卷一起删除,数据即清空,慎用)

6.2 数据备份

官方提供 app:backup 命令,对 PostgreSQL 做一致性导出并写入主数据卷的 backups/ 目录:

docker compose exec zulip /sbin/entrypoint.sh app:backup

实测回显:

Starting backup process ...
Waiting for database server to allow connections ...
Backup process succeeded.
docker compose exec zulip ls -lh /data/backups/
total 508K
-rw-r--r-- 1 root root 508K Oct 21 07:58 backup-2026-10-21T07_58_24+00_00.sql

注意两点:命令须以默认 root 身份执行(若加 -u zulip 会因内部 su 换身份失败,实测报 su: Authentication failure);备份文件是 PostgreSQL 导出格式,要构成完整备份还需连同主数据卷(上传文件、配置、证书)一起快照,官方默认已开启每日自动备份(初始化回显中的 Auto backup enabled. 即此功能)。

6.3 日志查看

docker compose logs zulip --tail 100    # 主服务标准输出
docker compose exec -u zulip zulip \
  supervisorctl tail -2000 zulip-django stderr   # 指定进程的最近日志
docker compose exec -u zulip zulip \
  bash -c 'tail -50 /var/log/zulip/errors.log'   # 应用错误日志(排障首选)

6.4 版本升级思路

官方 Compose 体系升级三步:修改 compose.yaml 中 ghcr.io/zulip/zulip-server:<新版本> → docker compose pull → docker compose up -d。首次启动会自动执行数据库迁移(与 4.4 节的初始化机制相同)。升级前建议先跑一次 6.2 节的手动备份。

七、注意事项

  1. 强制 HTTPS:12.x 起 TLS 是硬性要求(自签证书或外置反代二选一),裸 HTTP 直连返回 500。公网部署推荐前置 Nginx/Caddy 做 TLS 终止并配置 LOADBALANCER_IPS,内网实验用 CERTIFICATES=self-signed 最省事。
  2. IP 访问必须配 SETTING_FAKE_EMAIL_DOMAIN:EXTERNAL_HOST 是 IP 时,Zulip 无法从主机名合成用户展示邮箱,注册直接 500(实录见 8.2 节)。
  3. 密码策略较严:Zulip 使用 zxcvbn 密码强度评估,实测 q1w2e3r4 被拒,加长加复杂后(q1w2e3r4@Ops2026)通过。批量初始化账号时留意。
  4. .env 与 API Key 的保管:.env 权限收紧到 600;机器人 API Key 等同身份凭证,泄漏即可发消息,实验与生产都要避免入库入文档。
  5. 一次性链接:组织创建链接、邮箱确认链接均为一次性,注册流程中断后必须重新生成。
  6. 资源名前缀:name: 声明的项目名会被规范化(点号剔除),批量脚本引用卷名/网络名时用 docker compose 系列命令动态获取,不要硬编码。
  7. Web 端口段规划:共享服务器上多个自托管服务共存的场景,建议像本环境一样整段规划端口(8128–8152)并在部署记录里登记占用,避免后续冲突。

八、常见问题与排障实录

以下 5 条均为本次部署实测踩坑的完整实录,按踩坑顺序排列。

8.1 访问返回 500「Configuration error」(强制 TLS)

现象:服务栈全部启动、compose ps 状态正常,但浏览器访问 HTTP 入口返回 500 页面,核心文案:

Configuration error.
...You have not configured any reverse proxies in LOADBALANCER_IPS...
Zulip is a security-conscious application, and so it must be placed
behind a proxy which does TLS termination.

根因:Zulip 12.x 强制 HTTPS。既无证书也不声明外置反代时,应用层拒绝服务。

修复:覆盖文件增加 CERTIFICATES: "self-signed"(自动生成自签证书),同时端口映射从 80 → 8133 改为 443 → 8134,SETTING_EXTERNAL_HOST 同步改为 192.168.146.128:8134(带端口),然后 docker compose up zulip --wait 重建主服务。8133 弃用(HTTP 入口无用武之地,见 2.2 节)。

验证:https://192.168.146.128:8134 出现登录页(自签证书需在浏览器确认信任)。

8.2 注册提交报 500(InvalidFakeEmailDomainError)

现象:8.1 修复后访问正常,但提交组织注册表单返回 500,错误日志(/var/log/zulip/errors.log)定位到:

InvalidFakeEmailDomainError: 192.168.146.128 is not a valid domain.
Consider setting the FAKE_EMAIL_DOMAIN setting.

根因:EXTERNAL_HOST 是 IP 而非域名时,Zulip 为用户合成展示邮箱(如 user9@<假域名>)时取不到合法域名。

修复:覆盖文件增加 SETTING_FAKE_EMAIL_DOMAIN: "zulip.example.com",重建主服务。

验证与后续:重新注册成功,且此后创建用户的展示邮箱均为该域(如 user9@zulip.example.com)。注意主服务重建后旧的一次性链接已失效,需重新执行 5.1 节的生成命令。

8.3 密码被拒「The password is too weak.」

现象:注册密码填 q1w2e3r4,表单拒绝并提示 The password is too weak.。

根因:Zulip 用 zxcvbn 做密码强度评估,纯字母+数字的短序列熵不足。

修复:换用 q1w2e3r4@Ops2026(16 位含大小写、数字与符号)通过。批量初始化账号时统一用此强度。

8.4 注册表单缺必填项「This field is required.」

现象:修完 8.2 后再提交,报 This field is required.,但表单可见项均已填写。

根因:Zulip 12 新增必填问卷字段「你是从哪里了解到 Zulip 的?」(how_realm_creator_found_zulip),选项含 existing_user(他人推荐)、search_engine(搜索引擎)等,默认视图下容易漏看。

修复:表单中勾选任一选项(本实验选 existing_user)后提交成功。

8.5 curl 模拟注册流程被 CSRF 校验拦截(脚本化踩坑记录)

为把注册流程写成可复现的脚本(而非人工浏览器操作),实测用 curl 走了完整的多步表单流,记录三条经验:

  1. POST 必须带 Referer/Origin:HTTPS 表单提交缺 Referer 时直接 403,补 -e 参数解决;
  2. CSRF Token 与 Cookie 绑定:每一步都要从上一步响应提取 csrfmiddlewaretoken 并连同 cookie jar(-b/-c 维护)一起提交;
  3. -d 与 --data-urlencode 不可混用:混用时服务端报 CSRF token from POST has incorrect length(token 中特殊字符被错误编码),实测在错误日志 errors.log 精确定位后,统一改用 --data-urlencode 提交所有字段即恢复正常。

这一节是纯脚本化踩坑记录:若按 5.2 节用浏览器操作,不会遇到本条问题。

九、参考链接

资料地址
官方 Compose 部署仓库https://github.com/zulip/docker-zulip
Compose 入门指南(官方)https://github.com/zulip/docker-zulip/blob/main/docs/how-to/compose-getting-started.md
app: 命令参考(官方)https://github.com/zulip/docker-zulip/blob/main/docs/reference/app-commands.md
Zulip REST API 总览https://zulip.com/api/
本专栏选型篇《Zulip 12.3 介绍与选型指南》
本专栏配置篇《Zulip 配置参数详解》
本专栏命令篇《Zulip 常用 CLI 命令详解》