All‑in‑one Excalidraw deployment with a Python FastAPI backend. Ships a built frontend, a simple persistence API compatible with Excalidraw, and a lightweight admin UI.
- Overview
- Screenshots
- Quick Start
- Configuration
- Admin UI
- APIs
- Build & Deploy (GitHub Actions)
- Reverse Proxy (Caddy)
- Maintenance (Makefile)
- Troubleshooting
- Notes
- Serves the Excalidraw frontend (built inside the image).
- Save/load binary protocol compatible with Excalidraw.
- Simple web admin to list/open/delete documents and set names (stores share keys server‑side so items are directly openable).
- Minimal Firebase proxy endpoints used by Excalidraw.
- Storage backends: memory (default) or filesystem.
# Build
make build
# Start (detached)
make up
# Specific upstream version
make build EXCALIDRAW_REF=v0.17.3
# Foreground
make up-fg
# Stop / cleanup
make down
make clean-image
make clean-dataNote: Without make, you can run the equivalent docker buildx and docker run commands directly.
Build‑time (frontend endpoints)
PUBLIC_ORIGIN(required for production): e.g.,https://chart.example.comWS_ORIGIN(optional): defaults toPUBLIC_ORIGINEXCALIDRAW_REPO(optional): upstream repo URL (default official)EXCALIDRAW_REF(optional): branch/tag/commit (defaultmaster)
Runtime (container env)
STORAGE_TYPE:memory|filesystemLOCAL_STORAGE_PATH: data path for filesystem storage (default/app/data)PUBLIC_ORIGIN: admin page uses this origin when opening documents in the main app
Access
http://127.0.0.1:8888/admin
Behavior
- Admin lists only canvases that have a stored share key (i.e., can be opened).
- Use "Add Canvas" to paste a share link and optional name; Admin stores the key server-side so Open/Copy Link work directly on
PUBLIC_ORIGIN. - Frontend injection adds a "Save to Admin" button next to Excalidraw's Share/Copy Link UI, so you can save the current share link to Admin without leaving the app.
Security
- The admin page is unauthenticated by default. Protect it via reverse proxy auth, IP allowlist, VPN, etc., for production.
- Storing share keys on the server makes canvases openable from Admin and weakens pure end‑to‑end secrecy; restrict Admin access.
- The backend injects a small helper script into the Excalidraw frontend (
index.html) at response time. - Location:
server/routes/ui.pyreadsserver/inject/save-to-admin.jsand inlines it before</head>. - No extra network fetch: the JS is injected inline; if reading fails, injection is skipped (frontend behaves normally).
- Behavior: watches the Share dialog, places a “Save to Admin” button next to “Copy link”.
- On click, it reads the share link (from the readonly input, or temporarily triggers Copy to capture it),
opens a lightweight name modal, and POSTs
{ name, key }toPOST /api/v2/admin/documents/{id}/meta.
- On click, it reads the share link (from the readonly input, or temporarily triggers Copy to capture it),
opens a lightweight name modal, and POSTs
- Scope: only affects the frontend app pages;
/admin,/api,/v1,/pingare not modified. - Disable injection: remove or empty
server/inject/save-to-admin.js(the server will skip injection if the file can’t be read).
Document management
POST /api/v2/post/— body is raw bytes →{ "id": "..." }GET /api/v2/{id}/— returns raw bytesDELETE /api/v2/{id}— delete by id
Admin
GET /admin— web interfaceGET /api/v2/admin/documents— list only openable items: id, size, createdAt, name, shareLinkPOST /api/v2/admin/documents/{id}/name— set namePOST /api/v2/admin/documents/{id}/meta— set name and share key (parsed from share link)
Firebase compatibility
POST /v1/projects/{project}/databases/{db}/documents:commitPOST /v1/projects/{project}/databases/{db}/documents:batchGet
This repo includes .github/workflows/deploy.yml to build and deploy to a remote Linux server via SSH.
Server prerequisites
- Docker Engine installed; the SSH user can run
docker(indockergroup). - Open necessary ports (e.g., 8888; or place Caddy/NGINX in front).
Repository secrets (required)
SERVER_HOST: server IP/hostnameSERVER_USER: SSH usernameSERVER_SSH_KEY: private key for SSH authPUBLIC_ORIGIN: public origin (e.g.,https://chart.example.com)- Optional
WS_ORIGIN: WebSocket origin (defaults to PUBLIC_ORIGIN)
What the workflow does
push to main
→ Buildx build (linux/amd64) with PUBLIC_ORIGIN/WS_ORIGIN baked into frontend
→ docker save image.tar.gz and upload to server via SCP
→ SSH: docker load → rm -f old container → docker run -d with env + volume
→ (Optional) Caddy proxies 80/443 to :8888
Usage
- Automatic: push to
maintriggers the workflow. - Manual: “Run workflow” in Actions; you can override
excalidraw_repoandexcalidraw_ref.
Troubleshooting
- Platform mismatch: the workflow builds for
linux/amd64to avoid arm64/amd64 conflicts. - Permissions: add SSH user to
dockergroup, or adapt the workflow to usesudo docker.
Goal
- Serve app at
chart.example.com. - Serve admin at
chart-admin.example.com, rewriting/→/admin.
Host Caddyfile (bare metal)
chart.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:8888 {
header_up Host {host}
header_up X-Forwarded-Proto {scheme}
}
}
chart-admin.example.com {
encode zstd gzip
@root path /
rewrite @root /admin
reverse_proxy 127.0.0.1:8888 {
header_up Host {host}
header_up X-Forwarded-Proto {scheme}
}
}
Dockerized Caddy (example service)
services:
caddy:
image: caddy:2
container_name: caddy
ports: ["80:80", "443:443"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on: [excalidraw]
volumes:
caddy_data:
caddy_config:
Notes
- Caddy will manage TLS automatically; ensure ports 80/443 are open and DNS points to the server.
- Using a separate admin domain forces full‑page navigation (avoids SPA/Service Worker interception).
Targets
make build— build image (amd64) with frontend URLs baked inmake up— start container detached (recreates if exists)make up-fg— start container in foregroundmake down— stop & remove containermake logs— follow logsmake ps— show container statusmake clean-image— remove built imagemake clean-data— remove local./data
Variables (override via CLI or env)
EXCALIDRAW_REPO,EXCALIDRAW_REFPUBLIC_ORIGIN,WS_ORIGINIMAGE,CONTAINER,PORT,DATA_DIR
- App not reachable:
- Check
docker psanddocker logs --tail 200 excalidraw. - Ensure port mapping
-p 8888:8888and firewall rules.
- Check
- Admin page opens on wrong origin:
- Set runtime env
PUBLIC_ORIGINto your main app domain.
- Set runtime env
- Platform mismatch (arm64 host build → amd64 server):
- Build with
--platform linux/amd64(already in CI and scripts).
- Build with
- Filesystem storage not persisted:
- Ensure volume mount to
/app/dataandSTORAGE_TYPE=filesystem.
- Ensure volume mount to
- Frontend is cloned and built during the image build.
- Static site is served with SPA fallback (
index.html). - Admin endpoints are unauthenticated; protect in production.
- Frontend URLs are set at build time via
PUBLIC_ORIGIN/WS_ORIGIN. - Admin page uses runtime
PUBLIC_ORIGINto open docs on the main app origin.


