在自托管环境中,OnlyOffice DocumentServer 往往只是一个“应用执行层”:真正需要长期保存的文档通常位于外部云盘或文件服务中,而 DocumentServer 负责文档解析、协同编辑和格式转换。
这类应用看似可以直接“拉取新镜像然后重建”,但实际升级中仍有几个很容易被忽略的问题:JWT 密钥是否稳定、容器内部数据库如何迁移、哪些目录需要持久化,以及为什么一次普通的容器重建可能突然造成明显的磁盘 I/O 压力。
下面以一次实际的 Docker 升级过程为例,整理这些问题背后的原理。
一、先区分“应用数据”和“用户数据”
OnlyOffice DocumentServer 本身并不一定保存最终用户文档。
典型部署结构是:
云盘 / 文件服务
│
│ 文档内容
▼
OnlyOffice DocumentServer
│
├─ 文档转换
├─ 在线编辑
├─ 临时缓存
├─ 字体与转换资源
└─ 内部数据库/消息组件
真正重要的数据通常仍由云盘系统负责保存。
因此,对 DocumentServer 的升级策略不一定需要达到数据库服务器那种“任何字节都不能丢”的灾难恢复级别。
真正需要重点保护的是:
- Docker Compose 配置;
- 环境变量;
- JWT 密钥;
- 必要的持久化目录;
- 与外部应用之间的认证关系。
这也是判断备份范围时非常重要的原则:
不要因为应用内部存在数据库,就自动把它当作业务数据数据库。
二、JWT 密钥为什么应该固定
OnlyOffice 与外部云盘集成时,常使用 JWT 对请求进行认证。
一个常见问题是:
JWT secret 只存在于容器内部配置
↓
删除并重新创建容器
↓
新容器重新生成 secret
↓
云盘仍保存旧 secret
↓
集成失效
这种部署在日常运行时可能完全正常,因为普通的:
restart
不会删除容器 writable layer。
但只要执行:
recreate
问题就暴露出来了。
更合理的做法,是把 JWT 参数放入宿主机持久化配置,例如:
JWT enabled
JWT secret
JWT header
再由 Compose 注入容器。
这样就形成:
宿主机配置
│
└── 固定 JWT secret
│
▼
新旧容器保持一致
以后即使:
- 升级镜像;
- 删除容器;
- 重建 Compose project;
- 迁移到新服务器;
JWT 都不会因为容器生命周期变化而改变。
对于应用容器而言,这种配置持久化往往比备份整个 writable layer 更有价值。
三、restart 和 recreate 是两件完全不同的事情
很多 Docker 问题都来自这一点。
restart
执行 restart 时:
Container
├─ Image layer
└─ Writable layer
两个部分都还存在。
容器以前初始化生成的:
- 缓存;
- 字体索引;
- 临时配置;
- 运行时生成文件;
往往仍然存在。
因此重新启动可能非常快。
recreate
而 Compose recreate 的过程更接近:
旧 container 删除
↓
旧 writable layer 消失
↓
从 image 创建新 container
↓
重新执行初始化
即使镜像版本完全没有变化,也可能重新触发:
- 字体扫描;
- 字体索引生成;
- 数据目录初始化;
- 内部数据库检查;
- RabbitMQ 初始化;
- Web 服务生成配置;
- 各类缓存重建。
这意味着:
“镜像没变”并不等于“启动过程没变”。
四、为什么一次重建可能造成非常高的 I/O
在一次实际重建过程中,可以观察到字体生成程序长时间运行,同时整台服务器出现明显的 I/O wait。
这类情况有一个很容易误判的地方。
看到:
font generator
处于不可中断睡眠状态,并不能直接证明它就是唯一的 I/O 制造者。
更准确的解释是:
多个服务初始化
+
大量小文件访问
+
数据库写入
+
缓存重建
+
单块底层存储
↓
随机 I/O 队列积压
↓
大量进程进入 D state
此时经常会出现一种现象:
磁盘利用率接近饱和
但实际吞吐只有几 MB/s
这并不矛盾。
磁盘性能不仅取决于 MB/s,还取决于 IOPS 和延迟。
大量:
- 小文件读取;
- metadata 更新;
- journal 写入;
- 数据库随机访问;
即使吞吐量很低,也能把机械硬盘或较慢存储设备完全打满。
因此判断磁盘压力时,不能只看:
MB/s
还应该看:
await
queue depth
utilization
iowait
blocked processes
五、持久化 PostgreSQL 时要注意跨版本残留
OnlyOffice 的一体化镜像内部可能自带 PostgreSQL。
如果数据库目录被 bind mount 持久化,升级后可能看到类似:
old-major/
new-major/
两个数据库集群目录同时存在。
这时不能简单认为:
旧目录存在 = 仍然被使用
正确方法是检查当前 PostgreSQL cluster 状态。
如果当前只有新 major cluster:
online
而旧 cluster 已不再注册、不再运行,那么旧目录通常只是升级遗留。
这种清理应遵循:
确认当前运行 cluster
↓
确认旧目录未使用
↓
再删除旧目录
而不是直接根据目录名称猜测。
六、升级验证不应该只看“容器是 Up”
Docker 显示:
Up
只能证明 PID 还活着。
完整验证至少应该包括几个层次。
第一层:容器状态
检查:
running
restart count
OOM killed
image
第二层:应用内部服务
DocumentServer 一体化容器中通常还有:
Web server
Document service
Converter
Database
Message queue
Supervisor
必须确认核心服务实际处于运行状态。
第三层:健康接口
应该分别验证:
localhost health endpoint
reverse proxy public endpoint
这样可以同时确认:
应用
Docker publish
宿主网络
反向代理
TLS
整个链路。
第四层:认证
JWT 不应该只检查“变量存在”。
更可靠的方法是:
宿主机 secret
│
├── hash
│
容器 secret
│
└── hash
只比较是否相同
既能确认一致,又不会把 secret 打到终端或日志中。
第五层:真实业务测试
最终仍然需要真实应用验证:
打开文档
→ 编辑
→ 保存
→ 关闭
→ 再次打开
只有这一层成功,才能证明整个:
云盘
↔ JWT
↔ DocumentServer
↔ 保存回写
链路真正正常。
七、旧镜像应该精确删除,而不是 prune
升级成功以后,经常需要释放旧镜像占用空间。
更安全的方式是:
确定旧 image ID
↓
确认没有 container 引用
↓
删除该 image
而不是:
docker image prune
docker system prune
在运行大量自托管应用的服务器上,后两者可能清理掉本来希望保留的:
- 停止容器;
- 未运行但准备回退的镜像;
- 临时 network;
- 构建缓存。
精确删除始终比全局清理更适合作为生产服务器维护方式。
八、备份也需要控制范围
升级前进行备份当然是好习惯,但备份并非越多越好。
对于这类可重新部署的应用,真正高价值的备份通常只是:
docker-compose.yml
.env
关键认证配置
而不是每次升级都打包整个应用目录。
尤其当:
- 用户文档并不存储在该应用;
- 镜像可以重新下载;
- 内部缓存可以重建;
- 数据目录已经单独持久化;
那么创建数 GB 的全量归档可能只是增加:
- I/O;
- 操作时间;
- 清理工作;
- 判断复杂度。
好的备份策略应该围绕:
“哪些东西无法轻易重新生成?”
而不是:
“哪些东西能复制就全部复制。”
九、这次升级最重要的几个经验
OnlyOffice 这类 Docker 应用升级,可以归纳成一个相对简单的流程:
固定关键 secret
↓
备份少量核心配置
↓
拉取目标镜像
↓
正常关闭旧应用
↓
Compose recreate
↓
等待首次初始化完成
↓
验证内部服务
↓
验证 health endpoint
↓
验证 JWT
↓
进行真实文档测试
↓
精确清理旧镜像和旧数据残留
其中最值得注意的不是某个具体版本,而是几个长期有效的原则:
容器 restart 与 recreate 不等价。
真正重要的是外部持久化配置,而不是容器 writable layer。
低吞吐并不意味着磁盘没有饱和。
认证 secret 应该脱离容器生命周期。
生产服务器应优先精确清理,而不是全局 prune。
只要把这些边界处理清楚,OnlyOffice 这样的应用即使跨较大版本升级,也可以保持部署结构简单、可维护,而且不需要把升级过程做成一次复杂的数据迁移工程。