
A self-hosted dashboard and organizer for Excalidraw with live collaboration features.

Persistent storage for all your drawings

Real time collaboration

Version history and restore
Automatically retain recent drawing snapshots, preview past versions from the editor, and restore a previous state when needed.
(Optional) Multi User Authentication, OIDC Support




Scoped internal & external sharing

Search your drawings

Drag and drop drawings into collections

Export/import your drawings for backup

See release notes for a specific release.
ExcaliDash includes an in-app update notifier that checks GitHub Releases. If your deployment must not make outbound network calls, disable it on the backend:
UPDATE_CHECK_OUTBOUND=false
If you deployed using docker-compose.prod.yml (Docker Hub images), upgrade by pulling the latest images and recreating containers:
docker compose -f docker-compose.prod.yml pull && \
docker compose -f docker-compose.prod.yml up -d
If you prefer a clean stop/start (more downtime, but simpler), you can do:
docker compose -f docker-compose.prod.yml down && \
docker compose -f docker-compose.prod.yml pull && \
docker compose -f docker-compose.prod.yml up -d
Notes:
-v to down unless you intend to delete the persistent backend volume (your SQLite DB + secrets).--remove-orphans if you previously ran a different Compose file for the same project name and need to remove old/renamed services.[!CAUTION] This is a BETA deployment and production-readiness depends on deployment controls: use TLS, trusted reverse proxy, fixed secrets, backups, and endpoint rate limits.
[!CAUTION] ExcaliDash is in BETA. Please backup your data regularly.
Prereqs: Docker + Docker Compose v2.
Docker Hub (Recommended)
# Download docker-compose.prod.yml
curl -OL https://raw.githubusercontent.com/ZimengXiong/ExcaliDash/main/docker-compose.prod.yml
# Pull images
docker compose -f docker-compose.prod.yml pull
# Run container
docker compose -f docker-compose.prod.yml up -d
# Access the frontend at localhost:6767
For single-container deployments, JWT_SECRET can be omitted and will be auto-generated and persisted in the backend volume on first start. For portability and most production deployments, set a fixed JWT_SECRET explicitly.
By default, the provided Compose files set TRUST_PROXY=false for safer setup. Only set TRUST_PROXY to a positive hop count (for example, 1) when requests always pass through a trusted reverse proxy that correctly sets forwarded headers.
Docker Build
# Clone the repository (recommended)
git clone git@github.com:ZimengXiong/ExcaliDash.git
# or, clone with HTTPS
# git clone https://github.com/ZimengXiong/ExcaliDash.git
docker compose build
docker compose up -d
# Access the frontend at localhost:6767
Reverse Proxy / Traefik
When running ExcaliDash behind Traefik, Nginx, or another reverse proxy, configure both containers so that API + WebSocket calls resolve correctly:
| Variable | Purpose |
|---|---|
FRONTEND_URL |
Backend allowed origin(s). Must match the public URL users access (for example https://excalidash.example.com). Supports comma-separated values for multiple addresses. |
TRUST_PROXY |
Set to 1 when traffic passes through one trusted reverse-proxy hop (for example frontend nginx -> backend) and headers are sanitized. |
BACKEND_URL |
Frontend container-to-backend target used by Nginx. Override when backend host differs from default service DNS/host. |
ENFORCE_HTTPS_REDIRECT |
When FRONTEND_URL uses https://, the backend automatically redirects plain-HTTP requests to HTTPS. Set to false if your outer gateway already enforces HTTPS and you want to disable the built-in redirect (avoids redirect loops when X-Forwarded-Proto is not forwarded). Default: true. |
# docker-compose.yml example
backend:
environment:
# Single URL
- FRONTEND_URL=https://excalidash.example.com
# Trust exactly one reverse-proxy hop
- TRUST_PROXY=1
# Or multiple URLs (comma-separated) for local + network access
# - FRONTEND_URL=http://localhost:6767,http://192.168.1.100:6767,http://nas.local:6767
# If your outer gateway enforces HTTPS and X-Forwarded-Proto is not forwarded,
# disable the built-in redirect to prevent redirect loops:
# - ENFORCE_HTTPS_REDIRECT=false
frontend:
environment:
# For standard Docker Compose (default)
# - BACKEND_URL=backend:8000
# For Kubernetes, use the service DNS name:
- BACKEND_URL=excalidash-backend.default.svc.cluster.local:8000
Scaling / HA (Current Limitations)
ExcaliDash currently supports running one backend instance.
Why:
| Area | Limitation |
|---|---|
| Database | The backend uses SQLite by default. For production deployments requiring high availability, use PostgreSQL (DATABASE_PROVIDER=postgresql). SQLite still works for single-instance deployments but is not recommended for multi-replica setups. |
| Collaboration | Real-time presence state is tracked in-memory in the backend process, so multiple replicas will fragment presence/collaboration unless a shared Socket.IO adapter is added. |
Recommended deployment pattern:
| Component | Guidance |
|---|---|
| Backend | 1 replica, persistent volume, regular backups. |
| Frontend | 1 replica is simplest; scaling is generally fine since it is stateless. |
Auth, Onboarding, and First Admin Setup
ExcaliDash supports local login and OIDC, and includes a one-time first-admin bootstrap key to protect initial setup/migration flows.
Auth modes:
AUTH_MODE |
Behavior |
|---|---|
local (default) |
Native email/password login only. |
hybrid |
Native login plus OIDC login. |
oidc_enforced |
OIDC-only login (/auth/register and /auth/login disabled). |
If you upgrade and see an onboarding/setup flow, follow the UI. For emergency-only operator access, you can temporarily bypass the onboarding gate:
DISABLE_ONBOARDING_GATE=true docker compose -f docker-compose.prod.yml up -d
One-time first-admin bootstrap setup code (local auth only):
| What | Notes |
|---|---|
| When required | Auth enabled and no active users (fresh install or certain migrations). |
| Where to find it | Backend logs: [BOOTSTRAP SETUP] One-time admin setup code .... |
| Behavior | Single-use; if you enter an invalid/expired code, check logs for the refreshed code. |
Find the current code in logs:
docker compose -f docker-compose.prod.yml logs backend --tail=200 | grep "BOOTSTRAP SETUP"
OIDC configuration (for hybrid / oidc_enforced) requires these backend env vars:
backend:
environment:
- AUTH_MODE=oidc_enforced
- OIDC_PROVIDER_NAME=Authentik
- OIDC_ISSUER_URL=https://auth.example.com/application/o/excalidash/
# Optional split-horizon setup when backend reaches IdP via internal DNS.
# Keep OIDC_ISSUER_URL browser-routable; set OIDC_DISCOVERY_URL for backend-only access.
# - OIDC_DISCOVERY_URL=http://auth-internal:9000/application/o/excalidash/
- OIDC_CLIENT_ID=your-client-id
# Optional for public clients; required for confidential clients
# - OIDC_CLIENT_SECRET=your-client-secret
# Optional token endpoint auth override (useful for some IdPs/HS setups)
# - OIDC_TOKEN_ENDPOINT_AUTH_METHOD=client_secret_post
# Optional override when your IdP client is configured for a non-default ID token alg
# - OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG=HS256
- OIDC_REDIRECT_URI=https://excalidash.example.com/api/auth/oidc/callback
- OIDC_SCOPES=openid profile email
# Optional: path to groups/roles claim in ID token/user claims (supports dot path)
- OIDC_GROUPS_CLAIM=groups
# Optional: comma-separated group names that should be ADMIN in ExcaliDash
- OIDC_ADMIN_GROUPS=excalidash-admins,platform-admins
Quick preflight check (recommended before starting backend):
cd backend
npm run oidc:doctor
Provider-specific env templates for existing IdPs:
backend/.env.oidc.keycloak.examplebackend/.env.oidc.authentik.exampleCopy one to backend/.env, update issuer/client/redirect values, then run npm run oidc:doctor.
Notes:
| Topic | Notes |
|---|---|
OIDC-only (oidc_enforced) |
You typically do not use local bootstrap admin registration; first admin can be created through your IdP depending on config. |
| Reverse proxy | Set FRONTEND_URL and TRUST_PROXY correctly or auth + websockets may fail. |
| ID token algorithm | ExcaliDash defaults to RS256. If your IdP client is explicitly configured for another signed ID-token algorithm such as HS256, set OIDC_ID_TOKEN_SIGNED_RESPONSE_ALG to match that exact client setting. none is not allowed, and HS* requires OIDC_CLIENT_SECRET. |
| Keycloak issuer format | Use realm issuer URL: https://<keycloak-host>/realms/<realm>. |
| Authentik issuer format | Use provider issuer URL: https://<authentik-host>/application/o/<provider-slug>/. |
Authentik email_verified |
If Authentik does not emit email_verified=true, either add the scope mapping or set OIDC_REQUIRE_EMAIL_VERIFIED=false. |
| Redirect URI | Must be exact callback: https://<excalidash-host>/api/auth/oidc/callback. |
| Split-horizon IdP networking | Set OIDC_ISSUER_URL to the browser-reachable issuer and optionally OIDC_DISCOVERY_URL to a backend-reachable internal URL. |
| OIDC admin mapping | If OIDC_ADMIN_GROUPS is set, admin role is reconciled on each authenticated request for OIDC users: users in those groups are promoted to ADMIN, users not in those groups are demoted to USER. |
| Legacy sessions |
$ claude mcp add ExcaliDash \
-- python -m otcore.mcp_server <graph>