draw.io, Excalidraw ou Kroki: qual hospedar?
Compare draw.io, Excalidraw e Kroki no seu VPS e veja o que realmente chega ao servidor: os dois primeiros desenham no browser; Kroki recebe o texto por HTTP.
Que ferramenta de diagramas self-hosted deve executar?
As ferramentas de diagramas self-hosted têm dois modelos, e o modelo importa mais do que a lista de funcionalidades. draw.io e Excalidraw são aplicações de browser: o container fornece JavaScript, o browser faz o desenho e o servidor nunca vê o diagrama. Kroki funciona de forma oposta. Envia o texto do diagrama por HTTP e recebe uma imagem em resposta. Assim, todos os diagramas passam pela sua própria máquina.
Execute draw.io se quiser um editor completo junto de uma wiki. Execute Excalidraw se quiser uma ferramenta rápida para esboços e aceitar que nada seja guardado fora do browser onde fez o desenho. Execute Kroki se os seus diagramas forem texto armazenado no git junto do código que descrevem.
O que o self-hosting de uma ferramenta de diagramas altera na prática
Seja exato sobre quais partes comunicam com o seu servidor, porque esse facto determina se o self-hosting proporciona privacidade ou apenas disponibilidade.
- draw.io é renderizado no navegador. O seu container fornece o código da aplicação. O ficheiro é guardado no local que indicar ao editor.
- Excalidraw é renderizado no navegador e mantém a cena atual no armazenamento local desse navegador. Nada é escrito no servidor.
- Kroki é renderizado no servidor. O código-fonte do diagrama e a imagem final existem dentro do seu container.
Apenas o terceiro caso transfere dados para hardware que controla. Nos dois primeiros, o self-hosting proporciona controlo dos recursos e disponibilidade: o JavaScript vem do seu host, por isso o editor continua a funcionar quando um terceiro sofre uma indisponibilidade, altera os seus termos ou deixa de estar acessível a partir da sua rede. Isto tem valor real para algumas equipas. É uma afirmação diferente de "o diagrama nunca sai das instalações".
draw.io: um contentor oficial que não armazena nada
O projeto publica a sua própria imagem, e o início rápido no respetivo README é composto por uma linha.
docker run -it --rm --name="draw" -p 8080:8080 -p 8443:8443 jgraph/drawioIsto publica o editor em todos os endereços que o servidor tiver. Numa VPS, associe a porta publicada ao loopback e aceda-lhe através de um reverse proxy ou de um túnel SSH.
docker run -d --name drawio --restart unless-stopped -p 127.0.0.1:8080:8080 jgraph/drawioAbra http://127.0.0.1:8080/?offline=1&https=0 através do túnel. O README chama a ?offline=1 «uma funcionalidade de segurança que desativa o suporte para armazenamento na cloud». Sem esta opção, o editor disponibiliza Google Drive, OneDrive e GitHub como destinos de gravação, que são servidores de terceiros.
A associação a 127.0.0.1 é o que mantém essa porta fora da Internet pública. Um -p 8080:8080 simples não é filtrado pelo ufw, porque o Docker insere as suas próprias regras do iptables antes das cadeias geridas pelo ufw. Assim, o firewall parece estar corretamente configurado, mas a porta responde ao mundo. O Docker publica diretamente através do ufw explica o mecanismo e a correção.
Duas variáveis de ambiente são importantes assim que o editor deixa de estar no 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"A barra final não é um erro de digitação. O README define DRAWIO_SERVER_URL como o «URL da implementação pública com uma barra final» e DRAWIO_BASE_URL como o «mesmo URL sem uma barra final», utilizado pelo visualizador, pelo lightbox e pelos caminhos de código de incorporação. Se servir o editor num subcaminho como https://www.example.com/drawio/, ambos os valores têm de incluir esse subcaminho, porque a aplicação constrói os URLs do visualizador e da incorporação a partir deles.
Persistência: não existe, e isso faz parte do desenho. Não há nenhum volume nesse ficheiro Compose, porque o contentor não contém dados de diagramas. Um ficheiro .drawio é XML que o editor entrega ao browser, e o destino de gravação escolhido determina onde fica: uma transferência para a sua própria máquina ou a aplicação que incorporou o editor. Faça uma cópia de segurança desse destino. Se o destino for uma pasta na VPS, então essa pasta é o que deve proteger e o gestor de ficheiros que utiliza para lhe aceder, porque o draw.io não mantém nenhuma cópia.
O que ainda sai do seu servidor. Menos do que anteriormente, porque a exportação já não é enviada para nenhum serviço. O README lista agora EXPORT_URL e DRAWIO_SELF_CONTAINED entre as variáveis removidas que não têm efeito. Essas variáveis apontavam o editor para a imagem separada jgraph/export-server, que «chegou ao fim de vida». O entrypoint escreve agora window.EXPORT_URL = null em PreConfig.js, indicando ao editor que não existe nenhum serviço de exportação. Na prática, Export as > PDF abre a caixa de diálogo de impressão do browser. Por isso, a opção de incluir uma cópia do diagrama não é apresentada para PDF, e a exportação de PNG e JPEG renderizada no servidor fica oculta. A exportação de PNG, JPEG e SVG renderizada no browser continua a funcionar. ENABLE_DRAWIO_PROXY está desativada por predefinição e ativa um endpoint /proxy que obtém URLs de imagens externas em nome do browser. Mantenha-a desativada, exceto se precisar dela.
Excalidraw: um bundle estático sem servidor por trás
A página oficial da imagem fornece este comando.
docker run --rm -dit --name excalidraw -p 5000:80 excalidraw/excalidraw:latestMova a porta publicada para a interface loopback pelo mesmo motivo de antes.
docker run -d --name excalidraw --restart unless-stopped -p 127.0.0.1:5000:80 excalidraw/excalidraw:latestDentro do contentor, o nginx serve um bundle JavaScript compilado na porta 80. A imagem publicada tem cerca de 41 MB comprimidos (Docker Hub, outubro de 2026), o que mostra como é pequeno o seu conteúdo. Não há base de dados, armazenamento de sessões nem diretório de uploads, porque não existe nada para armazenar no servidor.
A página da imagem indica claramente a limitação: "Atualmente, alojar a sua própria instância não permite utilizar funcionalidades de partilha ou colaboração." Os botões continuam presentes na interface, por isso é importante compreender o motivo. A colaboração em tempo real precisa de um servidor websocket, publicado separadamente como excalidraw/excalidraw-room. Uma ligação de partilha precisa de um serviço de armazenamento para guardar a cena cifrada. Os endereços de ambos são compilados no bundle durante o build como variáveis do Vite (VITE_APP_WS_SERVER_URL, VITE_APP_BACKEND_V2_GET_URL, VITE_APP_BACKEND_V2_POST_URL), e os valores de produção no repositório apontam para os serviços alojados pela própria Excalidraw. O Vite substitui esses valores durante o build, por isso acabam como strings literais dentro do JavaScript. Definir essas variáveis como variáveis de ambiente do contentor não altera nada, porque nenhum código as lê em runtime. Apontar a colaboração para o seu próprio servidor de salas implica compilar o frontend a partir do código-fonte com os seus próprios valores. Verifique o estado desse servidor antes de basear o seu plano nele: em outubro de 2026, o push mais recente da imagem excalidraw/excalidraw-room no Docker Hub tinha sido feito em dezembro de 2023.
Onde um desenho fica realmente. A cena fica no armazenamento local do browser, nesse dispositivo e para essa origem. Abra o mesmo URL numa janela privada e a tela estará vazia. Esta é a forma mais rápida de confirmar o comportamento. Limpar os dados do site elimina o desenho, e não existe uma cópia no servidor que possa ser restaurada. Ensine, portanto, os utilizadores a usar "Guardar em..." e a manter o ficheiro .excalidraw, que está em JSON, num local incluído nos backups. Uma instância partilhada dá a cada pessoa a sua própria tela privada. Trate-a como um bloco de rascunho pessoal que, por acaso, está alojado.
Kroki: diagramas como código, renderizados no seu servidor
Kroki é um gateway HTTP à frente de vários renderizadores. Envia texto com POST e recebe SVG ou PNG. Graphviz, PlantUML, D2 e vários outros renderizadores estão incluídos na imagem do gateway. O renderizador de Mermaid, BPMN e Excalidraw é executado em contentores complementares, por isso Compose é a forma adequada de o executar. Este é o exemplo da documentação do 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 não publica nada no host, por isso os complementos só podem ser alcançados pelo gateway na rede do Compose. É isso que pretende. Altere a linha do gateway para "127.0.0.1:8000:8000", a menos que o wiki que o chama seja executado noutro host. Se ainda não criou um ficheiro Compose num servidor, executar Docker Compose numa VPS explica a estrutura dos ficheiros e o ciclo docker compose up -d.
Execute dois testes básicos, por esta ordem, porque falham por motivos diferentes.
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 é executado dentro do gateway, por isso um documento SVG aqui confirma que o próprio gateway está saudável. Agora teste o caminho que atravessa os contentores.
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 60O SVG do segundo comando confirma que KROKI_MERMAID_HOST foi resolvido e que o complemento respondeu. Se o primeiro comando funcionar e o segundo não, o problema está entre os dois contentores. Consulte docker compose logs kroki antes de alterar a sintaxe do diagrama.
O formato GET codifica o diagrama no URL. É assim que um wiki incorpora uma imagem sem qualquer plugin. A documentação fornece este codificador.
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'))"No Ubuntu, isso imprime python: command not found, porque o sistema fornece python3 e não fornece python sem versão. Use python3. O resultado é colocado no final de um URL com o formato /{diagram-type}/{output-format}/{encoded-diagram}, e qualquer etiqueta <img> pode apontar para esse URL. Existe um limite: KROKI_MAX_URI_LENGTH tem o valor predefinido de 4096 bytes, por isso um diagrama longo tem de ser enviado com POST.
O Kroki lê o texto que lhe envia, por isso as definições de segurança são as que importam. KROKI_SAFE_MODE tem o valor predefinido SECURE, o mais restritivo dos três níveis, e KROKI_PLANTUML_ALLOW_INCLUDE tem o valor predefinido false. Estes valores predefinidos existem porque a diretiva !include do PlantUML lê ficheiros e URLs do ponto de vista do renderizador. Se os flexibilizar num endpoint acessível a qualquer pessoa, estará a disponibilizar na Internet um leitor de ficheiros executado dentro do seu contentor. Mantenha-os inalterados, a menos que saiba de que caminho de inclusão precisa. Nesse caso, especifique-o com KROKI_PLANTUML_INCLUDE_PATH.
Memória: qual é o impacto num VPS pequeno
A ordem é previsível quando se sabe o que cada contentor executa.
- A imagem do Excalidraw é o nginx a servir ficheiros estáticos. É, de longe, a opção mais económica das três.
- O draw.io executa o Tomcat, um servidor de aplicações Java, por isso mantém uma JVM (Java virtual machine) mesmo quando ninguém está a desenhar.
- O gateway Kroki também é um serviço Java, distribuído como um jar para instalações manuais.
- O companion do mermaid é o mais dispendioso. O respetivo Dockerfile instala o Chromium e define
PUPPETEER_EXECUTABLE_PATH=/usr/lib/chromium/chrome, porque o Mermaid faz a renderização num motor de browser real.
Por isso, os valores em repouso dizem muito pouco. O valor importante é o pico enquanto um diagrama é renderizado, e KROKI_MERMAID_MAX_CONCURRENCY tem o valor predefinido 6, portanto podem estar em curso seis renderizações no browser ao mesmo tempo. Meça no seu próprio servidor em vez de confiar num valor publicado.
docker stats --no-stream
docker system dfExecute o primeiro comando quando tudo estiver em repouso e depois novamente enquanto renderiza um diagrama Mermaid grande num ciclo. Se o pico for demasiado elevado num plano pequeno, limite-o em vez de tentar adivinhar: definir limites de memória num serviço Compose mostra a sintaxe e o que acontece quando um contentor atinge o seu limite. Remover o companion do mermaid também é uma opção válida, porque o gateway continua a servir todos os renderizadores integrados.
Nenhum destes serviços fornece um modelo de utilizadores. Por isso, coloque uma camada de autenticação à frente deles.
draw.io não tem contas. Excalidraw não tem contas. Kroki responde a qualquer pedido que lhe chegue. A autenticação tem de ser fornecida pelo proxy.
sudo apt update && sudo apt install -y apache2-utils
sudo htpasswd -c /etc/nginx/.htpasswd alicehtpasswd -c cria o ficheiro e substitui um ficheiro existente. Por isso, passe -c apenas da primeira vez e nunca mais.
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;
}
}Aplique a configuração com sudo nginx -t && sudo systemctl reload nginx. A parte nginx -t é a mais importante: se a configuração estiver inválida, um reload mantém a configuração antiga em execução. Assim, o site continua a funcionar, mas a alteração não fica ativa. A configuração do reverse proxy, explicada linha a linha descreve o bloco de cabeçalhos e os caminhos dos certificados omitidos neste exemplo.
A autenticação básica não é adequada para o Kroki, e vale a pena compreender o motivo. Uma página wiki incorpora uma imagem do Kroki com uma tag <img>. O browser do leitor obtém esse URL como um subrecurso cross-origin. O browser envia as credenciais Basic que já tiver para o host do Kroki, mas os browsers atuais não mostram um pedido de autenticação para uma imagem cross-origin. Por isso, qualquer leitor que não tenha iniciado sessão diretamente no host do Kroki recebe um erro 401 e vê todos os diagramas da página como imagens quebradas. Em vez disso, mantenha o Kroki fora da internet pública. Coloque-o na mesma rede Docker que o contentor da wiki e permita que a wiki o aceda pelo nome do serviço, sem publicar qualquer porta no host. Como as redes Compose resolvem nomes de serviços explica o mecanismo que permite isto.
Diagramas junto de um wiki self-hosted
Esta é a razão habitual para as pessoas quererem esta configuração. Uma página do wiki precisa de uma imagem, e ninguém quer que essa imagem seja uma captura de ecrã do portátil de alguém.
O BookStack tem uma integração nativa com um editor self-hosted. O URL de incorporação predefinido é https://embed.diagrams.net/?embed=1&proto=json&spin=1&configure=1, e uma linha em .env redireciona-o para o seu contentor.
DRAWIO=https://drawio.example.com/?embed=1&proto=json&spin=1&configure=1Copie a cadeia de consulta exatamente. A documentação do BookStack diz que embed=1&proto=json&spin=1 "são necessários para a integração com o BookStack funcionar", porque selecionam o protocolo de mensagens JSON usado pelas duas páginas para comunicarem. A mesma página indica stealth=1 "se não quiser que sejam utilizados outros serviços externos", que é a opção a adicionar quando o objetivo do self-hosting era impedir chamadas de saída. Com esta configuração, o BookStack guarda o desenho no seu próprio armazenamento de imagens, junto da página, pelo que a cópia de segurança do wiki que já faz também inclui a cópia de segurança dos diagramas.
Se ainda não decidiu qual será o wiki, resolva isso primeiro. Escolher entre BookStack, Wiki.js e Outline é a decisão anterior, porque o wiki determina como um diagrama é anexado a uma página e, por isso, quais destas ferramentas deve adicionar.
Modos de falha e as mensagens apresentadas
O editor de desenhos abre no BookStack e fica a carregar indefinidamente. O indicador de carregamento está spin=1 à espera dos dados do diagrama. Com proto=json, o editor envia um evento init quando está pronto e espera uma ação load que transporte o diagrama como resposta. Por isso, o indicador permanece visível quando essa troca não é concluída. Confirme que embed=1&proto=json&spin=1 está presente no valor de DRAWIO e que a parte do host não contém erros de escrita.
A moldura do editor fica em branco numa wiki HTTPS. A consola do navegador indica conteúdo misto ao carregar http:// dentro de https://. O navegador bloqueia a moldura e o draw.io nunca é executado. Disponibilize o editor através de HTTPS.
O Kroki devolve 413 Request Entity Too Large. Essa formulação exata é usada pelo nginx para identificar o erro, por isso o proxy é o primeiro componente a verificar. No entanto, o Kroki também pode enviar um erro 413 próprio, designado Content Too Large na documentação. O valor predefinido de client_max_body_size no nginx é 1 MB. Esse é o limite atingido por uma origem PlantUML grande, porque o PlantUML é processado dentro do gateway. O KROKI_MAX_BODY_SIZE do Kroki também tem o valor predefinido 1mb, mas só existe nos contentores complementares do diagrams.net, Excalidraw e Mermaid. Por isso, uma origem Mermaid grande pode atingir qualquer um dos limites. Aumente primeiro client_max_body_size. Quando o diagrama passar por um contentor complementar, aumente também KROKI_MAX_BODY_SIZE nesse contentor.
O Mermaid falha, mas o graphviz funciona. O gateway está operacional e o contentor complementar não está a ser contactado. Confirme que o serviço está ativo com docker compose ps. Depois, confirme que KROKI_MERMAID_HOST corresponde ao nome do serviço, porque o valor predefinido é 127.0.0.1. Dentro do contentor do gateway, esse valor refere-se ao próprio gateway.
A colaboração do Excalidraw nunca estabelece ligação. Se criou um frontend para o seu próprio servidor de salas e o colocou atrás do nginx, o proxy tem de atualizar a ligação usando proxy_set_header Upgrade $http_upgrade; e proxy_set_header Connection "upgrade";. Sem esses elementos, o handshake websocket é tratado como um pedido HTTP normal e a sessão nunca começa.
A tela fica vazia depois de limpar os dados do navegador. A cena estava armazenada localmente nesse dispositivo e não existe uma cópia no servidor. A correção depende de um procedimento, não de uma definição: exporte o ficheiro .excalidraw de tudo o que precisar de preservar.
FAQ
O self-hosting do draw.io mantém os meus diagramas privados?
Mantém o código da aplicação no seu servidor, mas isso é diferente de manter os dados privados. O draw.io faz a renderização no navegador, por isso o contentor nunca contém um diagrama. A privacidade depende então do local onde guarda o ficheiro e das chamadas de saída que deixa ativas. Use ?offline=1 para desativar os destinos de armazenamento na cloud e mantenha ENABLE_DRAWIO_PROXY desativado. A exportação também não envia nada para fora: a imagem não configura nenhum serviço de exportação, por isso o PDF passa pela caixa de diálogo de impressão do navegador, e DRAWIO_SELF_CONTAINED deixa de ter efeito.
Porque não funciona a colaboração no meu Excalidraw self-hosted?
A página da imagem oficial indica que o self-hosting "não suporta funcionalidades de partilha ou colaboração". A colaboração em tempo real precisa do servidor websocket separado excalidraw/excalidraw-room, e as ligações de partilha precisam de um serviço de armazenamento. Os endereços de ambos são compilados no bundle JavaScript durante a compilação, como variáveis do Vite, por exemplo VITE_APP_WS_SERVER_URL. Por isso, definir uma variável de ambiente no contentor em execução não produz qualquer efeito. Para usar o seu próprio servidor de salas, tem de compilar o frontend a partir do código-fonte com os seus valores.
Como renderizo diagramas Mermaid no meu próprio servidor?
Execute o Kroki com o respetivo contentor companion do mermaid e defina KROKI_MERMAID_HOST com o nome desse serviço. Depois, faça POST do texto do diagrama para /mermaid/svg e leia o SVG da resposta, ou codifique o diagrama num URL GET e aponte uma tag <img> para esse URL. O companion controla o Chromium através do Puppeteer porque o Mermaid precisa de um motor de navegador. Por isso, planeie a utilização de memória: KROKI_MERMAID_MAX_CONCURRENCY tem como valor predefinido seis renderizações em simultâneo.
Preciso de uma palavra-passe à frente destas ferramentas?
Sim, porque nenhuma delas tem contas de utilizador. O draw.io e o Excalidraw disponibilizam um editor completo a qualquer pessoa que encontre o URL, e o Kroki renderiza qualquer texto que lhe seja enviado. A autenticação básica no reverse proxy é suficiente para os dois editores. No caso do Kroki, mantenha-o não publicado numa rede Docker partilhada com o wiki. O navegador do leitor não apresenta um pedido de início de sessão para um <img> noutra origem, por isso todos os diagramas incorporados deixariam de funcionar para quem não tivesse iniciado sessão diretamente no Kroki.