So sánh công cụ vẽ sơ đồ self-hosted trên VPS
So sánh draw.io, Excalidraw và Kroki trên VPS: phần nào không chạm vào server, và khác biệt render này quyết định self-hosting có bảo vệ privacy hay không.
Nên chạy công cụ vẽ sơ đồ self-hosted nào?
Công cụ vẽ sơ đồ self-hosted có 2 dạng, và dạng hoạt động quan trọng hơn danh sách tính năng. draw.io và Excalidraw là các ứng dụng chạy trên browser: container cung cấp JavaScript, browser của bạn thực hiện việc vẽ, còn server không bao giờ thấy sơ đồ. Kroki thì ngược lại. Bạn gửi nội dung sơ đồ dạng text qua HTTP, rồi Kroki gửi lại một image. Vì vậy, mọi sơ đồ đều đi qua máy của bạn.
Chạy draw.io nếu bạn muốn có một trình chỉnh sửa đầy đủ bên cạnh wiki. Chạy Excalidraw nếu bạn muốn một bảng phác thảo nhanh và chấp nhận rằng nó không lưu dữ liệu bên ngoài browser nơi bạn đã vẽ. Chạy Kroki nếu sơ đồ của bạn là text được lưu trong git cùng với code mà sơ đồ mô tả.
Những gì self-hosting một công cụ vẽ sơ đồ thực sự thay đổi
Hãy xác định chính xác phần nào truy cập vào server của bạn, vì điều này quyết định self-hosting giúp bảo vệ quyền riêng tư hay chỉ tăng availability.
- draw.io render trong browser. Container của bạn cung cấp application code. File sẽ được lưu ở nơi bạn chỉ định trong editor.
- Excalidraw render trong browser và lưu scene hiện tại trong local storage của browser đó. Không có dữ liệu nào được ghi ở phía server.
- Kroki render trên server. Source của diagram và image hoàn chỉnh đều tồn tại bên trong container của bạn.
Chỉ trường hợp thứ ba mới đưa dữ liệu lên hardware do bạn kiểm soát. Với hai trường hợp đầu, self-hosting giúp bạn kiểm soát asset và tăng availability: JavaScript được cung cấp từ host của bạn, nên editor vẫn hoạt động khi bên thứ ba gặp outage, thay đổi điều khoản hoặc không thể truy cập từ network của bạn. Với một số team, điều này có giá trị thực tế. Tuy nhiên, đó là một tuyên bố khác với “diagram không bao giờ rời khỏi tòa nhà”.
draw.io: container chính thức không lưu dữ liệu
Dự án phát hành image riêng, và phần khởi động nhanh trong README chỉ có một dòng.
docker run -it --rm --name="draw" -p 8080:8080 -p 8443:8443 jgraph/drawioLệnh này publish editor trên mọi địa chỉ mà máy chủ có. Trên VPS, hãy bind port được publish vào loopback rồi truy cập qua reverse proxy hoặc SSH tunnel.
docker run -d --name drawio --restart unless-stopped -p 127.0.0.1:8080:8080 jgraph/drawioMở http://127.0.0.1:8080/?offline=1&https=0 qua tunnel. README gọi ?offline=1 là “tính năng bảo mật vô hiệu hóa hỗ trợ cloud storage”. Nếu không bật tùy chọn này, editor cung cấp Google Drive, OneDrive và GitHub làm đích lưu file. Đây là các server của bên khác.
Bind vào 127.0.0.1 sẽ giữ port đó ngoài Internet công cộng. Một -p 8080:8080 thông thường không bị ufw lọc, vì Docker chèn rule iptables riêng trước các chain do ufw quản lý. Vì vậy firewall có vẻ được cấu hình đúng nhưng port vẫn phản hồi với mọi nơi trên Internet. Docker publish port đi thẳng qua ufw giải thích cơ chế và cách khắc phục.
Hai biến môi trường trở nên quan trọng ngay khi editor không chạy trên 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"Dấu gạch chéo ở cuối không phải lỗi đánh máy. README định nghĩa DRAWIO_SERVER_URL là “URL triển khai public có dấu gạch chéo ở cuối” và DRAWIO_BASE_URL là “cùng URL đó nhưng không có dấu gạch chéo ở cuối”. Viewer, lightbox và các đường dẫn embed code sử dụng hai giá trị này. Nếu bạn phục vụ editor dưới một subpath như https://www.example.com/drawio/, cả hai giá trị phải chứa subpath đó, vì app xây dựng URL viewer và embed từ các giá trị này.
Persistence: không có, và đó là thiết kế của ứng dụng. File Compose đó không có volume vì container không chứa dữ liệu sơ đồ. File .drawio là XML mà editor gửi cho trình duyệt, còn save target bạn chọn quyết định file được lưu ở đâu: được tải xuống máy của bạn hoặc được gửi đến ứng dụng đã embed editor. Hãy backup đích lưu đó. Nếu đích là một thư mục trên VPS, thì thư mục này và file manager bạn dùng để truy cập thư mục đó mới là những thứ cần bảo vệ, vì draw.io không giữ bản sao của bất kỳ dữ liệu nào.
Dữ liệu vẫn rời khỏi server của bạn. Ít hơn trước, vì chức năng export hiện không còn gửi dữ liệu đi đâu nữa. README hiện liệt kê EXPORT_URL và DRAWIO_SELF_CONTAINED trong nhóm biến đã bị loại bỏ và không còn tác dụng. Hai biến này từng trỏ editor đến image jgraph/export-server riêng, nhưng image đó “đã hết vòng đời”. Entrypoint hiện ghi window.EXPORT_URL = null vào PreConfig.js, cho editor biết không có export service. Trên thực tế, Export as > PDF sẽ mở hộp thoại in của trình duyệt. Vì vậy tùy chọn đưa một bản sao của sơ đồ vào file PDF không được cung cấp, đồng thời chức năng export PNG và JPEG render trên server bị ẩn. Chức năng export PNG, JPEG và SVG render trong trình duyệt vẫn hoạt động. ENABLE_DRAWIO_PROXY mặc định tắt và bật một endpoint /proxy để fetch URL image bên ngoài thay cho trình duyệt, vì vậy hãy để tùy chọn này tắt nếu bạn không cần.
Excalidraw: một static bundle không có server phía sau
Trang image chính thức cung cấp command này.
docker run --rm -dit --name excalidraw -p 5000:80 excalidraw/excalidraw:latestChuyển port được publish về loopback vì cùng lý do như trước.
docker run -d --name excalidraw --restart unless-stopped -p 127.0.0.1:5000:80 excalidraw/excalidraw:latestBên trong container, nginx phục vụ một JavaScript bundle đã được compile trên port 80. Image được publish có dung lượng khoảng 41 MB sau khi nén (Docker Hub, tháng 10 năm 2026), cho thấy bên trong có rất ít thành phần. Không có database, session store hoặc thư mục upload, vì server không có gì cần lưu trữ.
Trang image nêu rõ giới hạn này: “Hiện tại, tự host instance của riêng bạn không hỗ trợ tính năng chia sẻ hoặc cộng tác.” Các button vẫn xuất hiện trong giao diện, nên cần hiểu lý do. Tính năng cộng tác thời gian thực cần một websocket server, được publish riêng dưới dạng excalidraw/excalidraw-room. Share link cần một storage service để lưu scene đã được mã hóa. Địa chỉ của cả hai được compile vào bundle trong thời điểm build dưới dạng biến Vite (VITE_APP_WS_SERVER_URL, VITE_APP_BACKEND_V2_GET_URL, VITE_APP_BACKEND_V2_POST_URL), và các giá trị production trong repository trỏ đến những hosted service của Excalidraw. Vite thay thế các giá trị đó trong lúc build, nên chúng trở thành các chuỗi literal bên trong JavaScript. Đặt chúng thành container environment variable không thay đổi gì, vì không có code nào đọc chúng trong runtime. Muốn trỏ tính năng cộng tác đến room server của riêng bạn, bạn phải build frontend từ source với các giá trị riêng. Hãy kiểm tra trạng thái của server đó trước khi dựa vào nó: tính đến tháng 10 năm 2026, lần push mới nhất của image excalidraw/excalidraw-room trên Docker Hub là vào tháng 12 năm 2023.
Bản vẽ thực sự nằm ở đâu. Scene nằm trong local storage của browser, trên thiết bị đó và gắn với origin tương ứng. Mở cùng URL trong private window sẽ thấy canvas trống. Đây là cách nhanh nhất để tự xác nhận điều này. Xóa site data sẽ xóa bản vẽ, và không có bản sao trên server để khôi phục. Vì vậy, hãy hướng dẫn người dùng dùng “Save to...” và giữ file .excalidraw, là file JSON, ở một nơi có backup. Một instance dùng chung vẫn cung cấp cho mỗi người một canvas riêng tư. Hãy xem nó như một bảng phác thảo cá nhân được host trên server.
Kroki: biểu đồ dưới dạng code, render trên server của bạn
Kroki là một HTTP gateway đứng trước nhiều renderer. Bạn POST văn bản và nhận lại SVG hoặc PNG. Graphviz, PlantUML, D2 và một số renderer khác được tích hợp trong gateway image. Mermaid, BPMN và Excalidraw chạy trong các container đi kèm, nên Compose là cách phù hợp để chạy hệ thống này. Đây là ví dụ từ tài liệu 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 không publish gì ra host, nên các container đi kèm chỉ có thể được gateway truy cập trên Compose network. Đó là cấu hình bạn cần. Đổi dòng gateway thành "127.0.0.1:8000:8000", trừ khi wiki gọi gateway chạy trên một host khác. Nếu trước đây bạn chưa viết Compose file trên server, chạy Docker Compose trên VPS giải thích cấu trúc file và chu kỳ docker compose up -d.
Chạy 2 smoke test theo thứ tự này vì mỗi test có thể fail do một nguyên nhân khác nhau.
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 chạy bên trong gateway, nên document SVG ở đây xác nhận chính gateway đang hoạt động bình thường. Bây giờ hãy kiểm tra đường đi qua các container.
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 60SVG từ lệnh thứ 2 xác nhận KROKI_MERMAID_HOST đã được resolve và container đi kèm đã trả lời. Nếu lệnh thứ nhất chạy được nhưng lệnh thứ 2 không chạy, lỗi nằm giữa 2 container, vì vậy hãy đọc docker compose logs kroki trước khi kiểm tra cú pháp diagram.
Dạng GET encode diagram vào URL. Đây là cách wiki nhúng image mà không cần plugin. Tài liệu cung cấp encoder sau.
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'))"Trên Ubuntu, lệnh đó in ra python: command not found vì hệ thống cài python3 nhưng không có python không kèm version. Hãy dùng python3. Output được nối vào cuối URL có dạng /{diagram-type}/{output-format}/{encoded-diagram}, và bất kỳ thẻ <img> nào cũng có thể trỏ đến URL đó. Có giới hạn độ dài: KROKI_MAX_URI_LENGTH mặc định là 4096 bytes, nên diagram dài phải gửi bằng POST.
Kroki đọc nội dung bạn gửi, vì vậy các thiết lập bảo mật của nó mới là phần cần quan tâm. KROKI_SAFE_MODE mặc định là SECURE, mức hạn chế nhất trong 3 mức, còn KROKI_PLANTUML_ALLOW_INCLUDE mặc định là false. Các giá trị mặc định này tồn tại vì directive !include của PlantUML có thể đọc file và URL từ góc nhìn của renderer. Nếu nới lỏng các thiết lập đó trên endpoint mà bất kỳ ai cũng truy cập được, bạn đã cung cấp cho Internet một file reader chạy bên trong container. Giữ nguyên các giá trị này trừ khi bạn biết chính xác include path cần dùng, sau đó khai báo path đó bằng KROKI_PLANTUML_INCLUDE_PATH.
Bộ nhớ: thành phần nào gây tải trên VPS nhỏ
Thứ tự này dễ dự đoán khi bạn biết mỗi container chạy gì.
- Image Excalidraw chỉ dùng nginx để phục vụ file tĩnh. Đây là thành phần ít tốn tài nguyên nhất trong 3 thành phần.
- draw.io chạy Tomcat, một application server viết bằng Java, nên luôn sử dụng một JVM (Java virtual machine), dù có ai đang vẽ hay không.
- Gateway Kroki cũng là một service Java, được đóng gói dưới dạng file jar cho các bản cài đặt thủ công.
- Companion mermaid là thành phần tốn tài nguyên nhất. Dockerfile của nó cài Chromium và đặt
PUPPETEER_EXECUTABLE_PATH=/usr/lib/chromium/chrome, vì Mermaid render bằng một browser engine thực.
Vì vậy, các con số khi idle không cho bạn biết nhiều. Con số quan trọng là mức tăng đột biến khi render một diagram. KROKI_MERMAID_MAX_CONCURRENCY mặc định là 6, nên có thể chạy đồng thời 6 lượt render bằng browser. Hãy đo trên máy của bạn thay vì tin vào một con số được công bố.
docker stats --no-stream
docker system dfChạy lệnh đầu tiên khi mọi thứ đang idle, sau đó chạy lại trong lúc bạn render lặp một mermaid diagram lớn. Nếu mức tăng đột biến quá cao trên một plan nhỏ, hãy giới hạn nó thay vì đoán: đặt giới hạn bộ nhớ cho một service Compose trình bày cú pháp và điều gì xảy ra khi container chạm giới hạn. Loại bỏ companion mermaid cũng là một lựa chọn hợp lệ, vì gateway vẫn tiếp tục phục vụ mọi renderer được tích hợp sẵn.
Không thành phần nào có mô hình người dùng, nên hãy đặt một lớp proxy phía trước
draw.io không có tài khoản. Excalidraw không có tài khoản. Kroki xử lý mọi request đến nó. Mọi cơ chế đăng nhập phải do proxy cung cấp.
sudo apt update && sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd alicehtpasswd -c tạo file và ghi đè file hiện có, vì vậy hãy truyền -c trong lần đầu tiên và không truyền lại sau đó.
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;
}
}Áp dụng cấu hình bằng sudo nginx -t && sudo systemctl reload nginx. Phần nginx -t mới là phần quan trọng: nếu reload với cấu hình bị lỗi, cấu hình cũ vẫn tiếp tục chạy. Vì vậy website vẫn hoạt động nhưng thay đổi của bạn chưa có hiệu lực. Giải thích cấu hình reverse proxy theo từng dòng trình bày block header và các đường dẫn chứng chỉ bị lược bỏ trong đoạn này.
Basic authentication không phù hợp với Kroki, và cần hiểu rõ lý do. Một trang wiki nhúng hình Kroki bằng thẻ <img>. Trình duyệt của người đọc fetch URL đó như một cross-origin subresource. Trình duyệt sẽ gửi kèm thông tin xác thực Basic đã lưu cho host Kroki, nhưng các trình duyệt hiện nay không hiển thị login prompt cho hình ảnh cross-origin. Vì vậy, bất kỳ người đọc nào chưa đăng nhập trực tiếp vào host Kroki sẽ nhận lỗi 401 và thấy mọi sơ đồ trên trang dưới dạng hình ảnh bị hỏng. Thay vào đó, hãy giữ Kroki ngoài public internet. Đặt Kroki trên cùng Docker network với container wiki và cho wiki truy cập Kroki bằng service name, không publish bất kỳ cổng nào ra host. Cách các network của Compose phân giải service name là phần giúp cách này hoạt động.
Sơ đồ đặt cạnh một wiki tự host
Đây là lý do phổ biến nhất khiến mọi người cần những thứ này. Một trang wiki cần có hình ảnh, và không ai muốn hình đó chỉ là screenshot từ laptop của ai đó.
BookStack có hook tích hợp sẵn cho một editor tự host. URL embed mặc định là https://embed.diagrams.net/?embed=1&proto=json&spin=1&configure=1, và thêm một dòng trong .env sẽ chuyển URL đó đến container của bạn.
DRAWIO=https://drawio.example.com/?embed=1&proto=json&spin=1&configure=1Sao chép query string chính xác. Tài liệu BookStack cho biết embed=1&proto=json&spin=1 “là bắt buộc để integration với BookStack hoạt động”, vì chúng chọn JSON message protocol mà hai trang dùng để trao đổi với nhau. Trang đó cũng đề cập đến stealth=1 “nếu bạn không muốn sử dụng các external service khác”. Đây là option cần thêm khi mục tiêu self-hosting là chặn các outbound call. Sau khi kết nối xong, BookStack lưu bản vẽ vào image storage riêng, ngay cạnh trang wiki. Vì vậy, bản backup wiki bạn vẫn tạo cũng bao gồm bản backup của sơ đồ.
Nếu chưa quyết định dùng wiki nào, hãy giải quyết việc đó trước. Chọn giữa BookStack, Wiki.js và Outline là quyết định cần thực hiện trước, vì wiki quyết định cách sơ đồ được gắn vào trang và do đó quyết định bạn sẽ tích hợp công cụ nào trong số này.
Các tình huống lỗi và chuỗi bạn sẽ thấy
Trình chỉnh sửa sơ đồ mở trong BookStack nhưng quay vô hạn. Vòng quay đang spin=1 chờ dữ liệu sơ đồ. Với proto=json, trình chỉnh sửa gửi một sự kiện init khi đã sẵn sàng và chờ một action load chứa sơ đồ được gửi lại. Vì vậy, vòng quay vẫn hiển thị khi quá trình trao đổi này không hoàn tất. Kiểm tra embed=1&proto=json&spin=1 có trong giá trị DRAWIO và phần host không bị gõ sai.
Khung trình chỉnh sửa vẫn trống trên wiki dùng HTTPS. Console của trình duyệt báo mixed content khi tải http:// bên trong https://. Trình duyệt chặn khung này, nên draw.io không chạy. Hãy cung cấp trình chỉnh sửa qua HTTPS.
Kroki trả về 413 Request Entity Too Large. Đây là cách nginx ghi tên lỗi đó, vì vậy proxy là nơi cần kiểm tra đầu tiên. Tuy nhiên, Kroki cũng có thể tự gửi lỗi 413; tài liệu của Kroki gọi lỗi này là Content Too Large. Giá trị mặc định của nginx client_max_body_size là 1 MB. Đây là giới hạn mà source PlantUML lớn có thể chạm tới, vì PlantUML được render bên trong gateway. KROKI_MAX_BODY_SIZE của Kroki cũng mặc định là 1mb, nhưng biến này chỉ có trên các container companion của diagrams.net, Excalidraw và Mermaid. Vì vậy, source Mermaid lớn có thể chạm một trong hai giới hạn. Trước tiên, hãy tăng client_max_body_size. Khi sơ đồ đi qua một companion, hãy tăng cả KROKI_MAX_BODY_SIZE trên companion đó.
Mermaid lỗi nhưng graphviz vẫn hoạt động. Gateway đang hoạt động bình thường nhưng chưa kết nối đến companion. Kiểm tra service đang chạy bằng docker compose ps. Sau đó kiểm tra KROKI_MERMAID_HOST có khớp với tên service không, vì giá trị mặc định là 127.0.0.1. Bên trong gateway container, giá trị này có nghĩa là chính gateway.
Tính năng cộng tác của Excalidraw không bao giờ kết nối được. Nếu bạn xây frontend kết nối đến room server riêng và đặt frontend phía sau nginx, proxy phải nâng cấp kết nối bằng proxy_set_header Upgrade $http_upgrade; và proxy_set_header Connection "upgrade";. Nếu thiếu chúng, websocket handshake sẽ được xử lý như một HTTP request thông thường và session không bao giờ bắt đầu.
Canvas trống sau khi dọn dẹp dữ liệu trình duyệt. Scene được lưu trong local storage trên thiết bị đó và không có bản sao trên server. Cách khắc phục là tạo thói quen thay vì đổi setting: export file .excalidraw cho mọi nội dung cần giữ.
FAQ
Việc tự host draw.io có giữ cho sơ đồ của tôi riêng tư không?
Việc này giữ mã ứng dụng trên server của bạn, nhưng đó là chuyện khác với việc giữ dữ liệu riêng tư. draw.io render trong trình duyệt, nên container hoàn toàn không lưu sơ đồ. Quyền riêng tư khi đó phụ thuộc vào nơi bạn lưu file và những kết nối outbound nào bạn để bật. Dùng ?offline=1 để tắt các đích lưu trữ cloud và để ENABLE_DRAWIO_PROXY ở trạng thái tắt. Export cũng không gửi dữ liệu ra ngoài: image không cấu hình export service nào, nên PDF đi qua hộp thoại in của trình duyệt, và DRAWIO_SELF_CONTAINED không còn tác dụng.
Vì sao tính năng cộng tác không hoạt động trên Excalidraw tự host?
Trang image chính thức nêu rõ self-hosting "không hỗ trợ các tính năng chia sẻ hoặc cộng tác". Cộng tác realtime cần websocket server riêng excalidraw/excalidraw-room, còn share link cần một storage service. Địa chỉ của cả hai được biên dịch vào JavaScript bundle tại thời điểm build dưới dạng các biến Vite như VITE_APP_WS_SERVER_URL, nên đặt biến môi trường trên container đang chạy sẽ không có tác dụng. Nếu dùng room server của riêng bạn, bạn phải build frontend từ source với các giá trị tương ứng.
Làm cách nào để render sơ đồ Mermaid trên server của riêng tôi?
Chạy Kroki cùng container companion cho mermaid và đặt KROKI_MERMAID_HOST thành tên service đó. Sau đó POST nội dung sơ đồ đến /mermaid/svg rồi đọc SVG từ response, hoặc encode sơ đồ vào URL GET và trỏ một tag <img> đến URL đó. Companion điều khiển Chromium qua Puppeteer vì Mermaid cần browser engine, nên phải tính đến lượng memory: KROKI_MERMAID_MAX_CONCURRENCY mặc định cho phép 6 lần render đồng thời.
Tôi có cần đặt password phía trước các tool này không?
Có, vì không tool nào trong số này có account. draw.io và Excalidraw cung cấp full editor cho bất kỳ ai tìm được URL, còn Kroki render mọi text được gửi đến. Basic authentication tại reverse proxy là đủ cho hai editor. Với Kroki, hãy giữ service này ở trạng thái không public trên một Docker network dùng chung với wiki, vì trình duyệt của người đọc sẽ không hiển thị login prompt cho một <img> trên origin khác; nếu không, mọi sơ đồ được nhúng sẽ bị lỗi với những người chưa đăng nhập trực tiếp vào Kroki.