SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-21

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à hướng dẫn quick start trong README chỉ có một dòng.

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

Lệnh này publish editor trên mọi địa chỉ mà máy 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/drawio

Mở http://127.0.0.1:8080/?offline=1&https=0 qua tunnel. README gọi ?offline=1 là “một 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, tức là dữ liệu sẽ được gửi đến server của bên khác.

Bind vào 127.0.0.1 là cách giữ port đó không public trên Internet. Một -p 8080:8080 đơn giản không được ufw lọc, vì Docker chèn rule iptables riêng vào 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 nhận kết nối từ mọi nơi. Docker publish port vượt qua ufw giải thích cơ chế và cách khắc phục.

Có 2 biến môi trường cần thiết 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 dùng các giá trị này. Nếu serve editor dưới một subpath như https://www.example.com/drawio/, cả 2 giá trị phải chứa subpath đó, vì app tạo 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. Compose file đó không có volume vì container không giữ dữ liệu diagram. File .drawio là XML mà editor gửi cho browser, còn đích lưu bạn chọn sẽ quyết định dữ liệu được lưu ở đâu: một file download trên máy của bạn hoặc application đã embed editor. Hãy backup đích lưu đó. Nếu đích là một folder trên VPS, thì folder đó và file manager bạn dùng để truy cập folder mới là 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. Export sang PDF là trường hợp rõ nhất. README mô tả DRAWIO_SELF_CONTAINED là “Đặt thành 1 để định tuyến request export qua ExportProxyServlet của Tomcat (/service/0) thay vì gọi trực tiếp export server”. Hiểu theo chiều ngược lại: theo mặc định, request export không nằm bên trong deployment của bạn. Dự án cũng phát hành jgraph/export-server, một “standalone image-export-server của draw.io”, dành cho những người muốn chạy việc render trên hardware của mình. ENABLE_DRAWIO_PROXY mặc định ở trạng thái tắt và bật một endpoint /proxy để fetch URL image bên ngoài thay cho browser, vì vậy hãy để tùy chọn này tắt trừ khi bạn thực sự cần.

Excalidraw: bundle tĩnh, 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:latest

Chuyển port được publish sang 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:latest

Bên trong container, nginx phục vụ một bundle JavaScript đã biên dịch trên port 80. Image được publish có dung lượng khoảng 41 MB sau khi nén (Docker Hub, tháng 8/2026), cho thấy bên trong có rất ít thành phần. Không có database, session store hay thư mục upload, vì server không có gì để lưu trữ.

Trang image nêu rõ giới hạn này: "Hiện tại, việc self-host instance riêng chưa hỗ trợ các 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. Cộng tác theo 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 đã mã hóa. Địa chỉ của cả hai service được compile vào bundle tại 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), còn các giá trị production trong repository trỏ đến các service do Excalidraw tự host. 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 làm biến môi trường của container không thay đổi gì, vì không có code nào đọc chúng lúc runtime. Muốn trỏ tính năng cộng tác đến room server riêng, bạn phải build frontend từ source với các giá trị của mình. Hãy kiểm tra trạng thái của server đó trước khi lập kế hoạch dựa trên nó: đến tháng 8/2026, image excalidraw/excalidraw-room trên Docker Hub đã không được rebuild trong hơn hai năm.

Một bản vẽ thực sự nằm ở đâu. Scene nằm trong local storage của browser, trên thiết bị đó và thuộc origin đó. Mở cùng URL trong cửa sổ private 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 cấp cho mỗi người một canvas riêng tư. Hãy xem nó như một sketch pad 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 60

Graphviz 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 60

SVG 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 df

Chạ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 dịch vụ nào trong số này có user model, 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 gửi đến nó. 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 alice

