Skip to content

Self-hosting

For running Sentori on your own infra. The reference deploy is the docker-compose in this repo.

┌─────────────────┐
browser ──HTTPS──▶ │ reverse proxy │ (your TLS terminator)
│ caddy / nginx │
└────────┬─────────┘
│ http
┌────────▼─────────┐
│ sentori-web │ nginx + SPA bundle
│ /admin/api ▶───┼──────────┐
└──────────────────┘ │
┌──────────────────┐ │
SDK on phone ──HTTPS──────▶ │ sentori-server │ ◀────────┘
│ axum + Rust │
└────┬─────────┬───┘
│ │
┌──────────────▼─┐ ┌───▼─────────┐
│ postgres 18 │ │ valkey 8 │
│ (events, │ │ (rate │
│ issues, │ │ limit │
│ tokens, ...) │ │ counters) │
└────────────────┘ └─────────────┘

Optional: SMTP relay (any provider) for new-issue email.

Save to .env next to docker-compose.yml:

Terminal window
SENTORI_DEV_TOKEN=st_pk_$(head /dev/urandom | base32 | tr A-Z a-z | tr -d '=' | head -c 26)
SENTORI_ADMIN_PASSWORD=$(openssl rand -hex 12)
SENTORI_SESSION_SECRET=$(openssl rand -hex 32)
SENTORI_PG_PASSWORD=$(openssl rand -hex 16)

docker compose up will refuse to start if any of these are unset.

VarDefaultUse
SENTORI_RATE_LIMIT_PER_MIN1000per-token request limit
SENTORI_SMTP_HOSTunsetenables new-issue email; if unset, no email is sent
SENTORI_SMTP_PORT587STARTTLS
SENTORI_SMTP_USERunsetoptional auth
SENTORI_SMTP_PASSunsetoptional auth
SENTORI_SMTP_FROMsentori@localhostFrom: address
SENTORI_DATA_DIR/datasource-map blob storage path
SENTORI_ATTACHMENT_DIRunsetlocal-fs path for per-event attachments (screenshots, view trees). When unset, the upload endpoint returns 503 attachmentsDisabled and the SDK silently skips.
SENTORI_WEB_PORT8000host port for the web container
SENTORI_TRACE_RETENTION_DAYS14how long spans + traces are kept (see Data retention below)
SENTORI_SPAN_LIMIT_MONTHLY10000000per-org monthly span-ingest budget, separate from the error-event quota; 0 = unlimited
SENTORI_SELF_TRACE_PROJECT_IDunsetif set to a project UUID, the server emits its own http.server spans into that project
RUST_LOGinfo,sentori_server=info,tower_http=info

Copy docker-compose.override.example.yml to docker-compose.override.yml and edit. The override is auto-merged by docker compose.

A daily background pass (retention.rs) manages the time-partitioned tables:

  • events are kept for the longest plan retention across all orgs, floor 30 days. Errors are the high-value signal — keep them.
  • spans + traces are kept for SENTORI_TRACE_RETENTION_DAYS (default 14). Traces are high-volume and lower-value than errors, so a short hard window keeps storage bounded; recent traces stay 100% complete (Sentori does not sample at ingest). Set it longer if you have the disk; set it shorter to be aggressive.
  • event attachments (screenshots, view trees) follow the events cutoff — same daily pass deletes the event_attachments row and the on-disk blob via AttachmentStore::delete_event. Allocate SENTORI_ATTACHMENT_DIR somewhere with at least the same retention window’s worth of headroom; typical screenshot is 50-100 KB, an org producing 100 errors/day with screenshots on burns ~5 MB/day, ~150 MB over the 30-day floor.

The same pass keeps ~6 months of empty monthly partitions ahead of “now” so writes never spill into the *_default catch-all partition — so don’t stop the server for months at a time and expect partition hygiene to keep up. Expired monthly partitions are DROP TABLE-d (an instant metadata op); the traces table (not partitioned) is pruned with a delete.

Terminal window
docker compose up -d
docker compose ps # postgres should be "healthy"
docker compose logs -f server

