自建ONLYOFFICE文档服务:从容器到回调保存

一、为什么要在自己机器上跑 ONLYOFFICE

先说结论:如果你只是偶尔打开一个 docx 看一下,不需要自建文档协作服务;如果你经常要给客户发报表、给同事改合同、或者需要一个固定位置的表格长期维护,那自建一套 ONLYOFFICE Document Server 是值得的,成本只是一台 2 核 4G 的小机器。

市面上的在线文档无非两类:一类是 SaaS,把文件交给第三方,方便但数据不在自己手里,而且企业客户常有一条硬要求——合同、财务类的文档不能出内网;另一类是自建,最常用的两个开源选择是 ONLYOFFICE 和 Collabora Online。我最终选了 ONLYOFFICE,原因很实在:它的前端是纯 JavaScript 组件,可以脱离它自带的网盘单独集成,只要你有一个网页和一个回调接口,就能把编辑器嵌进去。这意味着你不需要为了用文档功能而再维护一套 Nextcloud。

但它的部署不是「装个 Docker 镜像就完事」的那种。真正让人卡住的地方全在集成层面:容器之间怎么互相访问、回调地址填什么、JWT 密钥怎么下发、保存权限怎么配。这套东西我前后趟了三四遍,把踩过的坑按顺序记下来,照着走大概半小时能跑通。

二、三种部署形态,先选对再动手

官方提供的安装路径有三条,适用场景完全不同:

  • 一体化镜像(Document Server 自带 Nginx 和前端示例页):最少依赖,适合先做一次功能验证,确认编辑、保存、协作都正常,再去对接自己的系统。
  • 只装 Document Server 容器:这是我们实际用的形态。它只提供一个编辑服务,你的业务系统通过 iframe 嵌入编辑器,通过回调接口收发文件。
  • 和自己的网盘产品打包(如 Workspace 全家桶):功能最全,但组件多、占用大,一台 4G 的小机器带不动。

我建议的顺序是:先用一体化镜像在本机跑通,再换成单容器接入自己的系统。不要一上来就奔着全套去,你会花两天时间在无关的组件上排错。

三、起容器:端口、卷、时区三件事

单容器启动的命令不长,但要一次写对。关键点有三个:编辑服务自己的端口不要直接暴露在公网,数据卷一定要挂出来,时区必须设对。

mkdir -p /data/onlyoffice/{data,logs,lib,db}
docker run -d --name onlyoffice \
  --restart unless-stopped \
  -e TZ=Asia/Shanghai \
  -p 127.0.0.1:8080:80 \
  -v /data/onlyoffice/data:/var/www/onlyoffice/Data \
  -v /data/onlyoffice/logs:/var/log/onlyoffice \
  -v /data/onlyoffice/lib:/var/lib/onlyoffice \
  -v /data/onlyoffice/db:/var/lib/postgresql \
  onlyoffice/documentserver:8.1

逐条解释为什么这么写:

端口绑 127.0.0.1。文档服务本身不需要对公网开放,它只需要被你的 Nginx 反向代理到,以及被业务服务器访问回调。直接 -p 8080:80 等于把一个没有做任何访问控制的编辑服务挂在公网上,扫描器几个小时就能找到。

四个卷必须挂。data 存字体、证书和配置,logs 排错全靠它,lib 存缓存和生成的临时文件,db 是内置的 PostgreSQL。少挂 db 那个卷,容器一旦重建,文档的历史版本和协作记录全丢——这个坑我踩过一次,重新部署之后发现以前编辑过的文档打不开了。

时区。不加 TZ 的话容器内是 UTC,日志时间和你对不上,排查「用户说下午三点保存失败」时会浪费大量时间。

另外注意版本号:不要用 latest。文档服务的版本和前端 API 是绑定的,某次大版本升级后回调参数结构有调整,用 latest 会在你完全不知情的时候把你升级掉。锁住具体的小版本,升级前先在测试环境跑一遍。

四、反向代理:Nginx 这一层最容易出错

文档服务要嵌入到 https 的页面里(iframe 必须同协议,否则浏览器直接拒绝),所以前面必须有一层 https 反向代理。这一层的配置有四处必须做对。

server {
    listen 443 ssl;
    server_name office.example.com;

    ssl_certificate     /etc/nginx/ssl/office.crt;
    ssl_certificate_key /etc/nginx/ssl/office.key;

    client_max_body_size 100m;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_read_timeout 3600s;
        proxy_send_timeout 3600s;
    }
}

