Can I self-host Whimsical?
YES · ONE COMMAND— setup effort 1 of 4YES — it's called draw.io. It takes one prompt, a 1024 MB VPS, and about 9 minutes. That is $10 a month you stop paying Whimsical — $120 a year on the Pro plan, 1 seat assumed.
Why people pay for Whimsical
Stated as the vendor would want it stated. A replacement you pick without knowing what the subscription actually buys is a replacement you abandon in a fortnight.
Whimsical sells speed and company. Flowcharts, wireframes, sticky notes, mind maps and docs live in one workspace with one set of shortcuts, several people can be on the same board at the same time, and a colleague can leave a comment on the exact box you got wrong. The paid tiers buy unlimited boards, more guests, longer version history and an admin who can hand a board back after somebody leaves. None of that is the drawing; it is everything around the drawing, and it is genuinely hard to reproduce alone.
| Plan | List price | What it buys |
|---|---|---|
| Free | free | Three team boards, three teams, ten guests, 7-day version history, and 20 AI credits per workspace in total. |
| Prothe plan this page prices against | $10/mo per seat | Shown as $10 per month per editor with annual billing selected, which is the page's default view. Unlimited team boards, six teams, fifty guests, 90-day version history. |
| Business | $20/mo per seat | Shown as $20 per month per editor with annual billing selected. Adds unlimited teams, 200 guests, unlimited version history, SSO/SAML and SCIM, with a contact-sales button next to the price. |
Vendor list prices in USD, read from the pricing page on 2026-08-06 · confidence: medium
Replaced by draw.io
One project, named before the prompt, so you know what you are about to install.
The structured-diagram editor as one container on a hostname you own, with every cloud storage backend switched off.
The closest match to what Whimsical is actually used for: structured diagrams, flowcharts, wireframes and org charts, with shapes that snap and arrows that stay attached when you move a box. It is also the entry on this site with the most honest catch, and we would rather say it than sell around it. draw.io's own hosted editor at app.diagrams.net is free and asks for no account, so self-hosting buys you the origin the page loads from and nothing else: an offline network, an air-gapped site, or a written policy about which servers may serve code to your browser. If none of those is your problem, use the free hosted one.
What else we looked at
Ranked, with the reason each one placed where it did. Only the pick has a prompt on this page — the runners-up are named so the choice is visible, not hidden.
- draw.ioour pickONE COMMAND— setup effort 1 of 4
The structured-diagram editor as one container on a hostname you own, with every cloud storage backend switched off.
The closest match to what Whimsical is actually used for: structured diagrams, flowcharts, wireframes and org charts, with shapes that snap and arrows that stay attached when you move a box. It is also the entry on this site with the most honest catch, and we would rather say it than sell around it. draw.io's own hosted editor at app.diagrams.net is free and asks for no account, so self-hosting buys you the origin the page loads from and nothing else: an offline network, an air-gapped site, or a written policy about which servers may serve code to your browser. If none of those is your problem, use the free hosted one.
- ExcalidrawONE COMMAND— setup effort 1 of 4
A hand-drawn-style whiteboard that runs as one static container and keeps every drawing in the browser that drew it.
The right answer if what you liked about Whimsical was the sketching rather than the structure. Excalidraw is faster for a rough idea and its hand-drawn look invites edits in a way that a clean flowchart does not, but it has no wireframe kit, no mind-map mode and no docs, and the image upstream publishes has no realtime server either. Second place here because Whimsical's centre of gravity is structured diagrams, not freehand.
The swap
You'd run
draw.io
ONE COMMAND · ~9 min to running · 1024 MB RAM
Whimsical Pro · 1 seat assumed · vendor list price · checked 2026-08-06 · source · confidence: medium
Before you start
- RAM floor
- 1024 MBfloor from upstream docs — not measured by us yet
- Disk
- 5 GBthe app, its data, and room for one backup
- Domain needed
- yes, one A recorda hostname pointed at the box before you start — TLS needs it on the cloud path, and the local path needs none
- Time budget
- ~9 minunder 10 minutes, through the first backup
The prompt
Two paths to the same draw.io: the cloud one assumes Prompt Zero is done on a server you rent, the local one assumes nothing but a computer that can run Docker Desktop. Read whichever you pick before you paste it, which is the whole reason both are on the page instead of behind a download.
Where it runs
308 lines · 14,484 bytes
What this prompt will do
- Preflight
- Layout
- Secrets
- compose.yml
- Caddy and TLS
- Firewall
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's machine. The user has completed Prompt Zero: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny.
Run every command in this prompt on the server over `ssh vps` unless the step says otherwise.
Install draw.io 31.1.8 on that server, reachable at https://<DOMAIN>, behind the existing
Caddy with automatic TLS.
## 1. Preflight
If `<DOMAIN>` is still literal, ask the user for the hostname once and stop until they
answer. The A record for it must already point at this server.
Say this to the user before anything installs, because it is the reason to stop and think
rather than the reason to keep going. draw.io already runs a hosted editor at
https://app.diagrams.net that costs nothing and asks for no account: their own home page
says "No account required. No credit card." What this install buys is not the editor, it
is where the editor comes from. The page loads from a hostname the user controls, on a
machine they control, with no third-party origin serving the code, which is what an
offline network, an air-gapped site or a written company policy actually needs. If none of
those three describe the user, tell them plainly that the free hosted editor does the same
job and let them decide before step 2.
draw.io needs 1024 MB of RAM available and 5 GB free on /srv. It is a Tomcat on a JVM, and
the image publishes amd64 and arm64. Measure all four first:
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
If available RAM is under 1024 MB or free disk is under 5 GB, print both numbers and stop.
Do not install and hope. If `dig +short` prints nothing the A record does not exist yet, so
print that and stop: Caddy cannot get a certificate for a hostname that does not resolve.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/drawio /srv/drawio/backups
ls -la /srv/drawio
```
Assert: `ls -la` shows `backups` owned by the login user. There is no `data` directory and
there is nothing to create one for. The container writes no diagram anywhere on this
server, so the only files under /srv/drawio are the two this prompt writes and the archive
step 8 makes of them.
## 3. Secrets
One secret, and it is not a login. draw.io has no accounts, so there is nothing to sign in
to and no admin page to protect. What this generates is `KEYSTORE_PASS`: the image builds a
self-signed certificate for its own port 8443 at every start, and when that variable is
unset it uses a password printed in its own public documentation. Port 8443 is never
published by this install, so nobody outside the container can reach that certificate, and
a documented default is still a documented default. Generate the value, do not print it, do
not repeat it in your summary, and do not put it in any log line.
```bash
umask 077
cat > /srv/drawio/.env <<EOF
DRAWIO_SERVER_URL=https://<DOMAIN>/
KEYSTORE_PASS=$(openssl rand -hex 32)
EOF
chmod 600 /srv/drawio/.env
umask 022
ls -l /srv/drawio/.env
```
Assert: the file exists with mode `-rw-------`. `DRAWIO_SERVER_URL` is upstream's variable
for the public deployment URL and it wants the trailing slash, which is why the line ends
in one. Tell the user this file holds no credential they will ever be asked to type.
## 4. compose.yml
```bash
cat > /srv/drawio/compose.yml <<'EOF'
# draw.io · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
# image, ports, env vars .. https://github.com/jgraph/docker-drawio/blob/v31.1.8/README.md
# entrypoint behaviour .... https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/docker-entrypoint.sh
# image build ............. https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/Dockerfile
#
# One container: Tomcat 9 on JDK 11 serving the compiled draw.io editor at the
# root path on container port 8080, as a non-root tomcat user. There is no
# database and no account system, and this file mounts no volume, because the
# server holds no diagram. A diagram is written to the reader's own device or
# into the storage of the browser that drew it.
#
# The entrypoint switches every cloud backend off when its credentials are
# absent: with no DRAWIO_GOOGLE_CLIENT_ID, DRAWIO_MSGRAPH_CLIENT_ID or
# DRAWIO_GITLAB_ID it writes gapi, od and gl to 0, and it always writes db, gh
# and tr to 0. This file sets none of them, so none of them are on.
#
# KEYSTORE_PASS arrives from /srv/drawio/.env. Left alone, the image falls back
# to a keystore password printed in its own documentation and puts it on a
# self-signed certificate it regenerates at every start. Container port 8443 is
# never published here, so nothing outside can reach that certificate; the
# generated value is what keeps a documented default from standing behind it.
#
# Tag and digest read from Docker Hub on 2026-08-06; the manifest list covers
# linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
drawio:
image: jgraph/drawio:31.1.8@sha256:0c8910ea14dfbccb17c784ee17d995317a8d753479f5ec0f21b2ab2213153100
container_name: drawio
restart: unless-stopped
env_file: /srv/drawio/.env
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/ >/dev/null || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
ports:
# Loopback only: the host's Caddy is the only thing that reaches 8158.
- "127.0.0.1:8158:8080"
EOF
cd /srv/drawio && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. The container serves on 8080 inside itself and 8158 on
this host is bound to 127.0.0.1, so Caddy is the only route in.
## 5. Caddy and TLS
Append the site block below to the Caddyfile Prompt Zero installed, with `<DOMAIN>`
replaced by the real hostname. Copy the file first, because a syntax error here takes down
every other site on the box.
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-drawio
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# draw.io · the Caddy site block for this service.
#
# Authored by caniselfhostit from https://caddyserver.com/docs/automatic-https
# and https://github.com/jgraph/docker-drawio/blob/v31.1.8/README.md
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname whose A record already points at this
# box. Caddy asks for the certificate on the first request and renews it on its
# own, so there is nothing to schedule. The container builds a self-signed
# certificate for its own port 8443 at every start; that port is not published,
# and Caddy is the only thing terminating TLS in front of this service.
<DOMAIN> {
# The editor is several megabytes of JavaScript on a cold load, so
# compression is the one setting here that changes what the user feels.
encode zstd gzip
# No Content-Security-Policy line on purpose. The container injects its own
# CSP as a meta tag at start-up and a second policy sent as a header would
# intersect with it, which fails as a blank editor rather than as an error.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# 8158 is the loopback port compose publishes on this host. It is not a
# container port and it is not open in the firewall.
reverse_proxy 127.0.0.1:8158
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
Assert: `caddy validate` exits 0 and the reload exits 0. If validate fails, restore
/etc/caddy/Caddyfile.before-drawio, reload, and report what it objected to. Caddy requests
the certificate on the first request to the hostname and renews it without a cron job.
## 6. Firewall
Two ports open, both of them Caddy's, and neither 8158 nor 8443 is one of them. These
commands are idempotent, so on a box Prompt Zero already configured they change nothing:
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
80/tcp redirects to HTTPS and answers the ACME challenge, 443/tcp is the only way in, and
443/udp is HTTP/3, which Caddy offers by default. 8158 stays closed because it is bound to
127.0.0.1, and 8443 stays closed because compose never publishes it: the container's own
TLS port has no host port a rule could apply to. Assert: `ufw status verbose` prints
`Status: active`, shows 80, 443/tcp and 443/udp, and shows no rule for 8158 or 8443.
## 7. Start and verify
Tomcat rewrites the editor's configuration files from the environment at every start, so
the first boot is slower than the ones after it.
```bash
cd /srv/drawio
docker compose pull
docker compose up -d
for i in $(seq 1 20); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 10; done
curl -sS https://<DOMAIN>/ | grep -c 'Flowchart Maker'
curl -sS https://<DOMAIN>/js/PreConfig.js | grep -cF "urlParams['gapi'] = '0'"
curl -sS https://<DOMAIN>/js/PreConfig.js | grep -cF "window.DRAWIO_SERVER_URL = 'https://<DOMAIN>/'"
```
Assert, all four, and print what you received for each. The loop ends printing `200`. The
second command prints `1`, because `Flowchart Maker` is in the title of the served
document. The third prints `1`, which is the security assert in this block: it proves the
container wrote `gapi` off, so the Google Drive backend is not offered to anyone who loads
this page. The fourth prints `1`, which proves the `.env` from step 3 reached the
container. If any of the four misses, stop, run `docker compose logs --tail 40 drawio`, and
say which earlier step is the likely cause: a `404` on PreConfig.js means the entrypoint
could not write into the webapp, and a first curl that never reaches `200` is usually DNS,
because a hostname whose A record was created minutes ago makes Caddy's first certificate
attempt fail and retry quietly. A running container is not success. Four asserts passing is
success.
The first screen at https://<DOMAIN>/?offline=1 is a dialog headed `Save diagrams to:` with
two buttons, `Device` and `Browser`, and a `Decide Later` link under them. There is no login
form and no sign-up link, because there are no accounts.
STOP: tell the user to open https://<DOMAIN>/?offline=1, confirm that dialog shows `Device`
and `Browser` and no Google Drive, OneDrive or GitHub button, and wait.
Do not continue until they confirm. No command can make that check for them, and it is the
difference between an editor that keeps their work on their own hardware and one that
offers to post it somewhere else.
## 8. First backup and restore
There is no database to dump and no data directory to archive, and saying so is more useful
than inventing one. The backup here is the configuration that rebuilds the service:
```bash
cd /srv/drawio
sudo tar -czf /srv/drawio/backups/drawio-config-$(date +%F).tar.gz -C /srv/drawio compose.yml .env -C /etc/caddy Caddyfile
ls -lh /srv/drawio/backups/
```
Assert: the archive exists and is non-empty. Print its size. Nothing is stopped, because
there is no write to catch halfway. A backup on the same disk is not a backup, so run this
one from the user's machine, not the server:
```bash
mkdir -p ~/backups/drawio
scp vps:/srv/drawio/backups/*.tar.gz ~/backups/drawio/
```
The diagrams are the other half, and no command on this server can reach them.
STOP: tell the user to open https://<DOMAIN>/?offline=1, draw one shape, and use File then
Save As to write the `.drawio` file somewhere on their own computer, and wait.
Do not continue until they confirm they have that file. Explain why: if they picked
`Browser` in that dialog the diagram is in one browser's local storage on one machine, and
no backup taken on this server would ever have contained it.
To restore the server: untar the archive into /srv/drawio, append the Caddy block from step
5 again, and run `docker compose up -d`. To restore a diagram, open the editor and use File
then Open to load the saved file. Tell the user that is two disaster plans, that the second
one holds the work, and that saving to their own disk is a habit rather than a step they
did once today.
## 9. Updating later
New versions are listed at https://github.com/jgraph/drawio/releases, and the Docker tag is
the release tag without its leading `v`. Take a backup first, then edit the image line in
/srv/drawio/compose.yml to the new tag and its digest:
```bash
cd /srv/drawio
docker compose pull
docker compose up -d
docker compose logs --tail 30 drawio
```
Then re-run all four asserts from step 7 before calling the update done. Nothing is
migrated in an upgrade here, because the container carries no state between versions.
## 10. What will probably go wrong
The first dialog. I clicked `Browser` because it sounded like the thing running on the
server, drew a diagram, and came back the next morning on a different laptop to an empty
canvas at the same hostname. Nothing was broken. `Browser` means that browser's local
storage on that machine, `Device` means a file on the computer in front of you, and neither
of them is the server. I spent several minutes reading Tomcat logs looking for a database
that has never existed. If the user reports a diagram has vanished, ask which browser and
machine they drew it on before you read a log line.
## 11. Out of scope
- Do not install the export server or set `DRAWIO_SELF_CONTAINED` or `EXPORT_URL`. That is
a second container carrying its own headless Chromium, and this prompt installs one
service. PNG and SVG export from the browser works without it.
- Do not set `DRAWIO_GOOGLE_CLIENT_ID`, `DRAWIO_MSGRAPH_CLIENT_ID` or `DRAWIO_GITLAB_ID`.
Each one is an OAuth application registered with somebody else, and step 7 asserts that
those backends are off.
- Do not set `ENABLE_DRAWIO_PROXY=1`. It opens a `/proxy` endpoint that fetches arbitrary
external URLs on this server's behalf, which is a request forwarder pointed at the user's
own network.
- Do not publish container port 8443 and do not set `LETS_ENCRYPT_ENABLED`. Caddy holds the
certificate for this hostname, and a second one inside the container would be a second
thing to renew.No terminal agent? Use the chat fallback — slower, you paste the commands
For ChatGPT or Claude in a browser. The model cannot touch your server, so it hands you one command at a time and you run each one. Same install, more of your evening.
This path is slower: you paste every command yourself, and there is nobody watching the
output but you. If you can run Claude Code, use the other tab.
You are installing draw.io 31.1.8 on a VPS where Prompt Zero is done: `ssh vps` works,
Docker and Caddy are installed, the firewall is default-deny. Run everything over `ssh vps`
unless a step says otherwise, and replace `<DOMAIN>` with the hostname whose A record
already points at the box.
Read this before step 1, because it is the reason to stop and think rather than the reason
to keep going. draw.io already runs a hosted editor at https://app.diagrams.net that costs
nothing and asks for no account: their own home page says "No account required. No credit
card." This install gives you the same editor. What it changes is where the page comes
from: it loads from a hostname you control, on a machine you control, and no third-party
origin ever serves the code. That is what an offline network, an air-gapped site or a
written company policy needs. If none of those three describe you, the free hosted editor
does this job and you can close this tab with nothing lost.
## 1. Preflight
```bash
free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}'
df -BG --output=avail /srv | tail -1
dpkg --print-architecture
dig +short <DOMAIN>
```
You should see: at least `1024` MB available, at least `5` G free, `amd64` or `arm64`, and
your server's IP on the last line.
If you do not: an empty last line means the A record does not exist yet. Add it, wait a
minute, and run `dig +short <DOMAIN>` again. Caddy cannot get a certificate for a hostname
that does not resolve, and failed attempts count against a rate limit you cannot see. The
RAM floor is 1024 MB because this is a Tomcat on a JVM rather than a static file server; on
a 512 MB box the container starts and then dies during the first page load, which reads as
a broken install rather than as a memory problem.
## 2. Layout
```bash
sudo install -d -m 750 -o $(id -u) -g $(id -g) /srv/drawio /srv/drawio/backups
ls -la /srv/drawio
```
You should see: `backups`, owned by you.
If you do not: there is no `data` directory in that command and that is deliberate. The
container writes no diagram anywhere on this server, so the only files that will ever live
under /srv/drawio are the two the next steps write and the archive step 8 makes of them.
## 3. Secrets
One secret, and it is not a login. draw.io has no accounts, so there is nothing to sign in
to and no admin page to protect. What this generates is `KEYSTORE_PASS`: the image builds a
self-signed certificate for its own container port 8443 at every start, and when that
variable is unset it uses a password printed in its own public documentation. Port 8443 is
never published by this install, so nobody outside the container can reach that
certificate, and a documented default is still a documented default.
```bash
umask 077
cat > /srv/drawio/.env <<EOF
DRAWIO_SERVER_URL=https://<DOMAIN>/
KEYSTORE_PASS=$(openssl rand -hex 32)
EOF
chmod 600 /srv/drawio/.env
umask 022
ls -l /srv/drawio/.env
```
You should see: mode `-rw-------`, your own username twice, and the path. Replace `<DOMAIN>`
on the first line with your real hostname before you paste, and keep the trailing slash:
`DRAWIO_SERVER_URL` is upstream's variable for the public deployment URL and it wants one.
If you do not: a mode of `-rw-r--r--` means `umask 077` did not take effect, which happens
if you pasted the lines separately in different shells. Run `chmod 600 /srv/drawio/.env` and
carry on. If the file already existed from an earlier attempt this block has overwritten
it, which costs nothing here, because the container regenerates its certificate at every
start anyway.
Do not paste that file, the generated value, or any command output containing it into this
chat window. Nothing in this install will ever ask you to type it back, so there is no
reason it should leave the server.
## 4. compose.yml
Paste the whole block at once, including the last two lines.
```bash
cat > /srv/drawio/compose.yml <<'EOF'
# draw.io · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
# image, ports, env vars .. https://github.com/jgraph/docker-drawio/blob/v31.1.8/README.md
# entrypoint behaviour .... https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/docker-entrypoint.sh
# image build ............. https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/Dockerfile
#
# One container: Tomcat 9 on JDK 11 serving the compiled draw.io editor at the
# root path on container port 8080, as a non-root tomcat user. There is no
# database and no account system, and this file mounts no volume, because the
# server holds no diagram. A diagram is written to the reader's own device or
# into the storage of the browser that drew it.
#
# The entrypoint switches every cloud backend off when its credentials are
# absent: with no DRAWIO_GOOGLE_CLIENT_ID, DRAWIO_MSGRAPH_CLIENT_ID or
# DRAWIO_GITLAB_ID it writes gapi, od and gl to 0, and it always writes db, gh
# and tr to 0. This file sets none of them, so none of them are on.
#
# KEYSTORE_PASS arrives from /srv/drawio/.env. Left alone, the image falls back
# to a keystore password printed in its own documentation and puts it on a
# self-signed certificate it regenerates at every start. Container port 8443 is
# never published here, so nothing outside can reach that certificate; the
# generated value is what keeps a documented default from standing behind it.
#
# Tag and digest read from Docker Hub on 2026-08-06; the manifest list covers
# linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
drawio:
image: jgraph/drawio:31.1.8@sha256:0c8910ea14dfbccb17c784ee17d995317a8d753479f5ec0f21b2ab2213153100
container_name: drawio
restart: unless-stopped
env_file: /srv/drawio/.env
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/ >/dev/null || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
ports:
# Loopback only: the host's Caddy is the only thing that reaches 8158.
- "127.0.0.1:8158:8080"
EOF
cd /srv/drawio && docker compose config >/dev/null && echo "compose OK"
```
You should see: `compose OK` and nothing else.
If you do not: `env file /srv/drawio/.env not found` means step 3 did not write the file.
`services must be a mapping` means the indentation was lost between the page and your
terminal, so run `rm /srv/drawio/compose.yml` and paste again in one go. There is no
`volumes:` key in that file and nothing is missing: the container has nowhere to put a
document, which is the single most important fact about this install.
## 5. Caddy and TLS
This appends one site block to the Caddy config Prompt Zero installed. Replace `<DOMAIN>` in
the block with your hostname before you paste. The first line takes a copy, because a
syntax error here takes down every other site on the box.
```bash
sudo cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.before-drawio
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sudo tee -a /etc/caddy/Caddyfile >/dev/null <<'EOF'
# draw.io · the Caddy site block for this service.
#
# Authored by caniselfhostit from https://caddyserver.com/docs/automatic-https
# and https://github.com/jgraph/docker-drawio/blob/v31.1.8/README.md
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname whose A record already points at this
# box. Caddy asks for the certificate on the first request and renews it on its
# own, so there is nothing to schedule. The container builds a self-signed
# certificate for its own port 8443 at every start; that port is not published,
# and Caddy is the only thing terminating TLS in front of this service.
<DOMAIN> {
# The editor is several megabytes of JavaScript on a cold load, so
# compression is the one setting here that changes what the user feels.
encode zstd gzip
# No Content-Security-Policy line on purpose. The container injects its own
# CSP as a meta tag at start-up and a second policy sent as a header would
# intersect with it, which fails as a blank editor rather than as an error.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# 8158 is the loopback port compose publishes on this host. It is not a
# container port and it is not open in the firewall.
reverse_proxy 127.0.0.1:8158
}
EOF
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
```
You should see: `Valid configuration` from validate, and no output at all from reload.
If you do not: run `sudo cp /etc/caddy/Caddyfile.before-drawio /etc/caddy/Caddyfile`,
reload, and paste again. Do not add a `Content-Security-Policy` header of your own here.
The container writes its own CSP into the page as a meta tag at start-up, and a browser
enforces the intersection of the two, so a second policy usually shows up as an editor that
loads to a blank grey screen with errors in the browser console and nothing at all in the
Caddy log.
## 6. Firewall
```bash
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
```
You should see: `Status: active`, rules for `80/tcp`, `443/tcp` and `443/udp`, and no rule
mentioning `8158` or `8443`.
If you do not: delete anything for those two with `sudo ufw delete allow 8158`. 8158 is
bound to 127.0.0.1 by the compose file, and 8443 is never published at all, so the
container's own TLS port has no host port a firewall rule could apply to. `Status: inactive`
is a different problem: Prompt Zero left this firewall enabled, so something has turned it
off since, and `sudo ufw enable` puts it back before you go any further.
## 7. Start and verify
Tomcat rewrites the editor's configuration files from the environment at every start, so
the first boot is slower than the ones after it.
```bash
cd /srv/drawio
docker compose pull
docker compose up -d
for i in $(seq 1 20); do code=$(curl -sS -o /dev/null -w '%{http_code}' https://<DOMAIN>/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 10; done
curl -sS https://<DOMAIN>/ | grep -c 'Flowchart Maker'
curl -sS https://<DOMAIN>/js/PreConfig.js | grep -cF "urlParams['gapi'] = '0'"
curl -sS https://<DOMAIN>/js/PreConfig.js | grep -cF "window.DRAWIO_SERVER_URL = 'https://<DOMAIN>/'"
```
You should see, in order: the loop reaching `200`, then `1`, then `1`, then `1`.
If you do not: the third command is the one worth understanding. It reads the configuration
file the container generated at start-up and checks that `gapi` is off, which means the
Google Drive backend is not offered to anyone who loads your editor. A `0` there means a
cloud storage button will appear in the app, and the cause is an environment variable that
should not be set. A `404` on PreConfig.js instead means the entrypoint could not write into
the webapp, which shows up in `docker compose logs --tail 40 drawio` as a `WARNING: No write
access` line. If the loop never reaches `200`, that is almost always DNS: a hostname whose
A record was created minutes ago makes Caddy's first certificate attempt fail and retry
quietly. A running container is not success. Four asserts passing is success.
The first screen at https://<DOMAIN>/?offline=1 is a dialog headed `Save diagrams to:` with
two buttons, `Device` and `Browser`, and a `Decide Later` link under them. There is no login
form and no sign-up link, because there are no accounts.
Open https://<DOMAIN>/?offline=1 now and confirm that dialog shows `Device` and `Browser`
and no Google Drive, OneDrive or GitHub button. That is the check no command can make for
you, and it is the difference between an editor that keeps your work on your own hardware
and one that offers to post it somewhere else.
## 8. First backup and restore
There is no database to dump and no data directory to archive, and saying so is more useful
than inventing one. The backup here is the configuration that rebuilds the service:
```bash
cd /srv/drawio
sudo tar -czf /srv/drawio/backups/drawio-config-$(date +%F).tar.gz -C /srv/drawio compose.yml .env -C /etc/caddy Caddyfile
ls -lh /srv/drawio/backups/
```
You should see: one file, a couple of kilobytes. Nothing goes offline, because there is no
write to catch halfway.
If you do not: an archive of about 45 bytes is an empty one, which means `tar` found none of
the three files. Check that you are in /srv/drawio and that step 3 and step 4 both wrote
their file.
A backup on the same disk as the data is not a backup. Run this one on your own machine,
not the server:
```bash
mkdir -p ~/backups/drawio
scp vps:/srv/drawio/backups/*.tar.gz ~/backups/drawio/
```
You should see: one file copied, and it listed by `ls -lh ~/backups/drawio/`.
If you do not: `Permission denied (publickey)` means you ran it on the server. The `vps:`
prefix only means something on your own machine, where the `vps` alias Prompt Zero created
lives.
Now the other half, and no command on that server can reach it. Open
https://<DOMAIN>/?offline=1, draw one shape, and use File then Save As to write the
`.drawio` file somewhere on your own computer. If you picked `Browser` in that first dialog
the diagram is in one browser's local storage on one machine, and the archive you took
above does not contain it and never will.
To restore the server: untar the archive into /srv/drawio, append the Caddy block from step
5 again, and run `docker compose up -d`. To restore a diagram, open the editor and use File
then Open to load the file you saved. That is two disaster plans, the second one holds your
work, and saving to your own disk is a habit rather than a step you did once today.
## 9. Updating later
New versions are listed at https://github.com/jgraph/drawio/releases, and the Docker tag is
the release tag without its leading `v`. Take the backup first, then edit the `image:` line
in /srv/drawio/compose.yml to the new tag and its digest.
```bash
cd /srv/drawio
docker compose pull
docker compose up -d
docker compose logs --tail 30 drawio
```
You should see: the entrypoint printing the configuration it wrote, then Tomcat starting,
and no repeating restart.
If you do not: put the old tag and digest back and run the same three commands. Then re-run
all four asserts from step 7 before you call the update done. Nothing is migrated in an
upgrade here, because the container carries no state between versions, which makes a
rollback the cheapest one in this catalogue.
## 10. What will probably go wrong
The first dialog. I clicked `Browser` because it sounded like the thing running on the
server, drew a diagram, and came back the next morning on a different laptop to an empty
canvas at the same hostname. Nothing was broken. `Browser` means that browser's local
storage on that machine, `Device` means a file on the computer in front of you, and neither
of them is the server. I spent several minutes reading Tomcat logs looking for a database
that has never existed. If a diagram disappears, ask which browser and machine you drew it
on before you read a log line.
## 11. Out of scope
- Do not install the export server or set `DRAWIO_SELF_CONTAINED` or `EXPORT_URL`. That is
a second container carrying its own headless Chromium, and this install runs one service.
PNG and SVG export from the browser works without it.
- Do not set `DRAWIO_GOOGLE_CLIENT_ID`, `DRAWIO_MSGRAPH_CLIENT_ID` or `DRAWIO_GITLAB_ID`.
Each one is an OAuth application registered with somebody else, and step 7 asserts that
those backends are off.
- Do not set `ENABLE_DRAWIO_PROXY=1`. It opens a `/proxy` endpoint that fetches arbitrary
external URLs on your server's behalf, which is a request forwarder pointed at your own
network.
- Do not publish container port 8443 and do not set `LETS_ENCRYPT_ENABLED`. Caddy holds the
certificate for this hostname, and a second one inside the container would be a second
thing to renew.312 lines · 14,994 bytes
What this prompt will do
- Preflight
- Docker
- Layout
- Secrets
- compose.yml
- Nothing is public
- Start and verify
- First backup and restore
- Updating later
- What will probably go wrong
- Out of scope
Read out of the prompt’s own step headings at build time — if the prompt changes, this list changes with it.
You are Claude Code on the user's own computer. There is no server and no Prompt Zero:
everything in this prompt runs on this machine and stays on it.
Run every command on this computer, in the shell you are already in. Nothing in this prompt
uses ssh.
Install draw.io 31.1.8 under ~/selfhost/drawio, answering at http://localhost:8158.
## 1. Preflight
Say both of these to the user before step 2 runs; together they decide whether this install
is worth doing.
First: draw.io already runs a hosted editor at https://app.diagrams.net that costs nothing
and asks for no account. Their own home page says "No account required. No credit card."
This prompt installs the same editor; what changes is where the page comes from. This copy
loads from a container here, so it works with the network unplugged and no third-party
origin serves the code. If neither matters, say plainly that the hosted editor does the
same job.
Second: nothing here stores a diagram. The container has no database and no document store,
so a drawing lives in the storage of the browser that drew it or in a file the user saves
themselves, and clearing site data loses work no backup could have held.
Detect the OS and measure the machine:
```bash
uname -s
case "$(uname -s)" in
Darwin) vm_stat | awk '/page size/{p=$8} /free|inactive/{s+=$3} END {printf "%d MB available\n", s*p/1048576}' ;;
Linux) . /etc/os-release && echo "$ID $VERSION_CODENAME"; free -m | awk '/^Mem:/ {print $7 " MB available of " $2 " MB"}' ;;
MINGW*|MSYS*) powershell -Command "(Get-CimInstance Win32_OperatingSystem).FreePhysicalMemory" | awk '$1+0 {printf "%d MB available\n", $1/1024}' ;;
esac
df -h ~
```
`Darwin` is macOS, `Linux` is Linux, `MINGW` or `MSYS` is Windows under Git Bash. On Linux
the distribution ID and codename print next, for step 2. draw.io is a Tomcat on a JVM and
wants 1024 MB of RAM available and 5 GB free on the home disk; the image publishes amd64
and arm64. Every branch prints free memory, so one floor covers all three, and Docker
Desktop takes its allocation out of that number. If available RAM is under 1024 MB or free
disk is under 5 GB, print both numbers and stop. Do not install and hope.
## 2. Docker
Check before installing anything:
```bash
docker info >/dev/null 2>&1 && echo "docker OK" || echo "docker MISSING"
docker compose version 2>/dev/null || true
```
If that printed `docker OK` and a compose version, skip to step 3.
Otherwise, install Docker for the OS step 1 detected:
- macOS: if `command -v brew` succeeds, run `brew install --cask docker`. If there is no
Homebrew, STOP: tell the user to download Docker Desktop from
https://www.docker.com/products/docker-desktop/ and install it, and wait until they
confirm. Either way, then STOP: tell the user to open Docker Desktop once, accept its
terms, and wait for the whale icon to say it is running. Do not continue until they
confirm.
- Windows: run `winget install -e --id Docker.DockerDesktop`. If winget is missing or the
install fails, STOP: tell the user to download Docker Desktop from the URL above and
install it, and wait until they confirm. Docker Desktop configures WSL 2 itself and may
ask for a reboot; if it does, STOP and tell the user to reboot and come back, this
prompt resumes at this step. Then STOP: have the user open Docker Desktop, accept its
terms, and confirm it says running.
- Linux, Debian or Ubuntu: install Docker Engine from download.docker.com's apt
repository, with its signing key saved to a file first, never piped into a shell. The
fence is guarded, a no-op on anything but a Linux with apt:
```bash
if [ "$(uname -s)" = "Linux" ] && command -v apt-get >/dev/null 2>&1; then
sudo apt-get update
sudo apt-get install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/$(. /etc/os-release && echo "$ID")/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/$(. /etc/os-release && echo "$ID") $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo usermod -aG docker "$USER"
fi
```
Adding the user to the docker group is root-equivalent on this machine; say that to the
user in one sentence, and tell them the group change lands at their next login.
- Linux, anything else: STOP. Tell the user to install Docker Engine and the compose
plugin with their distribution's package manager, and to run this prompt again once
`docker info` works.
Assert: `docker info` exits 0 and `docker compose version` prints a version. Do not
continue without both.
## 3. Layout
```bash
mkdir -p ~/selfhost/drawio/backups
ls -la ~/selfhost/drawio
```
Assert: `ls -la` shows `backups`, owned by the user. There is no `data` folder: the
container writes no diagram to disk, so step 5 mounts nothing and no ownership fix is
needed on any of the three systems.
## 4. Secrets
One secret, and it is not a login. draw.io has no accounts, so there is nothing to sign in
to. What this generates is `KEYSTORE_PASS`: the image builds a self-signed certificate for
its own container port 8443 at every start, and with that variable unset it uses a value
printed in its own public documentation. That port is never published here, so a documented
default would guard something unreachable, which is a reason to replace it rather than a
reason to keep it. Print it nowhere, and keep it out of your summary and out of any log.
```bash
umask 077
cat > ~/selfhost/drawio/.env <<EOF
DRAWIO_SERVER_URL=http://localhost:8158/
KEYSTORE_PASS=$(openssl rand -hex 32)
EOF
chmod 600 ~/selfhost/drawio/.env
umask 022
ls -l ~/selfhost/drawio/.env
```
Assert: the file exists with mode `-rw-------`. Git Bash ships openssl, so this runs the
same on all three systems. `DRAWIO_SERVER_URL` is upstream's variable for the deployment
URL and it wants the trailing slash, which is why the line ends in one.
On Windows those mode bits are advisory: NTFS does not enforce them, and the real boundary
is the user's own Windows account.
## 5. compose.yml
```bash
cat > ~/selfhost/drawio/compose.yml <<'EOF'
# draw.io · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
# image, ports, env vars .. https://github.com/jgraph/docker-drawio/blob/v31.1.8/README.md
# entrypoint behaviour .... https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/docker-entrypoint.sh
# image build ............. https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/Dockerfile
#
# One container: Tomcat 9 on JDK 11 serving the compiled draw.io editor at the
# root path on container port 8080, as a non-root tomcat user. This file lives
# in ~/selfhost/drawio/ and holds no absolute path, so it works on all three.
#
# There is no volume here, and that is not an omission: a diagram is written to
# this computer's disk or into the browser that drew it, never to the server.
#
# The entrypoint switches every cloud backend off when its credentials are
# absent: with no DRAWIO_GOOGLE_CLIENT_ID, DRAWIO_MSGRAPH_CLIENT_ID or
# DRAWIO_GITLAB_ID it writes gapi, od and gl to 0, and it always writes db, gh
# and tr to 0. This file sets none of them, so none of them are on.
#
# KEYSTORE_PASS arrives from ./.env. Left alone, the image falls back to a
# keystore value printed in its own documentation, on a self-signed certificate
# it makes at every start for container port 8443, which this file never
# publishes. The generated value replaces a documented default.
#
# Tag and digest read from Docker Hub on 2026-08-06; the manifest list covers
# linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
drawio:
image: jgraph/drawio:31.1.8@sha256:0c8910ea14dfbccb17c784ee17d995317a8d753479f5ec0f21b2ab2213153100
container_name: drawio
restart: unless-stopped
env_file: ./.env
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/ >/dev/null || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
ports:
# Loopback only: no other device on the wifi can reach 8158.
- "127.0.0.1:8158:8080"
EOF
cd ~/selfhost/drawio && docker compose config >/dev/null && echo "compose OK"
```
Assert: that prints `compose OK`. One service, one published port, no mounts.
## 6. Nothing is public
No reverse proxy, no certificate, no firewall rule. Each is a decision:
- No DNS. There is no hostname, so nothing to resolve and nothing to wait for.
- No TLS. A certificate attests a public name and nothing here has one. Browsers treat
http://localhost as a secure context anyway.
- No firewall rule. Nothing is published beyond loopback, so no port needs closing.
8158 is bound to 127.0.0.1, this computer only: not the user's phone, not a laptop on the
same wifi, not anyone on the internet. Confirm it:
```bash
grep -c '"127.0.0.1:' ~/selfhost/drawio/compose.yml
```
Assert: that prints `1`, the single published port `- "127.0.0.1:8158:8080"`. Container
port 8443 is never published, so the certificate the image makes at start-up has no way in.
## 7. Start and verify
Tomcat rewrites the editor's configuration from the environment at every start, so the
first boot is the slow one.
```bash
cd ~/selfhost/drawio
docker compose pull
docker compose up -d
for i in $(seq 1 20); do code=$(curl -sS -o /dev/null -w '%{http_code}' http://localhost:8158/); echo "$i $code"; [ "$code" = 200 ] && break; sleep 10; done
curl -sS http://localhost:8158/ | grep -c 'Flowchart Maker'
curl -sS http://localhost:8158/js/PreConfig.js | grep -cF "urlParams['gapi'] = '0'"
curl -sS http://localhost:8158/js/PreConfig.js | grep -cF "window.DRAWIO_SERVER_URL = 'http://localhost:8158/'"
```
Assert all four, and print what you received for each. The loop ends on `200`. The second
command prints `1`, because `Flowchart Maker` is in the title of the served document. The
third prints `1`, the security assert here: it proves the container wrote `gapi` off, so
the Google Drive backend is not offered on this page. The fourth prints `1`, proving the
`.env` from step 4 reached the container. If any of the four misses, stop, run
`docker compose logs --tail 40 drawio`, and name the likely cause: a `404` on PreConfig.js
means the entrypoint could not write into the webapp, and a loop that never reaches `200`
usually means Tomcat is still starting, so give it another minute. If
`port is already allocated` came back, find what holds 8158 with
`lsof -nP -iTCP:8158 -sTCP:LISTEN` and stop until the user frees it. A running container is
not success. Four asserts is success.
The first screen at http://localhost:8158/?offline=1 is a dialog headed `Save diagrams to:`
with two buttons, `Device` and `Browser`, and a `Decide Later` link under them. There is no
login form and no sign-up link, because there are no accounts.
STOP: tell the user to open http://localhost:8158/?offline=1, confirm that dialog shows
`Device` and `Browser` and no Google Drive, OneDrive or GitHub button, and wait.
Do not continue until they confirm. That check is the difference between an editor that
keeps the work on this machine and one that offers to post it somewhere else.
## 8. First backup and restore
There is no database to dump and no data directory to archive. The backup here is the
configuration that rebuilds the service:
```bash
cd ~/selfhost/drawio
tar -C ~/selfhost/drawio -czf ~/selfhost/drawio/backups/drawio-config-$(date +%F).tar.gz compose.yml .env
ls -lh ~/selfhost/drawio/backups/
```
Assert: the archive exists and is non-empty. Print its size. Nothing is stopped, because
there is no write to catch halfway.
That archive sits on the same disk as everything else, which is not a backup, and on a
laptop the disk and the machine fail together. Ask the user for a destination that leaves
this computer, a folder their sync service watches or a USB stick, and copy it there with
`cp`. In Git Bash a Windows drive is written `/d/Backups`. Assert: the user confirms the
filename is listed there.
The diagrams are the other half, and no command here can reach them.
STOP: tell the user to open http://localhost:8158/?offline=1, draw one shape, and use File
then Save As to write the `.drawio` file into a folder they already back up, and wait.
Do not continue until they confirm they have that file. Explain why: pick `Browser` in
that dialog and the diagram is in one browser's local storage, which the archive above
does not contain.
To restore the service: untar the archive into ~/selfhost/drawio and run
`docker compose up -d`. To restore a diagram, open the editor and use File then Open. Tell
the user that is two disaster plans and the second one holds the work.
## 9. Updating later
New versions are listed at https://github.com/jgraph/drawio/releases, and the Docker tag is
the release tag without its leading `v`. Take the backup first, then edit the image line in
~/selfhost/drawio/compose.yml to the new tag and its digest:
```bash
cd ~/selfhost/drawio
docker compose pull
docker compose up -d
docker compose logs --tail 30 drawio
```
Then re-run all four asserts from step 7 before calling the update done. Nothing is
migrated, because the container carries no state between versions.
## 10. What will probably go wrong
I rebooted, opened http://localhost:8158 out of habit, and got a connection error that
looked like the install had evaporated. It had not: Docker Desktop had not started with the
session, so nothing was listening on 8158. `restart: unless-stopped` acts only once the
Docker daemon is up. Turn on its start-at-login setting, and after a reboot run
`cd ~/selfhost/drawio && docker compose up -d` before concluding anything is broken. The
diagrams were never at risk: they were in the browser and on the disk, not in the container.
## 11. Out of scope
- Do not expose this to the internet.
- Do not configure port forwarding on the router.
- Do not add a reverse proxy or TLS.
- Do not rebind 8158 to 0.0.0.0 so a phone or a colleague can reach it. That publishes an
editor with no login on every network this machine joins.
- Do not install the export server or set `DRAWIO_SELF_CONTAINED` or `EXPORT_URL`. That is
a second container carrying headless Chromium; PNG and SVG export from the browser works
without it.
- Do not set `DRAWIO_GOOGLE_CLIENT_ID`, `DRAWIO_MSGRAPH_CLIENT_ID`, `DRAWIO_GITLAB_ID` or
`ENABLE_DRAWIO_PROXY=1`. The first three are OAuth applications registered with somebody
else, and step 7 asserts they are off; the last opens an endpoint that fetches arbitrary
external URLs from inside this machine's network.compose.local.ymlthe services, pinned · local layout43 lines
# draw.io · the deterministic fallback for the local path. Authored by
# caniselfhostit from the upstream documentation, not copied from a repository:
# image, ports, env vars .. https://github.com/jgraph/docker-drawio/blob/v31.1.8/README.md
# entrypoint behaviour .... https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/docker-entrypoint.sh
# image build ............. https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/Dockerfile
#
# One container: Tomcat 9 on JDK 11 serving the compiled draw.io editor at the
# root path on container port 8080, as a non-root tomcat user. This file lives
# in ~/selfhost/drawio/ and holds no absolute path, so it works on all three.
#
# There is no volume here, and that is not an omission: a diagram is written to
# this computer's disk or into the browser that drew it, never to the server.
#
# The entrypoint switches every cloud backend off when its credentials are
# absent: with no DRAWIO_GOOGLE_CLIENT_ID, DRAWIO_MSGRAPH_CLIENT_ID or
# DRAWIO_GITLAB_ID it writes gapi, od and gl to 0, and it always writes db, gh
# and tr to 0. This file sets none of them, so none of them are on.
#
# KEYSTORE_PASS arrives from ./.env. Left alone, the image falls back to a
# keystore value printed in its own documentation, on a self-signed certificate
# it makes at every start for container port 8443, which this file never
# publishes. The generated value replaces a documented default.
#
# Tag and digest read from Docker Hub on 2026-08-06; the manifest list covers
# linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
drawio:
image: jgraph/drawio:31.1.8@sha256:0c8910ea14dfbccb17c784ee17d995317a8d753479f5ec0f21b2ab2213153100
container_name: drawio
restart: unless-stopped
env_file: ./.env
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/ >/dev/null || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
ports:
# Loopback only: no other device on the wifi can reach 8158.
- "127.0.0.1:8158:8080"agent-readable mirror: /self-host/whimsical.md
The files, if you'd rather do it yourself
The cloud path with no agent involved: three files, in the order you'd use them. The cloud prompt above writes exactly these — if the two ever disagree, the files are the ones CI diffs. The local path ships its own compose file, collapsed under its own prompt.
compose.ymlthe services, pinned43 lines
# draw.io · the deterministic fallback. Authored by caniselfhostit from the
# upstream documentation, not copied from a repository:
# image, ports, env vars .. https://github.com/jgraph/docker-drawio/blob/v31.1.8/README.md
# entrypoint behaviour .... https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/docker-entrypoint.sh
# image build ............. https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/Dockerfile
#
# One container: Tomcat 9 on JDK 11 serving the compiled draw.io editor at the
# root path on container port 8080, as a non-root tomcat user. There is no
# database and no account system, and this file mounts no volume, because the
# server holds no diagram. A diagram is written to the reader's own device or
# into the storage of the browser that drew it.
#
# The entrypoint switches every cloud backend off when its credentials are
# absent: with no DRAWIO_GOOGLE_CLIENT_ID, DRAWIO_MSGRAPH_CLIENT_ID or
# DRAWIO_GITLAB_ID it writes gapi, od and gl to 0, and it always writes db, gh
# and tr to 0. This file sets none of them, so none of them are on.
#
# KEYSTORE_PASS arrives from /srv/drawio/.env. Left alone, the image falls back
# to a keystore password printed in its own documentation and puts it on a
# self-signed certificate it regenerates at every start. Container port 8443 is
# never published here, so nothing outside can reach that certificate; the
# generated value is what keeps a documented default from standing behind it.
#
# Tag and digest read from Docker Hub on 2026-08-06; the manifest list covers
# linux/amd64 and linux/arm64.
#
# NOT YET VERIFIED: no harness run has been recorded against this file.
services:
drawio:
image: jgraph/drawio:31.1.8@sha256:0c8910ea14dfbccb17c784ee17d995317a8d753479f5ec0f21b2ab2213153100
container_name: drawio
restart: unless-stopped
env_file: /srv/drawio/.env
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/ >/dev/null || exit 1"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
ports:
# Loopback only: the host's Caddy is the only thing that reaches 8158.
- "127.0.0.1:8158:8080"Caddyfilethe hostname and TLS32 lines
# draw.io · the Caddy site block for this service.
#
# Authored by caniselfhostit from https://caddyserver.com/docs/automatic-https
# and https://github.com/jgraph/docker-drawio/blob/v31.1.8/README.md
#
# Append this to /etc/caddy/Caddyfile, the Caddy that Prompt Zero installed,
# with <DOMAIN> replaced by the hostname whose A record already points at this
# box. Caddy asks for the certificate on the first request and renews it on its
# own, so there is nothing to schedule. The container builds a self-signed
# certificate for its own port 8443 at every start; that port is not published,
# and Caddy is the only thing terminating TLS in front of this service.
<DOMAIN> {
# The editor is several megabytes of JavaScript on a cold load, so
# compression is the one setting here that changes what the user feels.
encode zstd gzip
# No Content-Security-Policy line on purpose. The container injects its own
# CSP as a meta tag at start-up and a second policy sent as a header would
# intersect with it, which fails as a blank editor rather than as an error.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains"
X-Content-Type-Options "nosniff"
X-Frame-Options "SAMEORIGIN"
Referrer-Policy "no-referrer"
-Server
}
# 8158 is the loopback port compose publishes on this host. It is not a
# container port and it is not open in the firewall.
reverse_proxy 127.0.0.1:8158
}install.shthe same install, no agent157 lines
#!/usr/bin/env bash
# draw.io · the agent-free install.
#
# Everything prompt.md tells an agent to do, as a script you can read first.
# Run it on the VPS, as a non-root user who is in the docker group:
#
# DOMAIN_HOST=draw.example.com ./install.sh
#
# Authored by caniselfhostit from the upstream documentation:
# https://github.com/jgraph/docker-drawio/blob/v31.1.8/README.md
# https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/docker-entrypoint.sh
# https://github.com/jgraph/docker-drawio/blob/v31.1.8/main/Dockerfile
# https://caddyserver.com/docs/automatic-https
#
# Before you run this, know what it is for. draw.io's own hosted editor at
# https://app.diagrams.net is free and asks for no account, and it is the same
# editor. This install changes where the page comes from, not what it does: your
# hostname, your machine, no third-party origin serving the code. If that is not
# the thing you needed, you have not lost anything by closing the file here.
#
# One secret is generated on this machine: KEYSTORE_PASS, which replaces the
# value the image would otherwise take from its own public documentation for the
# self-signed certificate it builds at every start. It goes into /srv/drawio/.env
# with mode 600 and is never printed.
#
# NOT YET VERIFIED: no harness run has been recorded against this script.
set -euo pipefail
APP_DIR="${APP_DIR:-/srv/drawio}"
DOMAIN_HOST="${DOMAIN_HOST:-}"
die() { printf 'install.sh: %s\n' "$1" >&2; exit 1; }
# --- 1. Refuse to start on a machine that is not ready -----------------------
[ -n "$DOMAIN_HOST" ] || die "set DOMAIN_HOST to the hostname you pointed at this server, e.g. draw.example.com"
command -v docker >/dev/null 2>&1 || die "docker is not installed. Run Prompt Zero first."
docker compose version >/dev/null 2>&1 || die "the docker compose plugin is missing"
command -v caddy >/dev/null 2>&1 || die "caddy is not installed on the host. Run Prompt Zero first."
command -v openssl >/dev/null 2>&1 || die "openssl is not installed"
avail_mb="$(free -m | awk '/^Mem:/ {print $7}')"
[ "$avail_mb" -ge 1024 ] || die "only ${avail_mb} MB of RAM available; a Tomcat on a JVM wants 1024 MB"
avail_gb="$(df -BG --output=avail /srv | tail -1 | tr -dc '0-9')"
[ "$avail_gb" -ge 5 ] || die "only ${avail_gb} GB free on /srv; this install wants 5 GB"
resolved="$(getent hosts "$DOMAIN_HOST" | awk '{print $1; exit}' || true)"
[ -n "$resolved" ] || die "$DOMAIN_HOST does not resolve yet. Add the A record, wait a minute, run this again."
# --- 2. Lay the files out ----------------------------------------------------
#
# There is no data directory here and none is missing. The container writes no
# diagram to this server, so the only files under $APP_DIR are the two below.
sudo install -d -m 750 -o "$(id -u)" -g "$(id -g)" "$APP_DIR" "$APP_DIR/backups"
install -m 0644 "$(dirname "$0")/compose.yml" "$APP_DIR/compose.yml"
install -m 0644 "$(dirname "$0")/Caddyfile" "$APP_DIR/Caddyfile"
# --- 3. Generate the one secret, on the server -------------------------------
#
# Read it later, if you ever need to, with
# sudo grep KEYSTORE_PASS /srv/drawio/.env
# Nothing in this install will ask you to type it.
if [ ! -f "$APP_DIR/.env" ]; then
umask 077
cat > "$APP_DIR/.env" <<-ENVFILE
DRAWIO_SERVER_URL=https://${DOMAIN_HOST}/
KEYSTORE_PASS=$(openssl rand -hex 32)
ENVFILE
chmod 600 "$APP_DIR/.env"
umask 022
fi
cd "$APP_DIR"
docker compose config >/dev/null
# --- 4. Caddy site block, on the host ----------------------------------------
if ! sudo grep -qF "$DOMAIN_HOST {" /etc/caddy/Caddyfile; then
sudo cp /etc/caddy/Caddyfile "/etc/caddy/Caddyfile.before-drawio"
printf '\n' | sudo tee -a /etc/caddy/Caddyfile >/dev/null
sed "s|<DOMAIN>|${DOMAIN_HOST}|g" "$APP_DIR/Caddyfile" | sudo tee -a /etc/caddy/Caddyfile >/dev/null
fi
sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy
# --- 5. Ports: two open, and neither 8158 nor 8443 is one of them ------------
if command -v ufw >/dev/null 2>&1; then
echo "==> 80/tcp and 443/tcp for Caddy, 443/udp for HTTP/3; 8158 and 8443 stay closed"
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp
sudo ufw status verbose
fi
# --- 6. Start it -------------------------------------------------------------
#
# Tomcat rewrites the editor's configuration files from the environment at every
# start, so the first boot is slower than the ones after it.
docker compose pull
docker compose up -d
echo "==> waiting for https://${DOMAIN_HOST}/"
for _ in $(seq 1 20); do
code="$(curl -sS -o /dev/null -w '%{http_code}' "https://${DOMAIN_HOST}/" || true)"
[ "$code" = "200" ] && break
sleep 10
done
[ "${code:-}" = "200" ] || die "the site answered ${code:-nothing}. Check: docker compose logs --tail 40 drawio"
curl -sS "https://${DOMAIN_HOST}/" | grep -q 'Flowchart Maker' \
|| die "the page did not carry the editor's title. Check: docker compose logs --tail 40 drawio"
# The security assert: the container's generated configuration must have every
# cloud storage backend switched off, so nobody loading this editor is offered
# somewhere else to put their diagram.
preconfig="$(curl -sS "https://${DOMAIN_HOST}/js/PreConfig.js" || true)"
printf '%s' "$preconfig" | grep -qF "urlParams['gapi'] = '0'" \
|| die "PreConfig.js does not switch the Google Drive backend off. Stop and investigate."
printf '%s' "$preconfig" | grep -qF "window.DRAWIO_SERVER_URL = 'https://${DOMAIN_HOST}/'" \
|| die "PreConfig.js does not carry this hostname, so /srv/drawio/.env did not reach the container."
# --- 7. The first backup, before day one ends --------------------------------
#
# There is no database to dump and no data directory to archive. This archives
# the configuration that rebuilds the service, including the live Caddy file
# with the real hostname substituted into it.
STAMP="$(date +%Y%m%d-%H%M%S)"
sudo tar -czf "$APP_DIR/backups/drawio-config-${STAMP}.tar.gz" -C "$APP_DIR" compose.yml .env -C /etc/caddy Caddyfile
ls -lh "$APP_DIR/backups/"
[ -s "$APP_DIR/backups/drawio-config-${STAMP}.tar.gz" ] || die "the config archive is empty"
cat <<-DONE
draw.io is answering at https://${DOMAIN_HOST}/
1. Open https://${DOMAIN_HOST}/?offline=1 and you get a dialog headed
"Save diagrams to:" with two buttons, Device and Browser, and a
"Decide Later" link. No Google Drive, no OneDrive, no GitHub: this
script asserted those are off before it printed this.
2. There is no account and no password, because draw.io has neither.
Anyone who reaches this hostname gets their own blank canvas. They
cannot see your diagrams, because your diagrams are not here.
3. Nothing is stored on this server. Device saves a file on the computer
you are sitting at; Browser saves into that one browser's storage.
Draw something now and use File then Save As, because that file is the
only copy of your work that a backup can ever reach.
4. First backup written to $APP_DIR/backups: compose.yml, .env and the
live Caddy site block. It is on the same disk as the install, which is
not a backup. Copy it somewhere else tonight:
scp vps:$APP_DIR/backups/*.tar.gz ~/backups/drawio/
DONEWhat you're signing up for
The part a vendor's comparison page leaves out. None of it is a reason not to do this; all of it is yours the moment you cancel Whimsical.
- You may not need this at all, and we would rather say so. draw.io's own hosted editor at app.diagrams.net is free, needs no account, and is the same editor this installs. Self-hosting buys you the origin the page loads from: it works with the network unplugged, and no third-party server hands code to your browser. If neither of those is a requirement you actually have, the free hosted editor is the better answer.
- No realtime collaboration. This is one editor per person per diagram. The container has no sync service and no presence, so two people on one board is not slow here, it does not exist. That, the comments and the shared workspace are most of what Whimsical is actually selling.
- The server stores nothing. There is no database, no document store and no volume in the compose file. A diagram is a file you save to your own computer or a record in the storage of the browser that drew it, which means clearing site data loses work no backup on the server could have held.
- So the backup is small and the important half is not on the box. The archive holds compose.yml, the .env and the Caddy site block, and that is genuinely everything the server has. Your diagrams are wherever you saved them, and keeping them is a habit rather than a step in the install.
- No accounts, which cuts both ways. There is no password to lose and no sign-up page to close, and anyone who finds the hostname gets their own blank canvas. They cannot see your work, because your work was never sent to the server.
Where this came from
“No account required. No credit card.”
- draw.io's own hosted editor is free and states that no account and no sign-up are required, which is the alternative to weigh before renting a server for this one. source
- The image builds from a Tomcat base, serves the compiled editor at the root path on container port 8080, exposes 8080 and 8443, and runs as a non-root tomcat user with no volume of its own. source
- With no Google, Microsoft or GitLab client id set, the entrypoint writes gapi, od and gl to 0, and it always writes db, gh and tr to 0, so a default install offers no cloud storage backend at all. source
- Upstream documents the ?offline=1 URL parameter as a security feature that disables support of cloud storage, and lists KEYSTORE_PASS with a default value printed in the README itself. source
- The first dialog is the storage picker headed Save diagrams to, and the Browser option it offers is the browser's own local storage rather than anything on the server. source
Questions people actually ask
Answered from this page's own data — the same numbers, in sentences.
Can I self-host Whimsical?
Not Whimsical itself — the vendor does not ship a version you can run on your own server. What you can self-host is the job people pay it for, and the answer to that is draw.io. The structured-diagram editor as one container on a hostname you own, with every cloud storage backend switched off. The install is one command: one container behind Caddy with automatic TLS, secrets generated on the server rather than in a chat window, and a first backup taken before the agent says it is done, in about 9 minutes. The prompt on this page does it; the compose.yml, Caddyfile and install.sh below do the same install with no agent at all.
What replaces Whimsical?
draw.io. The structured-diagram editor as one container on a hostname you own, with every cloud storage backend switched off. The closest match to what Whimsical is actually used for: structured diagrams, flowcharts, wireframes and org charts, with shapes that snap and arrows that stay attached when you move a box. It is also the entry on this site with the most honest catch, and we would rather say it than sell around it. draw.io's own hosted editor at app.diagrams.net is free and asks for no account, so self-hosting buys you the origin the page loads from and nothing else: an offline network, an air-gapped site, or a written policy about which servers may serve code to your browser. If none of those is your problem, use the free hosted one. Also evaluated and ranked below it: Excalidraw. The prompt on this page installs draw.io only. draw.io is Apache-2.0-licensed and free; nothing on this page is a hosted service we sell you.
What does self-hosting cost compared to Whimsical?
1024 MB of RAM and 5 GB of disk — the smallest tier most VPS hosts sell, about $5 a month. draw.io itself is free and Apache-2.0-licensed; the bill is the server, plus a domain you probably already own. What you stop paying: Whimsical Pro, $10/mo — $120 a year, 1 seat assumed.
How hard is it really?
ONE COMMAND — under 10 minutes. The rule that produced that verdict: one container, no database, no outside integration, at most one secret. Nothing to negotiate with anyone else, nothing to back up separately, at most one secret to generate. This is the case where the compose file honestly is the whole install. The tier is derived from seven countable facts about the draw.io install, not from anyone's impression of it, and the whole rubric is published on the methodology page.
Can I run draw.io on my own computer instead of a server?
Yes — that is the second path in the prompt box above. "On my computer" installs the same draw.io on the machine you are sitting at: no VPS, no domain, no DNS, and nothing exposed to the internet. It checks for Docker first and installs Docker Desktop if the machine does not have it — macOS, Windows and Linux each get their own step — then binds everything to loopback, so the app answers on http://localhost and only on that computer. Worth knowing: The editor runs as well on a laptop as on a server because neither one stores a diagram: your work goes into a file you save or into this browser's storage, so the container is only ever the thing that serves the page. Same discipline as the cloud path: pinned images, secrets generated on the machine, and a first backup taken before the prompt says it is done.
Content last checked 2026-08-07. Verdicts are derived from the published rubric on /methodology; corrections go through the issue tracker.