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 条排障实录。
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.1 | Compose v2 内置插件 |
| Docker Compose | v5.5.1 | docker compose 子命令形态 |
| Zulip Server | 12.3-0 | 官方服务定义 ghcr.io/zulip/zulip-server:12.3-0(对应 2026-08-10 发布的 Zulip Server 12.3) |
| 数据库服务 | zulip/zulip-postgresql:14 | Zulip 官方定制的 PostgreSQL 14 |
| 缓存服务 | memcached:alpine | 上游官方版本 |
| 消息队列 | rabbitmq:4.2 | 上游官方版本 |
| Redis | redis:alpine | 上游官方版本 |
| 官方部署仓库 | https://github.com/zulip/docker-zulip | Compose 服务定义与 app: 命令的维护入口 |
💡 版本标识说明:
12.3-0中的-0是官方服务定义的打包修订号(同一 Zulip Server 版本可能因配置修补产生-1、-2等修订)。官方从 Zulip 12.x 起将服务定义发布到 GitHub 版本仓库(ghcr.io/zulip/zulip-server),旧地址 Docker Hubzulip/docker-zulip仅支持到 11.x,选用时务必认准新地址(来源:zulip/docker-zulip官方仓库 README,查询日期 2026-10-21)。
二、部署方案概览
2.1 官方 Compose 服务栈
官方 Compose 定义包含 5 个服务,各自职责与实测版本:
| 服务名 | 职责 | 实测版本 |
|---|---|---|
zulip | Zulip 应用主服务(内含 nginx、Django、Tornado 及全部后台工作进程) | ghcr.io/zulip/zulip-server:12.3-0 |
database | PostgreSQL,唯一持久化数据源 | 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_HOST | 192.168.146.128:8134 | 服务对外访问地址(生成链接、邮件、CSRF 校验都以它为准) |
SETTING_ZULIP_ADMINISTRATOR | admin@zulip.example.com | 管理通知邮箱(未配外发邮件时仅作占位) |
SETTING_FAKE_EMAIL_DOMAIN | zulip.example.com | 未配置真实邮件域时用户展示邮箱的合成域;以 IP 方式访问时必须显式设置(否则注册报错,实录见 8.2 节) |
CERTIFICATES | self-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 节的手动备份。
七、注意事项
- 强制 HTTPS:12.x 起 TLS 是硬性要求(自签证书或外置反代二选一),裸 HTTP 直连返回 500。公网部署推荐前置 Nginx/Caddy 做 TLS 终止并配置
LOADBALANCER_IPS,内网实验用CERTIFICATES=self-signed最省事。 - IP 访问必须配
SETTING_FAKE_EMAIL_DOMAIN:EXTERNAL_HOST是 IP 时,Zulip 无法从主机名合成用户展示邮箱,注册直接 500(实录见 8.2 节)。 - 密码策略较严:Zulip 使用 zxcvbn 密码强度评估,实测
q1w2e3r4被拒,加长加复杂后(q1w2e3r4@Ops2026)通过。批量初始化账号时留意。 .env与 API Key 的保管:.env权限收紧到 600;机器人 API Key 等同身份凭证,泄漏即可发消息,实验与生产都要避免入库入文档。- 一次性链接:组织创建链接、邮箱确认链接均为一次性,注册流程中断后必须重新生成。
- 资源名前缀:
name:声明的项目名会被规范化(点号剔除),批量脚本引用卷名/网络名时用docker compose系列命令动态获取,不要硬编码。 - 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 走了完整的多步表单流,记录三条经验:
- POST 必须带 Referer/Origin:HTTPS 表单提交缺 Referer 时直接 403,补
-e参数解决; - CSRF Token 与 Cookie 绑定:每一步都要从上一步响应提取
csrfmiddlewaretoken并连同 cookie jar(-b/-c维护)一起提交; -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 命令详解》 |