htpasswd -c tạo file và ghi đè file hiện có, nên chỉ truyền -c trong lần đầu và không dùng 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à điều quan trọng: reload một cấu hình bị lỗi sẽ giữ cấu hình cũ đang chạy, nên site vẫn hoạt động nhưng thay đổi của bạn chưa được áp dụng. 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 certificate bị lược bỏ trong đoạn cấu hình 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 tag <img>. Trình duyệt của người đọc sẽ fetch URL đó dưới dạng subresource và không gửi credential của bạn đến một origin khác, nên request trả về 401 và mọi diagram trên trang đều hiển thị dưới dạng hình bị hỏng. Thay vào đó, không để Kroki trên Internet công cộng. Đặt Kroki trên cùng Docker network với container wiki và để wiki truy cập bằng service name, hoàn toàn không publish cổng ra host. Cách Compose network phân giải service name là phần giúp mô hình 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=1

Sao 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 lỗi thường gặp và chuỗi bạn sẽ thấy

Trình chỉnh sửa bản vẽ mở trong BookStack rồi quay mãi. Trình spinner đang spin=1 chờ một handshake nhưng handshake không bao giờ đến. Kiểm tra embed=1&proto=json&spin=1 có trong giá trị DRAWIO hay không và phần host không bị gõ sai.

Frame của trình chỉnh sửa vẫn trống trên wiki HTTPS. Console của browser báo mixed content do tải http:// bên trong https://. Browser chặn frame nên draw.io không bao giờ chạy. Hãy cung cấp trình chỉnh sửa qua HTTPS.

Kroki trả về 413 Request Entity Too Large. Chuỗi này đến từ nginx, không phải từ Kroki. Giá trị mặc định của nginx client_max_body_size là 1 MB, còn giá trị mặc định của KROKI_MAX_BODY_SIZE trong Kroki là 1mb, nên source PlantUML lớn sẽ chạm giới hạn nào thấp hơn. Hãy tăng cả hai giới hạn.

Mermaid lỗi trong khi graphviz vẫn hoạt động. Gateway vẫn khỏe nhưng không kết nối được đế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 hay không, vì giá trị mặc định là 127.0.0.1. Bên trong gateway container, giá trị này trỏ về 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 build frontend để dùng room server riêng rồi đặt nó sau nginx, proxy phải nâng cấp connection bằng proxy_set_header Upgrade $http_upgrade;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 browser. Scene chỉ được lưu trong local storage trên thiết bị đó và không có bản sao trên server. Cách xử lý là tạo thói quen thay vì thay đổi setting: export file .excalidraw cho mọi nội dung cần giữ lại.

FAQ

Việc tự host draw.io có giữ 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. Điều đó khác với việc giữ dữ liệu riêng tư. draw.io render trong browser của bạn, nên container không lưu sơ đồ nào. Quyền riêng tư phụ thuộc vào nơi bạn lưu file và các request outbound mà bạn cho phép. Dùng ?offline=1 để tắt các đích lưu trữ cloud. Lưu ý rằng các request export sẽ được gửi đến một export server, trừ khi bạn đặt DRAWIO_SELF_CONTAINED=1 và tự chạy jgraph/export-server.

Vì sao collaboration không hoạt động trên Excalidraw tự host của tôi?

Trang image chính thức nêu rõ rằng self-hosting "không hỗ trợ các tính năng sharing hoặc collaboration". Live collaboration cần websocket server riêng excalidraw/excalidraw-room. Share link cần một storage service. Địa chỉ của cả hai được biên dịch vào JavaScript bundle khi build dưới dạng các biến Vite như VITE_APP_WS_SERVER_URL. Vì vậy, đặt một environment variable 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ị của mình.

Làm thế nào để render sơ đồ Mermaid trên server của tôi?

Chạy Kroki cùng với companion container của 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. Bạn cũng có thể encode sơ đồ vào một GET URL và trỏ một thẻ <img> đến URL đó. Companion điều khiển Chromium thông qua Puppeteer vì Mermaid cần browser engine. Do đó, hãy tính đến mức sử dụng memory: KROKI_MERMAID_MAX_CONCURRENCY mặc định cho phép render 6 sơ đồ cùng lúc.

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. Kroki render mọi nội dung text được gửi đến nó. Basic authentication tại reverse proxy là đủ cho hai editor. Với Kroki, hãy giữ service này không public trên một Docker network dùng chung với wiki. Một request <img> từ browser của người đọc sẽ không gửi credentials đến origin khác. Khi đó, mọi sơ đồ được embed sẽ bị lỗi.

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