The web UI lives at http://localhost:8000 by default.

Each project’s settings page in the dashboard has a Recipients panel for managing notification emails. For a quick CLI route you can also insert rows directly:

Terminal window
docker compose exec postgres psql -U sentori -d sentori -c "
INSERT INTO notification_recipients (id, project_id, email, on_new_issue)
VALUES (
gen_random_uuid(),
'019508a0-0000-7000-8000-000000000000', -- DEV_PROJECT_ID
'oncall@example.com',
true
);
"

Verify SMTP wiring by triggering any new fingerprint and watching the server logs:

Terminal window
docker compose logs -f server | grep -E 'new-issue|notifier'

So dashboard stack traces resolve to your source. After a release build, with @goliapkg/sentori-cli (npx works without installing):

Terminal window
npx @goliapkg/sentori-cli@latest upload sourcemap \
--release "myapp@1.2.3+456" \
--token "$SENTORI_DEV_TOKEN" \
--api-url "https://sentori.your-host.com" \
./bundle/ # a dir (scanned for *.map / *.js / *.bundle) or specific files

--release must equal the value the SDK reports via init({ release }). --token falls back to $SENTORI_TOKEN, --api-url to $SENTORI_API_URL. React Native (Hermes) needs the Metro + Hermes maps composed first — see docs → Recipes → “Source map upload” → React Native.

Files are deduped by sha256 and stored under SENTORI_DATA_DIR/artifacts/.

For local development — dashboard polish, query EXPLAIN baselines, performance audits — tools/seed-events.ts posts synthetic events directly to the ingest endpoint so you can see the dashboard with realistic shape without waiting on production users.

Terminal window
# 5,000 events across 200 user IDs and 10 release tags, last 7 days,
# with ~5% ANR mixed in.
bun tools/seed-events.ts \
--token "$SENTORI_DEV_TOKEN" \
--events 5000 --users 200 --releases 10 \
--include-anr \
--ingest-url http://localhost:8080

Each event is tagged synthetic: seed-events so you can clean up later with:

Terminal window
docker compose exec postgres psql -U sentori -d sentori -c \
"DELETE FROM events WHERE payload->'tags'->>'synthetic' = 'seed-events'"

To also simulate regressions (resolve some issues then re-post their fingerprints so the server flips them to regressed), pass an admin token + project ID:

Terminal window
bun tools/seed-events.ts ... \
--include-regression \
--admin-token "$SENTORI_ADMIN_TOKEN" \
--project-id 019508a0-0000-0000-0000-000000000000 \
--api-url http://localhost:8080

Postgres is the source of truth. A nightly logical backup is the v0.1 recommendation:

Terminal window
docker compose exec -T postgres pg_dump -U sentori sentori \
| gzip > backups/sentori-$(date +%F).sql.gz

Restore:

Terminal window
gunzip -c backups/sentori-2026-05-09.sql.gz \
| docker compose exec -T postgres psql -U sentori -d sentori

The server-data volume holds source-map blobs (re-uploadable from CI), so it’s nice-to-have but not critical.

Terminal window
git pull
docker compose pull # if using prebuilt ghcr.io images
docker compose up -d --build

Migrations run on server boot via sqlx::migrate!. v0.1 migrations are forward-only; for rollback, restore from a pre-update DB backup.

The web container speaks plain HTTP. Front it with Caddy / nginx / Cloudflare for TLS:

sentori.example.com {
reverse_proxy localhost:8000
}

The SDK ingest endpoint is the same host: SDKs send to https://sentori.example.com/v1/events, the web reverse-proxies /admin/api to the server, and /v1/* goes through unchanged because the web container only intercepts paths it knows about.

  • Pin docker images to a specific SHA tag (replace :latest)
  • Off-host Postgres with a read replica
  • WAL archiving + PITR
  • Run docker daemon as a non-root user
  • Cloudflare (or equivalent) in front for DDoS / WAF
  • Dedicated SMTP via Postmark / SES instead of shared providers

For the production setup history, see the CHANGELOG (v0.1.x — Phase 16).