Skip to content
 
 

Repository files navigation

Excalidraw (Self-Hosted, Shared Room)

Self-hosted Excalidraw with a runtime-configurable collaboration server and a persistent shared room — everyone who opens the app lands in the same board, no room links required.

Prerequisites

Projects

Name Description
excalidraw (submodule) Upstream Excalidraw client, built from source
excalidraw-room (submodule) Upstream collaboration server, built from source

Both are built from source via Dockerfile and Dockerfile.room respectively — no prebuilt images are pulled, so the stack works on any host architecture.

Installation

  1. Clone the repository including submodules: git clone --recurse-submodules git@github.com:<your-username>/excalidraw-docker.git
  2. Navigate to the root: cd excalidraw-docker
  3. Copy .env.example to .env and adjust the values accordingly (see Configuration).
  4. Leave ROOM_HASH empty for now — you generate it on first run (see Generating the shared room hash).

Run

Docker

docker compose up -d --build

This builds and starts both services on an internal Docker network. docker-compose.override.yml is picked up automatically and exposes them locally:

Service URL
excalidraw http://localhost:3000
excalidraw-collab-server http://localhost:3002

Check both are up — the collab server reports service running on its root path and has a healthcheck wired to it:

docker compose ps

To verify collaboration works, open http://localhost:3000 in two different browsers (or one normal and one incognito window). With ROOM_HASH set, both should land in the same board automatically and see each other draw in real time.

Generating the shared room hash

The shared room is identified by a room ID and a client-side encryption key, which Excalidraw generates when a collaboration session first starts. You create one once, then pin it via ROOM_HASH:

  1. Start the stack with ROOM_HASH empty.
  2. Open the app, then Share → Live collaboration to start a session.
  3. Copy the part of the URL after #room= — e.g. 1f62e10b0baae8414d16,57nradTETCWtSF0aQrlkog.
  4. Set it as ROOM_HASH in .env (without the room= prefix) and restart: docker compose up -d --build.

From then on, every visitor hitting the root URL is redirected into that room automatically. Changing rooms later is just a matter of changing the variable — nothing is baked into the image.

Note: the collaboration server is a stateless relay — it stores nothing. The board only lives in each participant's localStorage, which browsers evict over time. For anything long-lived, treat the exported .excalidraw file as the source of truth: import at the start of a session, export at the end.

Configuration

Variable Used by Description
ROOM_HASH excalidraw Fixed <room-id>,<key> value, auto-injected into index.html so every visitor lands in the same shared room
APP_WS_OLD_SERVER_URL excalidraw The hardcoded upstream collab URL to replace (rarely needs changing)
APP_WS_NEW_SERVER_URL excalidraw URL the client connects to for collaboration — http://localhost:3002 locally, your public domain in production

Variables are read from .env in the repository root and passed into the client container via env_file. The collab server needs no configuration.

Deployment

This setup is self-hosted — deploy it on your own infrastructure, at whatever domain you choose. Neither service publishes ports in docker-compose.yml; both listen on port 80 inside the container and are meant to sit behind a reverse proxy.

Both services can share a single domain: route the /socket.io path to the collab server and everything else to the client. Excalidraw's Socket.IO client hardcodes that path, so it cannot be moved to a custom one like /ws. A separate subdomain for the collab server works just as well if you prefer — just point APP_WS_NEW_SERVER_URL at it.

Coolify

  1. Create a new resource of type Docker Compose and point it at this repository.
  2. Under Domains, set:
    • excalidraw → https://draw.example.com
    • excalidraw-collab-server → https://draw.example.com/socket.io
  3. Turn off Strip Prefix on the collab server's domain. Socket.IO needs the /socket.io path to reach the server intact; with stripping enabled the request arrives as / and the handshake fails.
  4. Under Environment Variables, add the values from Configuration and tick Is Literal? on each. Leave ROOM_HASH empty for the first deploy.
  5. Deploy, then generate the room hash against the live instance (see Generating the shared room hash), set ROOM_HASH, and redeploy.

Do not put Traefik labels in the compose file — Coolify manages routing itself, and it escapes $ in labels, so ${VAR} interpolation there silently does not work.

If the domain sits behind Cloudflare Access, add a bypass policy for the /socket.io path. Access intercepts the WebSocket handshake and the browser fails to connect, even though the app itself loads fine from an already-authenticated session.

To debug a failing connection, check the handshake directly — a working collab server answers with a Socket.IO payload, not HTML:

curl -i "https://draw.example.com/socket.io/?EIO=4&transport=polling"

Other reverse proxies

Route /socket.io to the collab server and everything else to the client, both on port 80 in-container. Pass the path through unmodified, and make sure WebSocket upgrades are forwarded — this works out of the box with Caddy and Cloudflare Tunnel, and with nginx once the usual Upgrade/Connection headers are set.

License

This repository is a fork of pmoscode-helm/excalidraw-docker, built on top of Excalidraw and excalidraw-room. Licensed under the MIT License.

About

Self-hosted Excalidraw with a runtime-configurable collaboration server and a persistent shared room — everyone who opens the app lands in the same board, no room links required.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages