Docker Compose 安装 OnlyOffice

一、安装 OnlyOffice

使用 Docker Compose 安装 OnlyOffice 主要分为创建 docker-compose.yml 文件准备配置文件两步。这里为你提供一个标准、可快速上手的配置方案。

1. 创建 docker-compose.yml 文件

首先,为你的项目创建一个目录(例如 onlyoffice),并在其中新建 docker-compose.yml 文件,填入以下内容:

services:
  documentserver:
    image: onlyoffice/documentserver:latest
    container_name: onlyoffice_documentserver
    restart: always
    ports:
      - "8888:80"
    environment:
      - JWT_SECRET=example # 请修改为强随机密钥
    volumes:
      - /vol1/1000/_docker/onlyoffice/logs:/var/log/onlyoffice
      - /vol1/1000/_docker/onlyoffice/data:/var/www/onlyoffice/Data
      - /vol1/1000/_docker/onlyoffice/lib:/var/lib/onlyoffice
      - /vol1/1000/_docker/onlyoffice/db:/var/lib/postgresql
      # 字体路径
      - /vol1/1000/_docker/onlyoffice/fonts:/var/www/onlyoffice/documentserver/core-fonts/custom

说明volumes 部分将容器内的目录映射到宿主机,这样配置、数据和日志都会保留在本地,便于管理。

2. 导入字体文件

将常用字体压缩包内的所有 .ttf 文件,解压到 docker-compose.yml 文件中配置映射字体路径 /vol1/1000/_docker/onlyoffice/fonts 下,用于配置需要的字体文件。

常用字体压缩包下载:

已包含常用字体:方正小标宋_GBK仿宋_gb2312黑体Times New Roman 等。

注意:仅支持 .ttf 后缀的全英文名称的字体文件(如“fangsong_gb2312.ttf”),不支持含有中文、后缀存在大写的字体文件(如“仿宋_gb2312.TTF”)。

3. 构建容器运行

docker-compose up -d

