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.crtcert.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 命令,而是正确识别:

哪个错误是根因,哪个错误只是上游故障产生的连锁反应。

Leave a Reply

Your email address will not be published. Required fields are marked *