WebSocket 必须放行。协作编辑靠 WebSocket 同步光标和内容,少了 Upgrade 和 Connection 两个头,两个人在同一份文档里编辑时会互相看不到对方的改动,表现是「保存后我的内容被覆盖了」——看起来像数据丢失,其实是同步根本没建立。

超时要放大。默认 60 秒,一旦有人打开一份 200 页的文档,服务端生成预览图的时间超过 60 秒,连接被切断,前端显示「无法打开文档」。3600 秒是官方示例的保守值。

client_max_body_size 默认 1m,用户上传一张高清图进文档就 413 了,页面只给一个什么也不说明的错误。

WebSocket 的路径。8.x 之后 WebSocket 走的是根路径下的 upgrade,不需要单独 location /socket.io。老教程里那一段反而会让同步失效,别照抄老文章。

五、JWT:不只是填个密钥那么简单

从 7.2 起 JWT 默认开启。它的作用是让编辑器发出的每一个请求(包括回调到你服务器的请求)都带一个签名,防止别人伪造一份文档 URL 让服务器去加载任意文件。

开启方式是在启动时传入同一个密钥:

-e JWT_ENABLED=true -e JWT_SECRET=$(openssl rand -hex 32)

然后你的业务系统在生成配置时必须用同一个密钥签名。这里最容易出的错有两种:

  • 只在一个方向配了。文档服务开了 JWT,但你的后端生成配置时没签,前端打开就是白屏或者「文档安全令牌无效」。反过来也一样,你的后端签了,服务端没开,会被拒绝。
  • 密钥里有特殊字符。用 openssl rand -hex 32 生成,全是十六进制字符,不会出现 $ & 这类被 shell 或 docker-compose 吃掉一半的字符。手打一个「带感叹号的强密码」是自找麻烦。

还有一个隐蔽的点:JWT 的 exp 时间。默认签出来的令牌有效期设短一点(比如 5 分钟)比较安全,但如果你设得太短,用户打开一份大文档时前端请求已经过期,报错信息是「令牌无效」,其实是加载太慢。10 分钟是个稳妥的值。

六、回调接口:保存的真相在这里

很多人以为编辑器保存时会直接把文件写回你的服务器。不是的。流程是:用户在编辑器里改,改完由文档服务在它的容器里拼装出完整文件,然后主动向你配置的 callbackUrl 发一个 POST,告诉你文件在它的临时地址上,让你自己去下载。

回调的 JSON 里最关键的是 status 和 url 两个字段:

  • status=1:文档正在编辑,你可以顺手更新一下「最后编辑时间」。
  • status=2:所有用户都关闭了文档,文件已就绪,url 可以下载。这时你立刻去下载并覆盖原文件。
  • status=3:保存出错。
  • status=4:文档无改动关闭,url 指向原文件,不用处理。
  • status=6:强制保存(编辑中点了保存按钮),可以处理,但不能当成最终版。

这里有三条实践经验:

回调接口必须在 3 秒内返回。文档服务不会等你处理完下载和写盘。正确做法是收到 status=2 后立刻返回 {"error":0},把下载和落盘丢进一个异步任务。同步下载一份 20M 的文档再返回,服务端会判定超时并重试,最后你会收到三份重复的回调。

下载完必须做一次校验。对比 Content-Length 与写入后的文件大小,并且至少合并一次最小改动再打开确认。我遇到过一次 CDN 中间层把文档服务的 url 缓存了,下载回来的是上一个版本,用户在编辑器里看到的和最终保存的差了十几处修改。后来把回调下载的请求加上 Cache-Control: no-cache,并给 url 加一个时间戳参数才稳定。

回调地址必须能从容器访问。你写 localhost 是没用的,那是容器自己的回环;写内网 IP 但容器在独立 bridge 网络里也可能不通。最稳的是让文档服务能解析到一个能访问到你业务服务的域名,并且这个域名在容器内 curl 得通。排错第一步永远是进容器里 curl 一次。

七、把「打开文档」这件事做对

前端嵌入只需要一个 iframe,但配置对象的字段有一半是可以填错的。最小可用配置长这样:

const config = {
  document: {
    fileType: 'xlsx',
    key: 'doc-1024-v7',
    title: '月度报表.xlsx',
    url: 'https://files.example.com/tmp/1024.xlsx?token=xxx',
    permissions: { edit: true, download: true, print: false }
  },
  documentType: 'cell',
  editorConfig: {
    lang: 'zh-CN',
    callbackUrl: 'https://api.example.com/office/callback?id=1024',
    user: { id: 'u17', name: '老王' },
    customization: { autosave: true, forcesave: true }
  },
  token: '由后端签名生成的JWT'
};

四个字段最容易出错:

  • key:它是文档的会话标识,同一份文件在未关闭时必须用同一个 key,否则会生成两份互不相干的协作会话,用户会觉得「刚才改的没了」。但文件内容被替换后 key 必须变(我一般拼上版本号),否则编辑器会用缓存的旧版本。
  • document.url:这个地址是给容器去下载的,不是给浏览器。所以它必须是容器能访问到的内网或公网地址,且要带临时签名,不能是裸链接。
  • documentType:word 对应 word,xlsx 是 cell,pptx 是 slide。填错会报「不支持的文件格式」,但错误提示不会告诉你其实是类型填错了。
  • fileType:不要带点,写 xlsx 不是 .xlsx。

另外一个常被忽略的点:只读和可编辑用同一个接口时,权限只在前端配置里改是不够的。用户在浏览器里改一下 JS 就能把 edit 打开,但文档服务会重新校验 JWT 里带的权限,所以只要你后端的签名是照真实权限生成的,前端改也没用。这是 JWT 模式真正的价值。

八、字体:中文文档最常见的问题

默认镜像里的中文字体很少。表现是:用户上传一份用宋体、仿宋排版的公文,打开后所有字变成方框或者被替换成无衬线字体,行距全乱,打印出来完全不能用。

解决办法是把字体装进容器,然后刷新字体缓存。字体放在宿主机的挂载目录里更方便后续维护:

# 宿主机上
mkdir -p /data/onlyoffice/fonts
cp /usr/share/fonts/truetype/custom/*.ttf /data/onlyoffice/fonts/
docker cp /data/onlyoffice/fonts/. onlyoffice:/usr/share/fonts/truetype/custom/
docker exec onlyoffice documentserver-generate-allfonts.sh

注意两点:documentserver-generate-allfonts.sh 跑一次要一两分钟,期间服务会短暂不可用,别在业务高峰做;字体文件要装你实际会用到的那几个,把整个 Windows 字体目录搬进去既涉及授权问题,也会让容器启动慢到不可接受。

还有一条排查经验:装完字体后没生效,先确认字体文件名和内部名称是否一致。fc-list 里看到的名字才是文档服务认的名字,有些下载来的字体文件叫「SimSun.ttf」,内部名称却是别的,文档里写的字体名根本匹配不上。

九、上线前必做的五项检查

按这个顺序检查,一次能覆盖绝大多数问题:

  1. 容器内能不能访问到你的文件地址:docker exec -it onlyoffice curl -I "你的文档URL",必须是 200。
  2. 容器内能不能访问到回调地址:同样 curl 一次,返回 200 或 405 都算通(405 说明是 POST 接口)。
  3. https 页面里能不能嵌入:浏览器控制台如果没有混合内容警告,说明协议和证书都对。
  4. 两个人同时打开同一份文档,各改一处:两边都能立刻看到对方的改动,说明 WebSocket 通了。
  5. 改完关闭,等十秒,重新打开:内容还在,说明回调下载与落盘这条链路完整。

第五项是最容易漏也是最重要的。前四项都通过但第五项失败,说明回调返回了但异步任务没跑完或者写错了路径,这种「能编辑但保存不上」的状态对用户来说是最糟的,因为他不确定自己的修改是不是还在。

十、小结

自建文档服务这件事,装起来容易,集成起来琐碎。真正的难点不在 Docker 命令,而在三条链路上:容器能访问到你的文件、你的服务器能收到回调、回调下载回来的确实是新版。JWT 保证权限不被前端绕过,字体决定中文文档能不能正常显示,时区和版本号决定半年后你还能不能看懂自己的日志。

如果只记一句话:先用一体化镜像验证功能,再按上面三件事逐条检查集成链路,别在没跑通第五项检查的情况下就对外宣布「文档系统上线了」。

Last modification:November 6th, 2026 at 07:28 pm

Leave a Comment