Self-hosting
The same image runs on your host or ours. This page is the operator’s view: what to set, what to put in front of it, and how to keep it running with nothing leaving the building.
Layout
- One Python process runs the chat listener, the scheduler and the API with the dashboard in a single event loop.
- The container listens on the loopback interface only; a reverse proxy (Nginx or Caddy) terminates TLS and forwards to it.
- State lives in a data directory: the SQLite database, uploads, GeoIP data, generated keys and model weights. Point DATABASE_URL at Postgres for multi-host deployments; the additive migration set works on both.
- The chat server (Synapse or another Matrix homeserver) can be anywhere reachable.
Environment
| MATRIX_HOMESERVER, MATRIX_USER_ID, MATRIX_ACCESS_TOKEN | the bot account |
|---|---|
| MATRIX_STANDUP_ROOM_ID | the default standup room |
| MATRIX_ADMIN_TOKEN | homeserver admin token for provisioning and lookups; environment only, never persisted |
| SECRET_KEY | a 64+ character random string signing dashboard sessions |
| DATABASE_URL | sqlite by default, or a Postgres URL |
| BOOTSTRAP_ADMIN_MATRIX_ID | the first admin, promoted on startup |
| AI_PROVIDER and the provider key | see AI providers |
| INTEGRATION_VAULT_KEY | the key encrypting stored integration credentials |
| TZ, MAX_BODY_BYTES, AI_REASONING_MODEL | optional |
Reverse proxy
- Forward X-Forwarded-For and X-Forwarded-Proto; without the latter the product cannot tell HTTP from HTTPS and skips the HSTS header.
- Enable compression for proxied responses and HTTP/2; the dashboard pulls dozens of assets on a cold load.
- Never compress or buffer the event stream endpoint; live updates are server-sent events.
- Per-IP rate limiting is the proxy’s job; the product limits code requests and logins itself.
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; gzip_proxied any; http2 on;
Security headers and limits
Every response carries nosniff, frame denial, a no-referrer policy, a restrictive permissions policy, a two-year HSTS on TLS and a content-security policy. Request bodies are capped at 5 MiB by default. Every input is validated by schema before a handler runs; chat IDs are regex-checked before any homeserver call; outbound HTTP is pinned to the homeserver and the configured provider host.
Operate
- Upgrade: pull, rebuild, restart. Migrations run on boot and skip what exists.
- Back up the data directory nightly; for Postgres use pg_dump.
- Health: a fast unauthenticated liveness endpoint for the container check and an admin-gated detailed one with uptime, database size and provider status.
- GeoIP: run the refresh script weekly (keyless db-ip Lite, or MaxMind with a free key); a copy in the data volume is picked up on restart with no configuration.
- The scheduler runs the workday: presence samples every five minutes, absence checks every fifteen, nightly roll-ups and score materialisation, digests and reports on their cadences.
docker compose up -d --build docker exec inroots sqlite3 /app/data/standup.db ".backup /tmp/backup.db" GEO_DATA_DIR=/app/data/geo scripts/update_geoip.sh
Air-gapped
With a self-hosted Matrix server, Ollama as the provider, a local IP database on the host, spoken replies left off and no integrations enabled, the product needs no internet at all. There is no telemetry, no usage beacon and no crash reporter calling out; zero bytes leave unless you configure a destination.