SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

draw.io、Excalidraw 和 Kroki 自托管怎么选

对比 draw.io、Excalidraw 与 Kroki:前两者在浏览器渲染,服务器看不到图表内容;Kroki 会接收源文本并在 VPS 上生成图像,隐私差异取决于渲染位置。

应运行哪种自托管图表工具?

自托管图表工具主要有两种形态,而形态比功能列表更重要。draw.io 和 Excalidraw 是浏览器应用:容器提供 JavaScript,浏览器负责绘图,服务器完全看不到图表内容。Kroki 则相反。您通过 HTTP 向它发送图表文本,它返回图像,因此每个图表都会经过您自己的计算机。

如果您想在 wiki 旁边运行完整编辑器,请运行 draw.io。如果您想要快速草图工具,并接受内容不会保存到绘图所用浏览器之外,请运行 Excalidraw。如果您的图表以文本形式存储在 git 中,并与所描述的代码放在一起,请运行 Kroki。

自托管图表工具实际改变了什么

请准确确认哪些部分会访问您的服务器,因为这一点决定了自托管带来的是隐私,还是仅仅是可用性。

  • draw.io 在浏览器中渲染。您的容器负责提供应用程序代码。文件会保存到您指定的位置。
  • Excalidraw 在浏览器中渲染,并将当前场景保存在该浏览器的本地存储中。服务器端不会写入任何内容。
  • Kroki 在服务器上渲染。图表源文件和生成的图像都会存在于您的容器中。

只有第三种情况会将数据移到您控制的硬件上。对于前两种情况,自托管带来的是资源控制权和可用性:JavaScript 由您的主机提供,因此即使第三方服务中断、修改服务条款,或从您的网络无法访问,编辑器仍可继续工作。对某些团队而言,这具有实际价值。但这与“图表绝不会离开办公场所”是不同的说法。

draw.io:不存储任何内容的官方容器

该项目会发布自己的镜像,其 README 中的快速启动命令只有一行。

docker run -it --rm --name="draw" -p 8080:8080 -p 8443:8443 jgraph/drawio

这会在服务器的所有地址上发布编辑器。在 VPS 上,应将发布的端口绑定到 loopback,再通过反向代理或 SSH 隧道访问。

docker run -d --name drawio --restart unless-stopped -p 127.0.0.1:8080:8080 jgraph/drawio

通过隧道打开 http://127.0.0.1:8080/?offline=1&https=0。README 将 ?offline=1 称为“禁用云存储支持的安全功能”。未启用该选项时,编辑器会提供 Google Drive、OneDrive 和 GitHub 作为保存目标,而这些目标属于其他服务器。

绑定到 127.0.0.1 才能让该端口不暴露在公网。普通的 -p 8080:8080 不会受到 ufw 过滤,因为 Docker 会将自己的 iptables 规则插入到 ufw 管理的链之前。因此,防火墙配置看起来正确,但该端口仍会响应来自公网的请求。Docker 绕过 ufw 直接发布端口介绍了其机制和修复方法。

编辑器不再运行在 localhost 上后,两个环境变量很重要。

services:
  drawio:
    image: jgraph/drawio
    container_name: drawio
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
    environment:
      DRAWIO_SERVER_URL: "https://drawio.example.com/"
      DRAWIO_BASE_URL: "https://drawio.example.com"

