# Advanced deployment and operations This page contains the advanced deployment, authentication, OIDC, offline, backup, and operational notes that were previously in the main README. ## Advanced
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`. | ```yaml # 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 a local **SQLite file** database by default (`DATABASE_URL=file:/.../dev.db`). Running multiple backend replicas either creates split-brain state (separate DB files/volumes) or requires sharing a single SQLite file across hosts, which is not a reliable deployment pattern. | | 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: ```bash 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: ```bash 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: ```yaml 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): ```bash cd backend npm run oidc:doctor ``` Provider-specific env templates for existing IdPs: - `backend/.env.oidc.keycloak.example` - `backend/.env.oidc.authentik.example` Copy 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:///realms/`. | | Authentik issuer format | Use provider issuer URL: `https:///application/o//`. | | Authentik `email_verified` | If Authentik does not emit `email_verified=true`, either add the scope mapping or set `OIDC_REQUIRE_EMAIL_VERIFIED=false`. | | Microsoft Entra `email_verified` | Entra ID often omits `email_verified`. Either add a claim mapping policy that emits `email_verified=true` for your service principal, or set `OIDC_REQUIRE_EMAIL_VERIFIED=false` for trusted internal deployments. | | Redirect URI | Must be exact callback: `https:///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 | Users with old sessions (issued before group claims were embedded) should sign out/in once so OIDC group claims are refreshed. | | OIDC_DISCOVERY_URL | In Docker Compose or Kubernetes the backend container may not be able to reach your IdP's public hostname. Set `OIDC_DISCOVERY_URL` to an internal URL so the backend can fetch `.well-known/openid-configuration` without changing `OIDC_ISSUER_URL`, which must stay as the public URL for issuer validation and browser redirects. |
Local OIDC Test Stack (Docker + Keycloak) ### Local OIDC Test Stack (Docker + Keycloak) This repo includes a Keycloak container + realm seed for local OIDC testing: - Compose file: `docker-compose.oidc.yml` - Realm import: `oidc/keycloak/realm-excalidash.json` The realm seed intentionally contains **no users and no passwords**. You create a realm user and set a password via the Keycloak admin UI. Start Keycloak: ```bash # From repo root # Choose a strong password; do not commit it. export KEYCLOAK_ADMIN_PASSWORD='...' docker compose -f docker-compose.oidc.yml up -d ``` Open Keycloak admin UI (realm/user setup): - `http://localhost:8080/admin` - Switch realm to `excalidash` - Create a user and set a password in `Credentials` Configure ExcaliDash backend for hybrid OIDC: ```bash cd backend cp .env.oidc.example .env # If backend runs in Docker and Keycloak issuer is localhost for browser, set: # OIDC_DISCOVERY_URL=http://keycloak:8080/realms/excalidash # Ensure OIDC_REDIRECT_URI matches where your frontend is running: # - http://localhost:6767/api/auth/oidc/callback (repo frontend dev default) # - https://excalidash.example.com/api/auth/oidc/callback (production) ``` Stop/clean up: ```bash docker compose -f docker-compose.oidc.yml down ```
Configuration (Backend Environment Variables) Base values are documented in `backend/.env.example`. Common ones to care about: | Variable | Default / Example | Description | | ------------------------ | ------------------------- | ----------------------------------------------------------------------------------- | | `DATABASE_PROVIDER` | `sqlite` | Database provider: `sqlite` or `postgresql`. See Database provider below. | | `DATABASE_URL` | `file:/app/prisma/dev.db` | SQLite file or external DB URL. | | `FRONTEND_URL` | `http://localhost:6767` | Allowed frontend origin(s), comma-separated for multiple entries. | | `TRUST_PROXY` | `false` | `false`, `true`, or hop count (for example `1`). | | `JWT_SECRET` | `change-this-secret...` | Recommended in production so sessions remain stable across restarts and migrations. | | `CSRF_SECRET` | `change-this-secret` | Recommended in production so CSRF validation remains stable across restarts. | | `AUTH_MODE` | `local` | `local`, `hybrid`, `oidc_enforced`. | | `ENFORCE_HTTPS_REDIRECT` | `true` | Set to `false` to disable the built-in HTTP→HTTPS redirect when your outer gateway handles it. | | `PASSWORD_MIN_LENGTH` | `12` | Local-auth password minimum length. Combine with `PASSWORD_REQUIRE_*` flags to relax or enforce complexity. | | `BACKUP_SCHEDULE` | unset | Optional 5- or 6-field cron expression for scheduled SQLite backups, e.g. `0 0 4 * * *`. | | `BACKUP_DIR` | `/app/backups` | Directory where scheduled SQLite backup files are written. Mount this to persistent storage. |
Database provider ExcaliDash supports SQLite by default and PostgreSQL for deployments that want an external database. Set `DATABASE_PROVIDER` and `DATABASE_URL` together so startup, Prisma generation, and migrations all target the same provider. SQLite is the recommended single-instance path: ```yaml backend: environment: - DATABASE_PROVIDER=sqlite - DATABASE_URL=file:/app/prisma/dev.db volumes: - /mnt/user/appdata/excalidash/prisma:/app/prisma ``` PostgreSQL uses a normal Prisma PostgreSQL connection string: ```yaml backend: environment: - DATABASE_PROVIDER=postgresql - DATABASE_URL=postgresql://user:password@postgres:5432/excalidash ``` The Docker entrypoint copies the matching provider-specific migration folder and rewrites the runtime Prisma schema before running Prisma commands. For schema changes, generate migrations through the provider helper so checked-in migrations stay separated: ```bash export DATABASE_URL="postgresql://user:password@localhost:5432/excalidash" ./scripts/generate-migrations.sh --dev add_feature_name ``` The helper prompts for SQLite or PostgreSQL, sets `DATABASE_PROVIDER`, and writes the generated migration under `backend/prisma/migrations//`. Switching providers does not migrate existing data between SQLite and PostgreSQL; treat the target database as a separate install unless you build and verify an explicit data migration.
Offline / No-network deployments ExcaliDash packages the Excalidraw runtime assets into the frontend image and sets `window.EXCALIDRAW_ASSET_PATH` to the local origin, so browser operations such as inserting images and exporting drawings do not need `unpkg.com` or other public CDNs. The production Nginx CSP is intentionally self-hosted for scripts, styles, and fonts. The backend update checker is the remaining optional outbound call. Disable it for sealed networks: ```bash UPDATE_CHECK_OUTBOUND=false ```
Password policy, preferences, and backups Local authentication defaults to the previous strong policy: 12-100 characters with uppercase, lowercase, number, and symbol requirements. Internal deployments can relax it with environment variables: ```bash PASSWORD_MIN_LENGTH=10 PASSWORD_REQUIRE_UPPERCASE=false PASSWORD_REQUIRE_LOWERCASE=false PASSWORD_REQUIRE_NUMBER=false PASSWORD_REQUIRE_SYMBOL=false ``` The active policy is exposed through `/api/auth/status`, so registration, password reset, admin-created users, and profile password changes all validate against the same backend policy. Per-user theme and dashboard sort preferences are stored server-side once a user is authenticated, with localStorage as a pre-login/offline fallback. The app-shell display font can be replaced at build time with `VITE_EXCALIDASH_UI_FONT_FAMILY` and a self-hosted WOFF2 file via `VITE_EXCALIDASH_UI_FONT_URL`. Excalidraw canvas font support remains governed by the embedded Excalidraw package. For scheduled SQLite backups, mount a host path and configure a cron schedule: ```yaml backend: environment: - DATABASE_URL=file:/app/prisma/dev.db - BACKUP_SCHEDULE=0 0 4 * * * - BACKUP_DIR=/app/backups - BACKUP_RETENTION_DAYS=14 volumes: - /mnt/user/appdata/excalidash/prisma:/app/prisma - /mnt/user/backups/excalidash:/app/backups ``` For Unraid or other Docker templates, map the host directory to container path `/app/prisma` and keep `DATABASE_URL=file:/app/prisma/dev.db`; named volumes are harder to inspect and easier to accidentally recreate.
Community integrations - **[excalidash-mcp](https://github.com/davifernan/excalidash-mcp)** — community MCP server that lets AI agents draw and edit ExcaliDash boards live.