4. 设置字体文件权限并加载

  • 进入容器命令行 /bin/sh

  • 检查字体文件权限,确保权限至少是 644(所有用户可读):

    ls -la /var/www/onlyoffice/documentserver/core-fonts/custom/*.ttf
    

    输出应类似:

    -rw-r--r-- 1 root root 12345 Jul 13 ... kaitiGB2312.ttf
    

    如果权限不对,如:

    $ ls -la /var/www/onlyoffice/documentserver/core-fonts/custom/*.ttf
    ---------- 1 ubuntu 1001   750216 Jul 13 04:33 /var/www/onlyoffice/documentserver/core-fonts/custom/arialbd.ttf
    ---------- 1 ubuntu 1001   562728 Jul 13 04:33 /var/www/onlyoffice/documentserver/core-fonts/custom/arialbi.ttf
    ---------- 1 ubuntu 1001   557004 Jul 13 04:33 /var/www/onlyoffice/documentserver/core-fonts/custom/ariali.ttf
    

    则修正权限:

    chmod 644 /var/www/onlyoffice/documentserver/core-fonts/custom/*.ttf
    

    再次检查所有字体文件权限,应该显示:

    $ ls -la /var/www/onlyoffice/documentserver/core-fonts/custom/*.ttf
    -rw-rw-r-- 1 ubuntu 1001   750216 Jul 13 04:33 /var/www/onlyoffice/documentserver/core-fonts/custom/arialbd.ttf
    -rw-rw-r-- 1 ubuntu 1001   562728 Jul 13 04:33 /var/www/onlyoffice/documentserver/core-fonts/custom/arialbi.ttf
    -rw-rw-r-- 1 ubuntu 1001   557004 Jul 13 04:33 /var/www/onlyoffice/documentserver/core-fonts/custom/ariali.ttf
    

    若有某个字体文件无法修改权限,则字体文件损坏,需要重新下载导入。如:

    ---------- 1 ubuntu 1001   557004 Jul 13 04:33 /var/www/onlyoffice/documentserver/core-fonts/custom/ariali.ttf
    
  • 强制刷新系统字体缓存:

    fc-cache -f -v
    

    有时 fc-cache 需要明确指定目录:

    fc-cache -f -v /var/www/onlyoffice/documentserver/core-fonts/custom/
    

    然后检查是否被识别(如 fangsong ):

    fc-list | grep -i "kaiti\|fangsong"
    
    # 如果没有输出,则是字体文件没有复制或链接到系统标准字体目录
    # cp /var/www/onlyoffice/documentserver/core-fonts/*.ttf /usr/share/fonts/truetype/
    
  • 重新生成 OnlyOffice 字体索引:

    /usr/bin/documentserver-generate-allfonts.sh
    

    观察输出,注意是否出现了 kaitiGB2312fangsongGB2312 的字样。如果没有任何错误,脚本会生成新的字体列表。

  • 重启 OnlyOffice 服务。

    supervisorctl restart all
    

5. 验证字体加载情况

有两个位置:

  1. /var/www/onlyoffice/documentserver/sdkjs/common/AllFonts.js - 前端使用的字体列表
  2. /var/www/onlyoffice/documentserver/server/FileConverter/bin/AllFonts.js - 转换器使用的字体列表

查询配置内容:

grep -i "kaiti\|fangsong\|gb2312" /var/www/onlyoffice/documentserver/sdkjs/common/AllFonts.js
grep -i "kaiti\|fangsong\|gb2312" /var/www/onlyoffice/documentserver/server/FileConverter/bin/AllFonts.js

示例:

$ grep -i "kaiti\|fangsong\|gb2312" /var/www/onlyoffice/documentserver/sdkjs/common/AllFonts.js
["仿宋_GB2312",79,0,-1,-1,-1,-1,-1,-1],
["楷体_GB2312",82,0,-1,-1,-1,-1,-1,-1],
$ grep -i "kaiti\|fangsong\|gb2312" /var/www/onlyoffice/documentserver/server/FileConverter/bin/AllFonts.js
"/var/www/onlyoffice/documentserver/core-fonts/custom/fangsongGB2312.ttf",
"/var/www/onlyoffice/documentserver/core-fonts/custom/kaitiGB2312.ttf",
["仿宋_GB2312",79,0,-1,-1,-1,-1,-1,-1],
["楷体_GB2312",82,0,-1,-1,-1,-1,-1,-1],

6. 修复 HTTPS 连接验证报错

当我使用 动态转换工具 调用 OnlyOffice 进行 .docx 转换为 .pdf 操作时,报错:

[2026-07-30 00:22:44] [INFO] Process_P - [N/A.convert] OnlyOffice 发起转换:  http://192.168.1.7:10371/ConvertService.ashx , 源文件: https://192.168.1.8:10370/work/dynamic/php/depend/temp/convert_1785342164_aa1e735f.docx
[2026-07-30 00:22:48] [ERROR] Process_P - [N/A.main] OnlyOffice 转换失败 (error -4): Document Server 下载源文件失败(请检查回源地址是否可被 OnlyOffice 服务器访问)
[2026-07-30 00:22:48] [ERROR] Process_P - [N/A.main] 所有转换引擎均失败: OnlyOffice 转换失败 (error -4): Document Server 下载源文件失败(请检查回源地址是否可被 OnlyOffice 服务器访问)
[2026-07-30 00:22:48] [ERROR] MainProcess - [MainProcess._log_response] TransWordToPDF 执行失败:
{'OnlyOffice 转换失败 (error -4): Document Server 下载源文件失败(请检查回源地址是否可被 OnlyOffice 服务器访问)'}

分析:

  • 对于 OnlyOffice Document Server的 ConvertService.ashx 官方只支持URL方式,接受JSON(其中url字段指定可访问的源文件URL),不支持直接POST文件上传。

    我设计的 动态转换工具 调用 OnlyOffice 的逻辑,是向 OnlyOffice 发送 动态转换工具 生产服务器 NGINX 提供的局域网 HTTPS 网址,故而生产服务器实际的 SSL 证书(公网域名)与实际发送的局域网 HTTPS 网址域名不符,导致证书判定异常。

方案 A:让 OnlyOffice 跳过证书校验

改动最小,推荐先用这个。

在 192.168.1.7 上:

docker exec <onlyoffice容器名> sed -i 's/"rejectUnauthorized": true/"rejectUnauthorized": false/' /etc/onlyoffice/documentserver/default.json
docker restart <onlyoffice容器名>

config.ini 不用改,重试转换即可。注意:容器重建(docker compose down/up)后此修改会丢失,需重新执行或把 default.json 挂载出来。

方案 B:回源用域名走正常证书校验

更干净,前提是 work.yogile.site 证书是正规 CA 签发。

  1. 让 OnlyOffice 容器能把域名解析到 192.168.1.8 —— 在 docker-compose.yml 的服务下加:

    extra_hosts:
      - "work.yogile.site:192.168.1.8"
    

    然后 docker compose up -d 重建。

  2. 生产 config.ini 改为:

    onlyoffice_public_base_url = https://work.yogile.site:10370/work/dynamic/php/depend/temp
    

如果证书其实是自签的,方案 B 无效,直接用方案 A。

OnlyOffice 有一个“文档存储服务”的API,通常需要配合文档管理服务(如example)使用。但官方示例服务主要为演示和开发而设计,但其暴露的 API 端点历史上存在多个高危漏洞,在生产环境中启用它会为系统引入不必要的攻击面。

7. 验证 OnlyOffice 服务运行情况

test.py 测试文件:

"""
OnlyOffice 文档转换工具(直接调用内置 ConvertService API,无需 example 服务)

功能:通过 OnlyOffice Document Server 的官方转换接口 ConvertService.ashx,
      将本地 docx 文件转换为 PDF 并下载到指定文件夹。
      容器启动后即可使用,无需执行 supervisorctl start ds:example。

原理说明:
  ConvertService.ashx 不接受文件上传,而是要求提供一个可访问的 URL,
  由 Document Server 主动下载源文件。因此本脚本:
  1. 在本机临时启动一个 HTTP 服务(随机端口,仅暴露待转换的单个文件)
  2. POST /ConvertService.ashx(JSON + JWT 签名,async=false 同步等待)
  3. 从响应的 fileUrl 下载转换后的 PDF
  4. 关闭本地 HTTP 服务

  转换结果只存在于 Document Server 的临时缓存中(自动过期清理),
  不会像 example 服务那样持久化到存储目录,因此无需手动清理服务器文件。

使用方式:
  python test.py                                        # 使用默认路径
  python test.py <docx文件路径>                          # 指定输入文件
  python test.py <docx文件路径> <输出目录>                # 指定输入输出

依赖安装:
  pip install requests pyjwt

注意事项:
  - 本机防火墙需允许 OnlyOffice 服务器(192.168.1.7)访问临时端口的入站连接,
    否则 Document Server 无法下载源文件(报错 error -4)。
  - JWT_SECRET 必须与 docker-compose.yml 中的 JWT_SECRET 一致。
"""

import sys
import time
import uuid
import socket
import threading
from pathlib import Path
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

import requests
import jwt  # pip install PyJWT


# ==================== 配置 ====================

# OnlyOffice 容器所在服务器IPv4地址
ONLYOFFICE_HOST = "192.168.1.7"

# OnlyOffice 容器所在服务器对外服务端口
ONLYOFFICE_PORT = 10371  # 留空("" 或 None)时 URL 不拼接端口,使用协议默认端口

# OnlyOffice 容器所在服务器对外服务协议
ONLYOFFICE_PROTOCOL = "http"  # 仅允许 "http" 或 "https"
assert ONLYOFFICE_PROTOCOL in ("http", "https"), (
    f"ONLYOFFICE_PROTOCOL 必须为 \"http\" 或 \"https\",当前值为: {ONLYOFFICE_PROTOCOL!r}"
)
_ONLYOFFICE_PORT_PART = f":{ONLYOFFICE_PORT}" if ONLYOFFICE_PORT not in ("", None) else ""

# OnlyOffice 容器所在服务器对外服务网址(自动拼接)
ONLYOFFICE_URL = f"{ONLYOFFICE_PROTOCOL}://{ONLYOFFICE_HOST}{_ONLYOFFICE_PORT_PART}"

# JWT 密钥(必须与 docker-compose.yml 中的 JWT_SECRET 一致)
JWT_SECRET = "example"

# 项目根目录
PROJECT_ROOT = Path(__file__).resolve().parent

# 默认输入文件,文件必须与 test.py 在同一文件夹下(或另行指定DEFAULT_DOCX_PATH变量)
DEFAULT_DOCX_PATH = str(
    PROJECT_ROOT / "测试文件.docx"
)
# 默认输出目录,输出文件夹与 test.py 在同一文件夹下
DEFAULT_OUTPUT_DIR = str(PROJECT_ROOT / "输出")

# ConvertService 错误码对照表(官方文档定义)
CONVERT_ERRORS = {
    -1: "未知错误",
    -2: "转换超时",
    -3: "转换出错",
    -4: "Document Server 下载源文件失败(请检查本机防火墙是否放行临时端口)",
    -5: "文档密码不正确",
    -6: "访问转换数据库出错",
    -7: "请求参数错误",
    -8: "JWT 令牌无效(请检查 JWT_SECRET 是否与 docker-compose.yml 一致)",
    -9: "无法自动确定输出格式",
    -10: "文件大小超出限制",
}


# ==================== 本地临时文件服务器 ====================

def _get_local_ip():
    """
    获取本机在局域网中的 IP(即 OnlyOffice 服务器可访问到的地址)。

    通过向 OnlyOffice 主机发起 UDP "连接"(不实际发包)获取本机出口 IP,
    避免多网卡环境下取错地址。

    Returns:
        str: 本机局域网 IP
    """
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect((ONLYOFFICE_HOST, 1))
        return s.getsockname()[0]
    finally:
        s.close()


class _SingleFileServer:
    """
    只暴露单个文件的临时 HTTP 服务器。

    OnlyOffice ConvertService 需要通过 URL 下载源文件,
    该服务器在随机端口上仅响应指定路径的 GET 请求,其余一律 404。
    """

    def __init__(self, file_path):
        """
        Args:
            file_path: 待暴露的本地文件路径
        """
        file_bytes = Path(file_path).read_bytes()
        # ASCII 安全的 URL 路径,避免中文文件名编码问题
        url_name = f"convert_{int(time.time())}_{uuid.uuid4().hex[:6]}.docx"
        expected_path = f"/{url_name}"

        class Handler(BaseHTTPRequestHandler):
            def do_GET(self):
                if self.path != expected_path:
                    self.send_error(404)
                    return
                self.send_response(200)
                self.send_header("Content-Type", "application/octet-stream")
                self.send_header("Content-Length", str(len(file_bytes)))
                self.end_headers()
                self.wfile.write(file_bytes)

            def log_message(self, fmt, *args):
                # 静默访问日志
                pass

        # 绑定 0.0.0.0 随机端口
        self._server = ThreadingHTTPServer(("0.0.0.0", 0), Handler)
        self.port = self._server.server_address[1]
        self.file_url = f"http://{_get_local_ip()}:{self.port}{expected_path}"
        self._thread = threading.Thread(
            target=self._server.serve_forever, daemon=True
        )

    def start(self):
        """启动服务线程"""
        self._thread.start()

    def stop(self):
        """关闭服务器并释放端口"""
        self._server.shutdown()
        self._server.server_close()


# ==================== JWT 生成 ====================

def _sign_request(body):
    """
    为 ConvertService 请求生成 JWT。

    官方要求:
      - body 中的 token 字段:直接以请求参数为 payload 签名
      - Authorization 请求头:以 {"payload": <请求参数>} 为 payload 签名

    Args:
        body: 请求参数 dict(不含 token 字段)

    Returns:
        tuple[str, str]: (body 内嵌 token, Authorization 头 token)
    """
    body_token = jwt.encode(body, JWT_SECRET, algorithm="HS256")
    header_token = jwt.encode({"payload": body}, JWT_SECRET, algorithm="HS256")
    return body_token, header_token


# ==================== 文档转换核心逻辑 ====================

def convert_docx_to_pdf(docx_path, output_dir):
    """
    通过 OnlyOffice ConvertService.ashx 将 docx 文件转换为 PDF。

    执行流程:
      1. 验证输入文件
      2. 启动本地临时 HTTP 服务暴露源文件
      3. POST /ConvertService.ashx 发起同步转换
      4. 从响应的 fileUrl 下载 PDF

    Args:
        docx_path: 输入的 docx 文件路径
        output_dir: PDF 输出目录路径

    Returns:
        str: 输出 PDF 文件的绝对路径

    Raises:
        FileNotFoundError: 输入文件不存在
        ConnectionError: 无法连接 OnlyOffice 服务
        Exception: 转换/下载失败
    """
    docx_path = Path(docx_path).resolve()
    output_dir = Path(output_dir).resolve()

    # ---- 步骤 1:验证输入 ----
    if not docx_path.exists():
        raise FileNotFoundError(f"输入文件不存在: {docx_path}")

    output_dir.mkdir(parents=True, exist_ok=True)
    output_pdf = output_dir / f"{docx_path.stem}.pdf"

    print(f"  [1/4] 输入文件: {docx_path.name} ({docx_path.stat().st_size:,} 字节)")

    # ---- 步骤 2:启动本地临时文件服务 ----
    file_server = _SingleFileServer(docx_path)
    file_server.start()
    print(f"  [2/4] 本地临时文件服务已启动: {file_server.file_url}")

    try:
        # ---- 步骤 3:发起转换 ----
        file_key = f"convert_{int(time.time())}_{uuid.uuid4().hex[:8]}"
        body = {
            "async": False,
            "filetype": docx_path.suffix.lstrip(".").lower(),
            "key": file_key,
            "outputtype": "pdf",
            "title": f"{docx_path.stem}.pdf",
            "url": file_server.file_url,
        }
        body_token, header_token = _sign_request(body)
        body["token"] = body_token

        print(f"  [3/4] 调用 ConvertService.ashx (docx → pdf) ...")
        try:
            resp = requests.post(
                f"{ONLYOFFICE_URL}/ConvertService.ashx",
                json=body,
                headers={
                    "Accept": "application/json",
                    "Authorization": f"Bearer {header_token}",
                },
                timeout=180,
            )
        except requests.exceptions.ConnectionError:
            raise ConnectionError(
                f"无法连接到 OnlyOffice 服务 ({ONLYOFFICE_URL})\n"
                f"请确认:\n"
                f"  1. Docker 容器是否正在运行\n"
                f"  2. 防火墙是否允许端口 {ONLYOFFICE_PORT}"
            )
        except requests.exceptions.RequestException as e:
            raise Exception(f"转换请求失败: {e}")

        if resp.status_code != 200:
            raise Exception(
                f"转换失败 (HTTP {resp.status_code}): {resp.text[:300]}"
            )

        try:
            result = resp.json()
        except Exception:
            raise Exception(f"转换响应解析失败: {resp.text[:300]}")

        # 检查错误码
        if "error" in result:
            code = result["error"]
            raise Exception(
                f"转换失败 (error {code}): {CONVERT_ERRORS.get(code, '未知错误码')}"
            )

        if not result.get("endConvert"):
            raise Exception(f"转换未完成: {result}")

        file_url = result.get("fileUrl")
        if not file_url:
            raise Exception(f"转换完成但响应中未包含 fileUrl: {result}")

        print(f"        转换完成,结果地址: {file_url}")

        # ---- 步骤 4:下载 PDF ----
        print(f"  [4/4] 下载 PDF 到本地...")
        try:
            download_resp = requests.get(file_url, timeout=120)
            download_resp.raise_for_status()
        except requests.exceptions.RequestException as e:
            raise Exception(f"PDF 下载失败: {e}")

        with open(output_pdf, "wb") as f:
            f.write(download_resp.content)

        file_size = output_pdf.stat().st_size
        print(f"        PDF 已保存: {output_pdf}")
        print(f"        文件大小: {file_size:,} 字节")

        return str(output_pdf)

    finally:
        # 无论成败,关闭本地临时文件服务
        file_server.stop()
        # 转换结果仅存于 Document Server 临时缓存,会自动过期清理,
        # 无需像 example 服务那样手动删除服务器文件。


# ==================== 命令行入口 ====================

def main():
    """命令行主入口函数"""
    # Windows GBK 控制台无法打印 ✓/✗ 等符号,统一切换为 UTF-8 输出
    try:
        sys.stdout.reconfigure(encoding="utf-8", errors="replace")
        sys.stderr.reconfigure(encoding="utf-8", errors="replace")
    except AttributeError:
        pass

    print("=" * 60)
    print("  OnlyOffice 文档转换工具 (直接 API,无需 example 服务)")
    print("=" * 60)

    # 解析参数
    if len(sys.argv) >= 2:
        docx_path = sys.argv[1]
    else:
        docx_path = DEFAULT_DOCX_PATH

    if len(sys.argv) >= 3:
        output_dir = sys.argv[2]
    else:
        output_dir = DEFAULT_OUTPUT_DIR

    # 显示配置
    print(f"  OnlyOffice 服务 : {ONLYOFFICE_URL}")
    print(f"  输入文件        : {docx_path}")
    print(f"  输出目录        : {output_dir}")
    print("-" * 60)

    # 检查依赖
    try:
        import requests  # noqa: F401, F811
        import jwt       # noqa: F401, F811
    except ImportError as e:
        print(f"✗ 缺少依赖包: {e}")
        print("请安装: pip install requests pyjwt")
        return 4

    # 执行转换
    try:
        result_path = convert_docx_to_pdf(docx_path, output_dir)
        print("-" * 60)
        print(f"✓ 转换成功!输出文件: {result_path}")
        return 0
    except FileNotFoundError as e:
        print(f"\n✗ 文件错误: {e}")
        return 1
    except ConnectionError as e:
        print(f"\n✗ 连接错误: {e}")
        return 2
    except Exception as e:
        print(f"\n✗ 转换失败: {e}")
        return 3


if __name__ == "__main__":
    sys.exit(main())
posted @ 2026-07-30 17:31  Yogile  阅读(6)  评论(0)    收藏  举报