结尾的斜杠不是笔误。README 将 DRAWIO_SERVER_URL 定义为“带结尾斜杠的公开部署 URL”,将 DRAWIO_BASE_URL 定义为“不带结尾斜杠的相同 URL”,供 viewer、lightbox 和嵌入代码路径使用。如果通过子路径(例如 https://www.example.com/drawio/)提供编辑器,两个值都必须包含该子路径,因为应用会根据它们构建 viewer 和嵌入 URL。

持久化:不存在,这就是设计方式。该 Compose 文件中没有 volume,因为容器不保存任何图表数据。.drawio 文件是 XML,由编辑器交给浏览器;你选择的保存目标决定文件最终保存在哪里:下载到自己的计算机,或保存到嵌入编辑器的应用中。请备份该目标。如果目标是 VPS 上的某个目录,那么需要保护的是该目录,以及用于访问它的文件管理器,因为 draw.io 不会保留任何副本。

仍会离开服务器的内容。由于导出不再发送到其他位置,离开服务器的内容比过去少。README 现在将 EXPORT_URL 和 DRAWIO_SELF_CONTAINED 列为已移除且不起作用的变量。它们原先会让编辑器使用独立的 jgraph/export-server 镜像,而该镜像“已停止维护”。现在,entrypoint 会将 window.EXPORT_URL = null 写入 PreConfig.js,告知编辑器不存在导出服务。实际使用中,选择“导出为 > PDF”会打开浏览器的打印对话框,因此 PDF 不再提供包含图表副本的选项,服务器渲染的 PNG 和 JPEG 导出也会被隐藏。在浏览器中渲染的 PNG、JPEG 和 SVG 导出仍可用。ENABLE_DRAWIO_PROXY 默认关闭;启用后会提供一个 /proxy endpoint,代表浏览器获取外部图片 URL,因此除非确实需要,否则应保持关闭。

Excalidraw:只包含静态资源包,不提供后端服务器

官方镜像页面提供了以下命令。

docker run --rm -dit --name excalidraw -p 5000:80 excalidraw/excalidraw:latest

出于与前文相同的原因,将发布端口绑定到 loopback。

docker run -d --name excalidraw --restart unless-stopped -p 127.0.0.1:5000:80 excalidraw/excalidraw:latest

容器内的 nginx 在端口 80 上提供编译后的 JavaScript 资源包。该镜像的压缩大小约为 41 MB(Docker Hub,2026 年 10 月),这说明其中包含的内容很少。没有数据库、会话存储或上传目录,因为服务器没有需要存储的内容。

镜像页面明确说明了限制:“目前,自托管实例不支持共享或协作功能。”界面中仍然显示这些按钮,因此有必要了解原因。实时协作需要 websocket 服务器,该服务器单独发布为 excalidraw/excalidraw-room。共享链接需要存储服务来保存加密场景。两者的地址会在构建时作为 Vite 变量(VITE_APP_WS_SERVER_URL、VITE_APP_BACKEND_V2_GET_URL、VITE_APP_BACKEND_V2_POST_URL)编译到资源包中,而仓库中的生产环境值指向 Excalidraw 自己托管的服务。Vite 会在构建期间替换这些值,因此它们最终会作为字面字符串写入 JavaScript。将这些值设置为容器环境变量不会产生任何作用,因为没有代码在运行时读取它们。如果要让协作功能指向自己的 room server,就必须使用自定义值从源代码构建前端。在据此规划之前,先确认该服务器的状态:截至 2026 年 10 月,Docker Hub 上 excalidraw/excalidraw-room 镜像最新一次推送是在 2023 年 12 月。

绘图实际保存在哪里。 场景保存在浏览器的本地存储中,位于该设备上对应的 origin 下。在隐私窗口中打开相同 URL,画布会为空,这是最快的验证方法。清除站点数据会删除绘图,而且服务器上没有副本可供恢复。因此,应指导用户使用“保存到...”并保留 .excalidraw 文件;该文件是 JSON 格式,应存放在会进行备份的位置。共享实例会为每个人提供独立的私有画布。应将其视为一个托管的个人草稿本。

Kroki:图表即代码,在服务器上渲染

Kroki 是位于多个渲染器前的统一 HTTP 网关。您通过 POST 发送文本,并获取 SVG 或 PNG。Graphviz、PlantUML、D2 及其他多个渲染器已内置在网关镜像中。Mermaid、BPMN 和 Excalidraw 在配套容器中运行,因此使用 Compose 是合理的部署方式。下面的示例来自 Kroki 文档。

services:
  kroki:
    image: yuzutech/kroki
    depends_on:
      - mermaid
      - bpmn
      - excalidraw
    environment:
      - KROKI_MERMAID_HOST=mermaid
      - KROKI_BPMN_HOST=bpmn
      - KROKI_EXCALIDRAW_HOST=excalidraw
    ports:
      - "8000:8000"
    tmpfs:
      - /tmp:exec
  mermaid:
    image: yuzutech/kroki-mermaid
    expose:
      - "8002"
  bpmn:
    image: yuzutech/kroki-bpmn
    expose:
      - "8003"
  excalidraw:
    image: yuzutech/kroki-excalidraw
    expose:
      - "8004"

expose不会向主机发布任何端口,因此配套容器只能通过 Compose 网络从网关访问。这正是所需的行为。除非调用它的 wiki 运行在另一台主机上,否则请将网关行改为 "127.0.0.1:8000:8000"。如果您以前没有在服务器上编写过 Compose 文件,请参阅在 VPS 上运行 Docker Compose,其中介绍了文件布局和 docker compose up -d 周期。

按以下顺序运行两个基本测试,因为它们失败的原因不同。

curl -s -X POST http://127.0.0.1:8000/graphviz/svg \
  -H 'Content-Type: text/plain' \
  --data-binary 'digraph G {Hello->World}' | head -c 60

Graphviz 在网关内部运行,因此这里返回 SVG 文档即可证明网关本身运行正常。现在测试跨容器的路径。

curl -s -X POST http://127.0.0.1:8000/mermaid/svg \
  -H 'Content-Type: text/plain' \
  --data-binary 'graph TD; A-->B;' | head -c 60

第二条命令返回 SVG,证明 KROKI_MERMAID_HOST 已解析,并且配套容器已响应。如果第一条命令成功而第二条失败,故障位于两个容器之间,因此请先查看 docker compose logs kroki,再检查图表语法。

GET 形式会将图表编码到 URL 中,因此 wiki 无需任何插件即可嵌入图像。文档提供了此编码器。

cat hello.dot | python -c "import sys; import base64; import zlib; print(base64.urlsafe_b64encode(zlib.compress(sys.stdin.read().encode('utf-8'), 9)).decode('ascii'))"

在 Ubuntu 上,该命令输出 python: command not found,因为系统提供的是 python3,而不是不带版本号的 python。请使用 python3。输出内容应追加到形如 /{diagram-type}/{output-format}/{encoded-diagram} 的 URL 末尾,任何 <img> 标签都可以指向该 URL。此方式存在长度上限:KROKI_MAX_URI_LENGTH 默认为 4096 字节,因此较长的图表必须通过 POST 发送。

Kroki 会读取您发送的文本,因此应重点配置其安全设置。 KROKI_SAFE_MODE 默认为 SECURE,这是三个级别中限制最严格的一个;KROKI_PLANTUML_ALLOW_INCLUDE 默认为 false。设置这些默认值的原因是,PlantUML 的 !include 指令会从渲染器的视角读取文件和 URL。在任何人都能访问的端点上放宽这些设置,相当于让互联网访问运行在您容器内的文件读取器。除非您明确知道需要哪个 include 路径,否则请保持默认设置;需要时使用 KROKI_PLANTUML_INCLUDE_PATH 指定路径。

内存:在小型 VPS 上,哪个容器最占资源

了解每个容器运行的内容后,资源占用顺序很容易判断。

  • Excalidraw 镜像由 nginx 提供静态文件服务。在这 3 个容器中,它的开销低得多。
  • draw.io 运行 Tomcat,也就是 Java 应用服务器,因此始终会占用 JVM(Java 虚拟机)资源,无论是否有人绘图。
  • Kroki 网关同样是 Java 服务,手动安装时以 jar 文件形式提供。
  • mermaid companion 的开销最高。其 Dockerfile 会安装 Chromium,并设置 PUPPETEER_EXECUTABLE_PATH=/usr/lib/chromium/chrome,因为 Mermaid 会使用真实的浏览器引擎进行渲染。

因此,空闲时的数值参考价值很低。真正需要关注的是渲染图表时的峰值,而 KROKI_MERMAID_MAX_CONCURRENCY 的默认值为 6,因此最多可以同时进行 6 次浏览器渲染。请在自己的服务器上进行测量,不要直接相信已发布的数据。

docker stats --no-stream
docker system df

先在所有服务都处于空闲状态时运行第一个命令,然后循环渲染大型 mermaid 图表时再次运行。如果小型方案上的峰值过高,请直接设置上限,不要靠猜测:为 Compose 服务设置内存限制介绍了具体语法,以及容器达到上限时会发生什么。移除 mermaid companion 也是可行的方案,因为该网关仍会继续提供其中内置的所有渲染器。

这些服务都没有内置用户模型,因此需要在前面加一层

draw.io 没有账户。Excalidraw 没有账户。Kroki 会响应收到的任何请求。登录功能必须由代理提供。

sudo apt update && sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd alice

htpasswd -c 会创建文件,并覆盖已有文件,因此第一次传递 -c,之后不要再传递。

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

    location / {
        auth_basic "diagrams";
        auth_basic_user_file /etc/nginx/.htpasswd;
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

使用 sudo nginx -t && sudo systemctl reload nginx 应用配置。nginx -t 部分很重要:如果配置损坏,reload 会保留旧配置继续运行,因此网站仍然可用,但修改不会生效。逐行解释反向代理配置介绍了此代码片段省略的请求头配置块和证书路径。

对于 Kroki,基本身份验证不是正确的工具,原因值得理解。wiki 页面会使用 <img> 标签嵌入 Kroki 图片。读者的浏览器会将该 URL 作为跨源子资源请求。浏览器会附带它已保存的 Kroki 主机 Basic 凭据,但当前浏览器不会为跨源图片显示登录提示。因此,尚未直接登录 Kroki 主机的读者会收到 401,并将页面上的所有图表都看作加载失败的图片。应让 Kroki 保持在公网之外。将它放在与 wiki 容器相同的 Docker 网络中,让 wiki 通过服务名访问它,并且完全不向主机发布端口。Compose 网络如何解析服务名说明了实现这一点的关键机制。

自托管 Wiki 旁边运行的图表工具

这通常是人们需要这类工具的原因。Wiki 页面需要插图,但没人希望这张图只是某人的笔记本电脑截图。

BookStack 原生支持自托管编辑器。其默认嵌入 URL 是 https://embed.diagrams.net/?embed=1&proto=json&spin=1&configure=1,在 .env 中添加一行即可将其指向您的容器。

DRAWIO=https://drawio.example.com/?embed=1&proto=json&spin=1&configure=1

请完整复制查询字符串。BookStack 文档说明,embed=1&proto=json&spin=1 “是集成 BookStack 正常运行所必需的”,因为它们选择了两个页面用于通信的 JSON 消息协议。同一页面还指出了 stealth=1:“如果您不希望使用其他外部服务”,则可添加此选项;当停止出站调用是自托管的目标时,就应使用该选项。完成配置后,BookStack 会将图表保存到页面旁边的自身图像存储中,因此您现有的 Wiki 备份也会包含图表备份。

如果还未确定使用哪个 Wiki,请先解决这个问题。在 BookStack、Wiki.js 和 Outline 之间做出选择 是前一步的决策,因为 Wiki 决定了图表如何附加到页面,也决定了您应将这些工具中的哪一个接入其中。

故障模式及你将看到的字符串

绘图编辑器在 BookStack 中打开后一直转圈。 加载指示器正在 spin=1 等待图表数据。使用 proto=json 时,编辑器准备就绪后会发送 init 事件,并期望收到携带图表的 load 操作作为响应。因此,只要这次交互未完成,加载指示器就会一直显示。检查 embed=1&proto=json&spin=1 是否存在于你的 DRAWIO 值中,并确认主机部分没有拼写错误。

HTTPS wiki 中的编辑器框架保持空白。 浏览器控制台报告混合内容,并在 https:// 中加载 http://。浏览器会阻止该框架,draw.io 也不会运行。请通过 HTTPS 提供编辑器。

Kroki 返回 413 Request Entity Too Large。 这是 nginx 对该错误的确切命名,因此应首先检查代理。但 Kroki 也可能自行发送 413,其文档将此称为 Content Too Large。nginx 的 client_max_body_size 默认值为 1 MB,这是较大的 PlantUML 源代码会触及的限制,因为 PlantUML 在网关内渲染。Kroki 自身的 KROKI_MAX_BODY_SIZE 默认也为 1mb,但该设置只存在于 diagrams.net、Excalidraw 和 Mermaid 伴随容器中。因此,较大的 Mermaid 源代码可能触及任一限制。首先提高 client_max_body_size;当图表经过伴随容器时,也要提高该容器上的 KROKI_MAX_BODY_SIZE。

Mermaid 失败,但 graphviz 正常。 网关运行正常,但未访问伴随容器。使用 docker compose ps 检查服务是否已启动,然后检查 KROKI_MERMAID_HOST 是否与服务名称匹配,因为它默认为 127.0.0.1;在网关容器内,这表示网关自身。

Excalidraw 协作始终无法连接。 如果你基于自己的房间服务器构建前端,并将其置于 nginx 后面,代理必须通过 proxy_set_header Upgrade $http_upgrade; 和 proxy_set_header Connection "upgrade"; 升级连接。缺少这两个设置时,websocket 握手会被作为普通 HTTP 请求处理,会话也不会启动。

浏览器清理后画布为空。 场景数据保存在该设备的本地存储中,服务器上没有副本。解决方法不是修改设置,而是养成习惯:将任何值得保留的内容导出为 .excalidraw 文件。

FAQ

自托管 draw.io 会让我的图表保持私密吗?

它会将应用程序代码保留在您的服务器上,但这与保证数据私密是两回事。draw.io 在浏览器中渲染,因此容器实际上不会保存任何图表。隐私取决于您将文件保存在哪里,以及保留启用哪些出站调用。使用 ?offline=1 可禁用云存储目标,并保持 ENABLE_DRAWIO_PROXY 关闭。导出也不会发送任何数据:该镜像未配置导出服务,因此 PDF 会通过浏览器的打印对话框生成,DRAWIO_SELF_CONTAINED 也不再生效。

为什么自托管 Excalidraw 无法协作?

官方镜像页面说明,自托管“不支持共享或协作功能”。实时协作需要独立的 excalidraw/excalidraw-room WebSocket 服务器,共享链接需要存储服务。两者的地址会在构建时作为 Vite 变量编译到 JavaScript 包中,例如 VITE_APP_WS_SERVER_URL,因此在运行中的容器内设置环境变量不会生效。使用您自己的房间服务器时,需要使用这些值从源代码构建前端。

如何在自己的服务器上渲染 Mermaid 图表?

运行 Kroki 及其 Mermaid 配套容器,并将 KROKI_MERMAID_HOST 设置为该服务名称。然后将图表文本 POST 到 /mermaid/svg,再从响应中读取 SVG;也可以将图表编码到 GET URL 中,并将其指向 <img> 标签。该配套容器通过 Puppeteer 驱动 Chromium,因为 Mermaid 需要浏览器引擎,因此需要预留内存:KROKI_MERMAID_MAX_CONCURRENCY 默认同时执行 6 次渲染。

这些工具前面需要配置密码吗?

需要,因为这些工具都没有账户功能。draw.io 和 Excalidraw 会向任何找到 URL 的人提供完整编辑器,Kroki 则会渲染发送给它的任意文本。对于这两个编辑器,在反向代理上配置基本身份验证即可。对于 Kroki,应将其限制在与 wiki 共享的 Docker 网络中,不要发布到公网,因为读者的浏览器不会为另一个源上的 <img> 显示登录提示;因此,任何未直接登录 Kroki 的用户都会无法查看所有嵌入式图表。

#diagrams#drawio#excalidraw#mermaid#kroki#Docker