自架圖表工具比較:draw.io、Excalidraw、Kroki
比較 draw.io、Excalidraw 與 Kroki 自架於 VPS 的差異,釐清哪些內容不會接觸伺服器,以及為何只有 Kroki 會在主機上處理圖表資料。
應執行哪個自架圖表工具?
自架圖表工具可分為兩種類型,而且類型比功能清單更重要。draw.io 和 Excalidraw 是瀏覽器應用程式:容器提供 JavaScript,由瀏覽器負責繪圖,伺服器不會看到圖表內容。Kroki 則相反。您會透過 HTTP 傳送圖表文字,Kroki 再回傳影像,因此每張圖表都會經過您自己的機器。
如果您想在 wiki 旁使用完整編輯器,請執行 draw.io。如果您想要快速的草圖工具,並接受內容只會儲存在繪圖所用的瀏覽器中,請執行 Excalidraw。如果圖表是與所描述程式碼放在同一個 git 儲存庫中的文字,請執行 Kroki。
自架圖表工具實際改變的內容
請準確確認哪些部分會接觸您的伺服器,因為這項因素會決定自架服務帶來的是隱私,還是只有可用性。
- draw.io 在瀏覽器中呈現。您的容器提供應用程式程式碼。檔案會儲存到您指定的位置。
- Excalidraw 在瀏覽器中呈現,並將目前的場景保存在該瀏覽器的 local storage 中。伺服器端不會寫入任何資料。
- Kroki 在伺服器上呈現。圖表原始碼與產生的圖片都會存在您的容器內。
只有第三種情況會將資料移至您控制的硬體。前兩種情況下,自架服務帶來的是資產控制權與可用性:JavaScript 由您的主機提供,因此即使第三方服務中斷、變更服務條款,或從您的網路無法連線,編輯器仍能繼續運作。對部分團隊而言,這具有實際的經濟價值。但這與「圖表永遠不會離開辦公場所」是不同的說法。
draw.io:不儲存任何資料的官方容器
專案會發布自己的映像檔,README 中的快速入門只需要一行指令。
docker run -it --rm --name="draw" -p 8080:8080 -p 8443:8443 jgraph/drawio這會讓編輯器在該主機的所有位址上提供服務。在 VPS 上,請將發布的連接埠繫結至 loopback,再透過反向代理或 SSH tunnel 存取。
docker run -d --name drawio --restart unless-stopped -p 127.0.0.1:8080:8080 jgraph/drawio透過 tunnel 開啟 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 會在 ufw 管理的 chain 前方插入自己的 iptables 規則。因此,防火牆設定看似正確,但該連接埠仍會回應所有外部連線。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 和 embed 程式碼路徑使用。如果你將編輯器發布在 https://www.example.com/drawio/ 這類子路徑下,兩個值都必須包含該子路徑,因為應用程式會根據這些值建立 viewer 和 embed URL。
持久化:不存在,這就是設計方式。該 Compose 檔案沒有設定任何 volume,因為容器不持有圖表資料。.drawio 檔案是編輯器交給瀏覽器的 XML,而你選擇的儲存目標會決定資料的存放位置:下載至自己的電腦,或交給嵌入編輯器的應用程式。請備份該目的地。如果目的地是 VPS 上的資料夾,那麼需要保護的就是該資料夾,以及你用來存取它的檔案管理器,因為 draw.io 不會保留任何副本。
仍會離開伺服器的資料。匯出為 PDF 是最明顯的例子。README 將 DRAWIO_SELF_CONTAINED 說明為「設定為 1,透過 Tomcat 的 ExportProxyServlet(/service/0)處理匯出請求,而不是直接呼叫匯出伺服器」。反向理解即可:預設情況下,匯出請求不會留在你的部署環境內。專案也發布 jgraph/export-server,這是「draw.io 的獨立 image-export-server」,供希望在自有硬體上進行該轉譯工作的人使用。ENABLE_DRAWIO_PROXY 預設為關閉,啟用後會提供 /proxy endpoint,代表瀏覽器代為擷取外部 image URL,因此除非有需要,請保持關閉。
Excalidraw:不含後端伺服器的靜態 bundle
官方映像檔頁面提供以下指令。
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 會在 port 80 提供編譯後的 JavaScript bundle。發布的 image 壓縮後約為 41 MB(Docker Hub,2026 年 8 月),由此可知其中的內容非常少。沒有資料庫、session store 或 upload directory,因為伺服器沒有任何需要儲存的內容。
映像檔頁面明確說明了限制:「目前,自行代管的執行個體不支援分享或協作功能。」介面中仍保留這些按鈕,因此了解原因很重要。即時協作需要 websocket server,另外以 excalidraw/excalidraw-room 發布。分享連結需要 storage service 儲存加密後的場景。這兩者的位址會在建置時,透過 Vite 變數(VITE_APP_WS_SERVER_URL、VITE_APP_BACKEND_V2_GET_URL、VITE_APP_BACKEND_V2_POST_URL)編譯至 bundle 中,而 repository 內的 production 值指向 Excalidraw 自行代管的服務。Vite 會在建置期間代換這些值,因此它們最後會成為 JavaScript 內的字串常值。在 container environment variables 中設定這些值不會產生任何作用,因為沒有程式碼在執行階段讀取它們。若要將協作功能指向自己的 room server,就必須使用自訂值從 source 建置 frontend。在規劃前,先確認該 server 的狀態:截至 2026 年 8 月,Docker Hub 上的 excalidraw/excalidraw-room image 已超過兩年未重新建置。
實際儲存繪圖的位置。 場景位於瀏覽器的 local storage 中,儲存在該裝置、該 origin 的範圍內。在 private window 中開啟相同 URL,canvas 會是空的,這是最快的自行驗證方式。清除 site data 會刪除繪圖,而且沒有伺服器副本可供還原。因此,應教導使用者使用「Save to...」,並將 JSON 格式的 .excalidraw 檔案儲存在會進行備份的位置。共用的 instance 會讓每個人擁有自己的私人 canvas。請將它視為一個碰巧代管在伺服器上的個人草稿本。
Kroki:以程式碼描述圖表,並在伺服器上轉譯
Kroki 是位於多個轉譯器前方的單一 HTTP gateway。您以 POST 傳送文字,再取得 SVG 或 PNG。Graphviz、PlantUML、D2 及其他數個轉譯器已內建於 gateway image。Mermaid、BPMN 與 Excalidraw 轉譯器則執行於 companion containers,因此使用 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 不會向主機發布任何連接埠,因此 companion containers 只能從 Compose network 內的 gateway 存取。這正是所需的行為。除非呼叫它的 wiki 執行於不同主機,否則請將 gateway 那一行修改為 "127.0.0.1:8000:8000"。如果您尚未在伺服器上撰寫過 Compose 檔案,在 VPS 上執行 Docker Compose 會說明檔案配置與 docker compose up -d 週期。
請依序執行兩項 smoke test,因為它們失敗的原因不同。
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 60Graphviz 在 gateway 內執行,因此此處的 SVG 文件可證明 gateway 本身運作正常。現在測試會跨越 containers 的路徑。
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 已解析,且 companion 已回應。如果第一個測試成功而第二個失敗,故障位於兩個 containers 之間,因此請先查看 docker compose logs kroki,不要先處理圖表語法。
GET 形式會將圖表編碼至 URL,wiki 因此無須任何 plugin 即可嵌入影像。文件提供了這個 encoder。
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> tag 都可以指向該 URL。這有長度上限:KROKI_MAX_URI_LENGTH 預設為 4096 bytes,因此較長的圖表必須透過 POST 傳送。
Kroki 讀取您傳送給它的文字,因此真正重要的是它的安全性設定。 KROKI_SAFE_MODE 預設為 SECURE,這是三個層級中限制最嚴格的一個;KROKI_PLANTUML_ALLOW_INCLUDE 預設為 false。這些預設值存在的原因,是 PlantUML 的 !include directive 會從轉譯器的角度讀取檔案與 URL。在任何人都能存取的 endpoint 上放寬這些設定,就等於把執行於 container 內的檔案讀取器交給網際網路。除非您確定需要哪個 include path,否則請維持原設定;必要時再使用 KROKI_PLANTUML_INCLUDE_PATH 指定該路徑。
Memory:小型 VPS 上哪個服務最耗資源
了解每個容器執行的內容後,耗用順序就很明顯。
- Excalidraw image 是由 nginx 提供靜態檔案服務,三者相比耗用資源低很多。
- draw.io 執行 Tomcat,也就是 Java 應用程式伺服器,因此無論是否有人繪圖,都會載入 JVM (Java virtual machine)。
- Kroki gateway 也是 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 也是可行的做法,因為 gateway 仍會提供其中內建的所有轉譯器。
這些服務都沒有內建使用者模型,因此要在前面加上驗證層
draw.io 沒有帳號功能。Excalidraw 也沒有帳號功能。Kroki 會回應所有送達的請求。登入功能必須由 proxy 提供。
sudo apt update && sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd alicehtpasswd -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 會保留舊設定繼續執行,因此網站仍可運作,但變更不會生效。逐行說明 reverse proxy 設定涵蓋此片段省略的標頭區塊與憑證路徑。
Basic authentication 不適合 Kroki,原因值得了解。wiki 頁面會使用 <img> 標籤嵌入 Kroki 圖片。讀者的瀏覽器會將該 URL 作為子資源擷取,且不會將你的認證資訊傳送至不同的 origin,因此請求會收到 401,頁面上的每個圖表都會顯示為損毀圖片。請改為避免讓 Kroki 暴露在公開網際網路上。將它放在與 wiki container 相同的 Docker network,讓 wiki 透過 service name 連線,完全不要將任何連接埠發布至 host。Compose network 如何解析 service name說明了這種配置的運作方式。
自架 Wiki 旁的圖表
這通常是人們需要這類工具的原因。Wiki 頁面需要圖片,而沒有人希望該圖片只是某人筆電上的螢幕擷取畫面。
BookStack 原生支援自架編輯器。其預設嵌入 URL 為 https://embed.diagrams.net/?embed=1&proto=json&spin=1&configure=1,在 .env 中加入一行即可將其移至你的 container。
DRAWIO=https://drawio.example.com/?embed=1&proto=json&spin=1&configure=1請完整複製 query string。BookStack 文件指出,embed=1&proto=json&spin=1「是讓 BookStack 整合正常運作所必需的」,因為這些參數會選取兩個頁面用來互相通訊的 JSON 訊息協定。同一頁也建議使用 stealth=1,「如果你不希望使用其他外部服務」,也就是當自架的目的包含停止對外連線時,應加入的選項。完成設定後,BookStack 會將圖形儲存到自己的圖片儲存區,並放在頁面旁,因此你原本執行的 Wiki 備份也會一併備份圖表。
如果尚未決定使用哪個 Wiki,請先處理這項決定。選擇 BookStack、Wiki.js 與 Outline 是前一個決策,因為 Wiki 會決定圖表如何附加到頁面,也會決定你要搭配哪一個工具。
失敗模式與你會看到的字串
繪圖編輯器在 BookStack 中開啟後持續轉圈。 轉圈圖示表示 spin=1 正在等待永遠不會到達的 handshake。確認 embed=1&proto=json&spin=1 存在於你的 DRAWIO 值中,且主機部分沒有拼字錯誤。
編輯器框架在 HTTPS wiki 上維持空白。 瀏覽器主控台回報 mixed content,正在 https:// 中載入 http://。瀏覽器會封鎖此框架,因此 draw.io 永遠不會執行。請透過 HTTPS 提供編輯器。
Kroki 回傳 413 Request Entity Too Large。 此字串來自 nginx,而不是 Kroki。nginx 的 client_max_body_size 預設值為 1 MB,Kroki 自身的 KROKI_MAX_BODY_SIZE 預設值為 1mb,因此較大的 PlantUML 原始碼會先達到其中較低的限制。請同時調高這兩項設定。
Mermaid 失敗,但 graphviz 可正常運作。 gateway 正常,但沒有連線到 companion。使用 docker compose ps 確認服務正在執行,接著確認 KROKI_MERMAID_HOST 是否符合服務名稱,因為其預設值為 127.0.0.1;在 gateway 容器內,這代表 gateway 本身。
Excalidraw 協作始終無法連線。 如果你針對自有的 room server 建置前端,並將其置於 nginx 後方,proxy 必須使用 proxy_set_header Upgrade $http_upgrade; 和 proxy_set_header Connection "upgrade"; 升級連線。缺少這兩項設定時,websocket handshake 會被當作一般 HTTP 請求處理,工作階段因此永遠不會開始。
瀏覽器清理後畫布變成空白。 該場景儲存在那台裝置的 local storage 中,伺服器上沒有副本。解決方式不是調整設定,而是養成習慣:凡是值得保留的內容,都匯出 .excalidraw 檔案。
FAQ
自架 draw.io 能保護我的圖表隱私嗎?
它會將應用程式程式碼保留在您的伺服器上,但這與保護資料隱私是兩回事。draw.io 會在瀏覽器中進行繪製,因此容器根本不會持有圖表。隱私取決於您將檔案儲存在哪裡,以及保留哪些對外連線。使用 ?offline=1 停用雲端儲存目標,並記住,除非設定 DRAWIO_SELF_CONTAINED=1 並自行執行 jgraph/export-server,否則匯出要求會傳送至匯出伺服器。
為什麼自架的 Excalidraw 無法協作?
官方映像檔頁面指出,自架版本「不支援分享或協作功能」。即時協作需要獨立的 excalidraw/excalidraw-room websocket 伺服器,而分享連結需要儲存服務。兩者的位址都會在建置時,以 VITE_APP_WS_SERVER_URL 等 Vite 變數編譯到 JavaScript bundle 中,因此在執行中的容器設定環境變數不會生效。若要使用自己的 room server,必須以自訂值從原始碼建置前端。
如何在自己的伺服器上轉譯 Mermaid 圖表?
執行 Kroki 及其 mermaid companion container,並將 KROKI_MERMAID_HOST 設為該服務名稱。接著將圖表文字 POST 至 /mermaid/svg,再從回應中讀取 SVG;或者將圖表編碼至 GET URL,並將 <img> 標籤指向該 URL。由於 Mermaid 需要瀏覽器引擎,companion 會透過 Puppeteer 驅動 Chromium,因此必須預留記憶體:KROKI_MERMAID_MAX_CONCURRENCY 預設同時進行 6 次轉譯。
這些工具前面需要設定密碼嗎?
需要,因為這些工具都沒有帳號功能。draw.io 和 Excalidraw 會將完整編輯器提供給任何找到 URL 的人,而 Kroki 會轉譯傳送給它的任何文字。對兩個編輯器而言,在反向代理設定 Basic authentication 即已足夠。對 Kroki 而言,請讓它維持未公開狀態,並放在與 wiki 共用的 Docker network 上,因為讀者瀏覽器發出的 <img> 請求不會將憑證傳送至另一個 origin,導致每個嵌入的圖表都無法載入。