Jitsi Meet 的 Docker 升级,有时并不是简单修改镜像标签。
当新版本从传统 rootful 容器转向:
rootless
read-only root filesystem
tmpfs runtime
persistent storage
部署模型本身就发生了改变。
如果仍然拿旧 Compose 文件直接套新镜像,很容易遇到:
- Web 服务启动失败;
- TLS 私钥无法读取;
- Prosody 无法建立 TLS context;
- Jicofo 与 JVB 无法登录 XMPP;
- 公网反向代理返回 502;
- 容器明明处于 running,却没有任何实际功能。
下面整理一次完整迁移过程中最值得记录的技术问题。
一、为什么不能只替换 image tag
旧版 Jitsi Docker 架构通常允许容器:
直接写 /config
以 root 权限运行
运行时配置长期保留
新版架构则变成:
/config
↓
只作为输入
/run
↓
运行时生成配置
/storage
↓
需要长期保存的可写数据
/tmp
↓
临时运行数据
同时容器切换为非 root 用户运行,并且:
ReadonlyRootfs = true
这意味着旧部署中很多“碰巧能工作”的做法都会失效。
例如旧私钥:
owner = root
mode = 0600
以前 root 容器当然可以读取。
切换到 rootless 后:
UID 1000
↓
读取 root:root 0600
↓
Permission denied
文件明明存在,应用却等同于“没有这个文件”。
二、Web 服务为什么会因为一把私钥直接失效
Jitsi Web 内部 Nginx 需要一对:
certificate
private key
新版容器使用的逻辑大致是:
operator input
/config/keys/
│
▼
startup script
│
▼
persistent storage
/storage/keys/
│
▼
Nginx runtime
启动时:
如果 /config 中 cert + key 都存在并可读取
→ 复制到 /storage
否则如果 /storage 中已有完整 cert + key
→ 使用已有文件
否则
→ 生成自签名证书
问题就在“可读取”。
如果:
cert.crt readable
cert.key root-only
rootless 进程只能看到证书,看不到私钥。
结果可能形成一种很特殊的状态:
/storage/keys/cert.crt exists
/storage/keys/cert.key missing
然后 Nginx 启动时报错:
cannot load certificate key
虽然 Docker container 仍然显示:
running
但容器内部真正提供 Web 服务的 Nginx 已经失败。
于是整个链路变成:
公网用户
↓
反向代理
↓
Jitsi HTTP upstream
↓
connection failure
↓
502
这说明:
Docker container running 绝不等于 application healthy。
三、公网 TLS 和 Jitsi 内部 HTTPS 不是同一个东西
反向代理架构中,公网 TLS 往往在前置代理服务器终止:
Internet
↓ HTTPS
Reverse Proxy
↓ HTTP
Jitsi Web
这种情况下:
Let's Encrypt 公网证书
实际上属于:
Reverse Proxy
而不是 Jitsi Web。
Jitsi Web 内部即使还有一个 HTTPS listener,也可能只是:
- 本机访问;
- 内网使用;
- 容器默认功能;
- 调试用途。
因此不能看到 Jitsi 有 cert.crt、cert.key 就直接把公网证书复制进去。
正确做法是先确认:
公网 TLS 到底在哪里终止?
如果是反向代理终止 TLS,然后走 HTTP upstream:
公网证书更新
↓
只需要更新反向代理
并不需要把同一套证书传播到 Jitsi。
这可以避免制造一条完全没有必要的证书同步链路。
四、Prosody 的证书和 Web 证书不是一回事
Jitsi 内部还有一套非常重要的 TLS:
Prosody XMPP
它服务的是内部身份,例如:
会议域
认证域
这些证书通常是:
internal self-signed certificate
而不是公网域名证书。
关系大致是:
Jicofo
│
│ XMPP/TLS
▼
Prosody
▲
│ XMPP/TLS
│
JVB
因此:
Public Web TLS certificate
和:
Prosody internal XMPP certificate
是两个完全不同的问题。
把公网证书直接拿来替换 Prosody 内部证书,通常并不是正确修复方法。
五、Rootless 迁移为什么会同时击中 Web 和 Prosody
旧系统中的私钥权限可能类似:
root-only
或者属于某个历史 UID。
旧 rootful container 可以正常读取。
升级到 rootless 后:
Web UID 1000
Prosody UID 1000
突然都无法读取。
于是 Web 出现:
missing private key
Prosody 出现:
No TLS context available
随后产生连锁反应:
Prosody TLS failure
↓
Jicofo XMPP login failure
↓
JVB XMPP registration failure
↓
Conference unavailable
此时如果只看到:
Jicofo error
JVB error
很容易错误地去调:
- JWT;
- XMPP password;
- public IP;
- firewall;
- NAT;
- UDP;
实际上真正的根因可能只是:
Prosody 读不到 private key
这是典型的“上游故障产生大量下游错误”。
六、修复权限时为什么不能直接 chown -R
遇到 rootless 权限问题,最危险的快速修复是:
chown -R
chmod -R
这样虽然可能让应用瞬间恢复,但也会改变大量:
- 配置文件;
- 历史备份;
- 插件;
- certificate;
- runtime state;
甚至破坏原本正确的权限边界。
更安全的方法是:
确定真正无法读取的文件
↓
只修改该文件
↓
验证 owner/mode
↓
restart 单一服务
例如:
private key
owner = application UID
mode = 0600
而公开证书:
certificate
mode = 0644
通常已经足够。
这是生产服务器维护中非常重要的一条原则:
权限修复应当尽量缩小到单文件,而不是整个目录树。
七、为什么只重启 Prosody,而不重启整个 Jitsi
Jitsi 是多个相互协作的服务:
Web
Prosody
Jicofo
JVB
Prosody 是 XMPP 核心。
如果只是 Prosody 证书更新,更好的顺序是:
restart Prosody
↓
等待
↓
观察 Jicofo reconnect
↓
观察 JVB reconnect
如果 Jicofo 和 JVB 能自动恢复:
无需 restart
这样可以验证系统本身的恢复能力,同时减少:
- 不必要中断;
- 新变量;
- 日志噪声;
- 排障复杂度。
一次维护中,Prosody 重启后:
Jicofo 自动重新认证
JVB 自动重新认证
JVB 自动重新加入内部会议组件
说明整个 XMPP reconnect 机制工作正常。
这种结果比简单地:
restart everything
更有诊断价值。
八、内部证书过期为什么仍可能“正常工作”
一次审计中还发现了一个有趣现象:
Prosody 的内部自签名证书已经超过 notAfter,但:
Prosody
Jicofo
JVB
仍然能够正常建立 TLS 会话。
这是因为:
证书存在有效期字段
并不意味着:
所有客户端一定严格验证有效期
在内部自签名体系中,具体行为取决于:
- 客户端验证策略;
- trust model;
- XMPP 实现;
- TLS 配置。
因此不能因为:
现在还能连
就得出:
证书有效期不重要
更合理的处理方式仍然是:
确认已经过期
↓
独立维护
↓
按应用官方机制重新生成
九、内部证书应该如何安全更新
更新内部 XMPP 证书时,不应该直接在生产目录中生成覆盖。
更稳妥的流程是:
确认官方生成机制
↓
备份当前 cert/key
↓
在隔离临时环境生成
↓
验证 certificate
↓
验证 private key
↓
验证两者匹配
↓
确认 CN / SAN
↓
确认新的有效期
↓
原子替换
↓
restart Prosody
↓
验证 XMPP
其中一个非常关键的动作是:
certificate/key pair matching
应该在真正替换之前完成。
这样能够避免把:
certificate A
private key B
意外组合到一起。
十、Compose project 名也属于部署状态
Docker Compose 的 project name 不只是显示用途。
它会影响:
container name
network name
label
resource ownership
管理命令
迁移期间为了隔离旧部署,通常会创建临时 project,例如:
application-migration
升级完成以后,如果继续留下临时 project 名:
docker compose ps
和实际运行容器之间就可能产生认知偏差。
更规范的最终状态应该是:
Compose project = application canonical name
这样以后直接:
docker compose ps
docker compose up -d
docker compose restart web
即可管理正式部署。
十一、旧容器和旧镜像如何清理
升级完成以后,旧资源应该分两步删除。
第一步:旧容器
先确认:
exited
旧 image
不属于当前 stable project
然后精确删除。
第二步:旧镜像
再检查:
是否还有 container 引用该 image ID
确认没有引用后:
逐个删除旧 image
仍然不需要:
docker system prune
这种做法的优点是:
清理范围完全可解释
以后回看维护记录时,可以准确知道删除了什么。
十二、Jitsi 升级真正需要验证哪些层次
完整验证应该至少包含四层。
1. Docker 层
container running
restart count
OOM status
image
Compose project
2. HTTP 层
本机 HTTP
本机 HTTPS
公网 Web
BOSH / HTTP-Bind
3. XMPP / JVB 层
Prosody certificate loaded
Jicofo authenticated
JVB authenticated
JVB joined brewery
JVB health
advertised public address
4. 用户真实业务层
最终仍然要让两个真实客户端加入会议:
Client A
↕
audio/video
↕
Client B
并从真正使用 Jitsi 的上游应用发起 JWT 会议。
只有这个阶段成功,才能证明:
Reverse Proxy
JWT
Web
Prosody
Jicofo
JVB
NAT
UDP media
整个链路全部可用。
十三、这次迁移最值得保留的经验
这类 Jitsi Docker 大版本迁移,可以总结成几个长期有效的原则。
第一,镜像升级可能意味着架构升级。
不能只看 image tag。
必须比较:
user model
filesystem model
mount model
runtime config model
container port
第二,rootless 迁移最先检查权限。
很多:
file missing
certificate missing
config missing
实际上是:
file exists but unreadable
第三,公网 TLS、Web TLS、XMPP TLS 必须分开理解。
它们属于不同层次,不应该混成一套证书。
第四,优先寻找上游故障。
Prosody TLS 失败之后,Jicofo 和 JVB 的大量错误往往只是结果。
第五,只重启真正需要重启的服务。
这样既降低影响,也保留更多诊断信息。
第六,迁移完成后要规范 Compose project。
临时迁移状态不应该永久进入生产环境。
结语
Jitsi 这种多服务实时通信系统,比普通单容器 Web 应用更容易在升级时出现“容器都在运行,但系统整体不可用”的情况。
真正可靠的升级流程并不是:
pull
up -d
而是:
理解新版架构
↓
迁移配置
↓
处理 rootless 权限
↓
验证 Web
↓
验证 Prosody
↓
验证 Jicofo/JVB
↓
验证公网链路
↓
真实双客户端测试
↓
清理旧资源
↓
规范化正式部署
一旦把 Web、XMPP、媒体链路和证书体系分别看清楚,Jitsi 的升级其实并不神秘。
真正困难的部分,不是执行 Docker 命令,而是正确识别:
哪个错误是根因,哪个错误只是上游故障产生的连锁反应。