Self-hosted Excalidraw that keeps to itself: links, collaboration and fonts all stay on your server.
5.2K
Excalidraw is a virtual whiteboard for sketches that look hand-drawn. This is a self-hosted build of it that keeps to itself: shared links, live collaboration, the session scene, the fonts and the icons all come from your own server, and the browser never contacts anyone else. One container, one port, nothing to configure.
A one-knight job: I build it, keep it running, work through the issues and add what people ask for, until nothing is missing. It is free, with no accounts, no telemetry, no ads and no paid tier. No asterisk anywhere. Nothing readable ever leaves your own walls. Forged on evenings and weekends, with heart and stubbornness.
If it has earned a place on your server or computer, toss a coin to your knight: it helps cover the costs and keeps the project alive. It also makes this knight's heart beat a little faster. Three ways below, whichever suits you.
Excalidraw is excellent, and the published image of it is a plain web server with the app inside. What that image does not tell you is how much of the app still talks to Excalidraw's own infrastructure: a shared link is stored on their server, a live session keeps its scene in Google Firestore, the fonts come from a CDN, and an analytics script loads on every visit.
None of that is a criticism of the project. It is how a free hosted service pays for itself, and all of it is configurable at build time, which is precisely why the published image cannot offer it as a setting.
This image is that build, done differently. Everything above points back at the container, and a gate in the build refuses to produce an image where any of it still points outward.
The whiteboard itself, unchanged: this is Excalidraw, drawing the way it always does.
Both of these now run on your server. The session is end-to-end encrypted, and the key never leaves the link.
| What | Upstream | Here |
|---|---|---|
| Shared links | json.excalidraw.com | this container, in SQLite |
| Live session scene | Google Firestore | this container, in SQLite |
| Pasted images | Firebase Storage | this container, in SQLite |
| Collaboration socket | oss-collab.excalidraw.com | this container |
| Fonts | a CDN | this container |
| Analytics | simpleanalyticscdn.com | removed |
| Shape library | libraries.excalidraw.com | off by default, switch below |
| Text to diagram | oss-ai.excalidraw.com | off by default, switch below |
The last two are real features rather than telemetry, so they are switches instead of a decision made for you. Off means the request never leaves your server; the feature reports an error rather than pretending to work.
Everything a drawing contains is encrypted in your browser before it is stored, with the key in
the part of the link after the #, which browsers never send to a server. The container holds
bytes it cannot read.
Three processes behind one nginx:
The addresses in the app are relative paths, not an absolute URL built from a variable you have to set. Your browser resolves them against whatever address you opened, so the same image works on a LAN IP, behind a reverse proxy and under a subdomain with nothing to configure.
Search for excalidraw in Community Applications, or add the container by hand:
docker run -d \
--name excalidraw \
-p 8080:80 \
-p 8443:443 \
-v /mnt/user/appdata/excalidraw:/config \
--restart unless-stopped \
ghcr.io/junkerderprovinz/excalidraw:latest
Then open https://your-server:8443.
Use the HTTPS port. Live collaboration needs crypto.subtle, which browsers only provide in a
secure context, so over plain HTTP the session button fails with a cryptography error. The
container generates a self-signed certificate on first start and keeps it in /config, so your
browser only has to be told once. The HTTP port stays for the single-user case where no session is
ever started.
| Variable | Default | What it does |
|---|---|---|
ENABLE_LIBRARY | false | Set to true to let the shape library load from libraries.excalidraw.com. |
ENABLE_AI | false | Set to true to let the text-to-diagram feature send your text to oss-ai.excalidraw.com. |
STORE_DB | /config/store.sqlite | Where drawings, rooms and images are kept. |
TZ | Etc/UTC | Time zone for the log. |
/config holds the database and the certificate. Back it up and you have backed up everything.
Point your proxy at port 80 of the container and let it terminate TLS. The app only ever uses relative paths, so nothing needs to know its own address. Two things the proxy has to allow:
/socket.io/, or live collaboration cannot connect.git clone https://github.com/junkerderprovinz/excalidraw.git
cd excalidraw
docker build -t excalidraw .
The build takes a while, because it compiles Excalidraw from source. The last step is the gate: it searches the finished app for every address that should be gone and fails the build if it finds one, so an image that calls home cannot be produced by accident.
The Go store has its own tests:
cd backend && go test ./...
The upstream commit is pinned in the Dockerfile as EXCALIDRAW_SHA, so the image
does not change under you. To move it forward, set the new commit and rebuild. If the replaced file
has changed upstream, the build fails at the TypeScript step rather than silently shipping a broken
whiteboard, and the gate independently checks that the two switchable addresses are still where the
runtime expects them.
This repository is licensed under AGPL-3.0 (see LICENSE).
Excalidraw itself is MIT-licensed
and belongs to the Excalidraw team. The two replaced files in frontend/ are derived from theirs
and say so in their headers. This project is not affiliated with or endorsed by Excalidraw.
One knight builds this, and AI is one of the tools I work with, the same way I work with an editor or a compiler. It helps me write code and documentation and it checks my work, and that saves me a good many evenings. It does not make the decisions, though. I read and understand everything before it ships, and if something here breaks, that is on me and not on the tool.
You do not have to take my word for it. The code is open and every release note is written by hand. The issue tracker shows how problems actually get handled, including the ones I got wrong the first time. If you find something that is not right, open an issue and I will look at it.
Questions via the support thread, bugs, ideas and feature requests via GitHub issues.
A one-knight job: I build it, keep it running, work through the issues and add what people ask for, until nothing is missing. It is free, with no accounts, no telemetry, no ads and no paid tier. No asterisk anywhere. Nothing readable ever leaves your own walls. Forged on evenings and weekends, with heart and stubbornness.
If it has earned a place on your server or computer, toss a coin to your knight: it helps cover the costs and keeps the project alive. It also makes this knight's heart beat a little faster. Three ways below, whichever suits you.
Content type
Image
Digest
sha256:9e3884b16…
Size
105.7 MB
Last updated
about 21 hours ago
docker pull junkerderprovinz/excalidraw