17 KiB
Self-Hosted Sentry — Operations Runbook
This runbook covers the self-hosted Sentry deployment that lives in
infra/sentry/. It is the operational companion to
docs/improvement-plan/02-observability/sentry-selfhost-setup.md (the
planning artefact) and assumes you have current operator-approved SSH access to
the boilerplate-staging host. The archived
docs/improvement-plan/archive/STAGING-ACCESS.md file is historical inventory
only; prefer live Coolify/Hetzner access records when they differ.
For the lightweight alternative (GlitchTip, no Performance/Replay/OTLP), see
sentry-glitchtip-alternative.md.
At a glance
| Item | Value |
|---|---|
| Upstream | getsentry/self-hosted tag 25.12.1 |
| Public hostname | https://sentry.boiler.saas-ctrl.com |
| Host | boilerplate-staging (46.225.5.93, Hetzner Nürnberg) |
| Orchestrator | Coolify-managed Docker Compose, Traefik v3.6 in front |
| Persistent volumes | /data/coolify/services/sentry/{postgres,clickhouse,kafka,zookeeper,redis,symbolicator}-data |
| OTLP endpoint | https://sentry.boiler.saas-ctrl.com/api/{project_id}/integration/otlp/v1/traces |
| Backup target | s3://${SENTRY_S3_BACKUP_BUCKET}/sentry/YYYY-MM-DD/ (encrypted) |
1. First-time deploy
-
Provision DNS. Add an
Arecord forsentry.boiler.saas-ctrl.compointing at46.225.5.93. TLS is issued automatically by Traefik via theletsencryptACME resolver on first request. -
Fill secrets. SSH to the host, then:
cd /data/coolify/services/sentry git clone git@git.tool-ctrl.de:toolctrl-labs/boilerplate.git . cd infra/sentry cp .env.example .env # Edit .env — fill SENTRY_ADMIN_EMAIL, SENTRY_ADMIN_PASSWORD, # SENTRY_SECRET_KEY (generate with `python -c "import secrets; print(secrets.token_urlsafe(50))"`), # SENTRY_S3_BACKUP_*. chmod 0600 .env -
Run the installer. From
infra/sentry/:./install.shThe wrapper clones the pinned upstream tag, applies our overlays, sources
.env, and runs the upstream installer in non-interactive mode (--skip-user-prompt --no-report-self-hosted-issues). Expect 5–10 minutes on first run because Snuba migrations take a while on a cold ClickHouse. -
Verify.
- Web UI is reachable:
curl -I https://sentry.boiler.saas-ctrl.comreturns200. - Admin login works with the credentials from
.env. /auth/register/returns 404 (public signup disabled).- OTLP endpoint accepts an empty payload:
curl -X POST "https://sentry.boiler.saas-ctrl.com/api/<PROJECT_ID>/integration/otlp/v1/traces" \ -H "Content-Type: application/json" \ -H "X-Sentry-Auth: Sentry sentry_version=7,sentry_key=<PUBLIC_KEY>" \ -d '{"resourceSpans":[]}' # Expect: 202
- Web UI is reachable:
-
Create the three Sentry projects in the Sentry UI:
boilerplate-apiboilerplate-workerboilerplate-web
Copy their DSNs into
infra/sentry/.env(SENTRY_DSN_API,SENTRY_DSN_WORKER,SENTRY_DSN_WEB) and into the Coolify secret vault for the respective app containers. The five Coolify worker apps (worker-general,worker-mail,worker-import,worker-webhook,worker-plugin) intentionally share the server-sideboilerplate-workerproject andSENTRY_DSN_WORKERunless the operator explicitly creates per-group Sentry projects later. Per-project allowed-domains list is filled in for theboilerplate-webproject only (API + Worker DSNs are server-side, no browser exposure). -
Enable 2FA on the admin account immediately (User Settings → Security → Two-Factor → TOTP). The Hardening Checklist below treats admin 2FA as mandatory.
2. Coolify integration
Coolify on this host runs as a target (the controller dashboard is on
a separate host; verify against live operator access records, not the archived
STAGING-ACCESS inventory). After the first manual
install.sh run:
- In Coolify, register the
infra/sentry/upstream/self-hosted/Compose stack as a managed service. Coolify will not re-pullinfra/sentry/on every push tomain(the directory of overlays is read byinstall.sh, not by Coolify directly). - Trigger
install.shagain whenever overlays change. Suggested pattern: add apost-deploymenthook on the Coolify service that runscd /data/coolify/services/sentry/infra/sentry && ./install.shwhenever the parent repo changes.
3. Patch upgrade (25.12.x → 25.12.y)
- Read upstream release notes at https://github.com/getsentry/self-hosted/releases.
- Bump
UPSTREAM_TAGininfra/sentry/install.shAND updateinfra/sentry/UPSTREAM.md. - If upstream renamed any
sentry.conf.pysetting we override, mirror the change ininfra/sentry/sentry/sentry.conf.py. - Commit and push to
main. Coolify auto-deploys; SSH to the host and run./install.sh(it is idempotent and runs only the new migrations). - Smoke-test (Section 1, step 4).
4. Minor upgrade (25.x.* → 25.x+1.*)
Same procedure as Section 3, but:
- Run the backup container manually first (
docker compose run --rm backup /srv/backup/backup.sh) and verify the dump landed in S3. - Read every intermediate release note (Sentry historically slips Snuba-table migrations into minor releases).
- After the installer completes, manually re-run the
cleanupDjango command to defragment ClickHouse:docker compose exec web sentry cleanup --days 90
5. Major upgrade (v25 → v26)
This is a disaster-recovery-grade operation. Schedule a maintenance window.
- Pre-flight backup. Run
docker compose run --rm backup /srv/backup/backup.shANDdocker compose down. Take a host-level tar of/data/coolify/services/sentry/{postgres,clickhouse}-dataand copy it off-box. - Read every changelog between the source and target tag. Pay particular attention to Kafka consumer renames, Snuba table schema changes, and Symbolicator API breaks.
- Bump
UPSTREAM_TAGininstall.sh+UPSTREAM.md. Update our overlays for any renamed config keys. - Run install.sh on a clean clone first (e.g.
/data/coolify/services/sentry-test/) pointed at a restored copy of the production volumes. Verify the migration completes cleanly and the UI loads. - Switch over. Stop the old stack, repoint Traefik to the new stack (Compose label rename), bring up the new stack with the live volumes.
- Rollback path. If the new stack misbehaves,
docker compose down, restore the pre-flight volume tar over/data/coolify/services/sentry/, redeploy the OLDUPSTREAM_TAGviainstall.sh.
6. Event cleanup / disk pressure
B-008 is closed as an operator alerting item: Coolify disk-pressure alerting is enabled, so future disk-full risk is monitored rather than tracked as an active code blocker. The remaining responsibilities in this section are operator-owned Sentry/Postgres retention and backup hygiene; repository changes are not expected unless this runbook or the backup scripts drift.
Sentry's cleanup command is idempotent and safe to run any time:
docker compose exec web sentry cleanup --days 90
Run weekly via crontab (or as a Coolify scheduled task) when retention should drop below 90 days. ClickHouse compacts partitions automatically afterwards but it can take 24h to reclaim disk.
To inspect current disk usage:
docker compose exec clickhouse \
clickhouse-client --query "SELECT database, formatReadableSize(sum(bytes)) AS size \
FROM system.parts WHERE active GROUP BY database ORDER BY sum(bytes) DESC"
7. Backup + restore drill
Backups are continuous and encrypted (see infra/sentry/backup/).
Restore drill quarterly:
- Spin up an empty Sentry stack on a scratch host.
- Pull the latest dump from S3:
aws s3 cp --endpoint-url "$SENTRY_S3_BACKUP_ENDPOINT" \ "s3://$SENTRY_S3_BACKUP_BUCKET/sentry/$(date -u +%Y-%m-%d)/postgres-*.dump.gpg" . - Decrypt:
gpg --decrypt --output postgres.dump postgres-*.dump.gpg - Restore:
pg_restore --clean --if-exists --no-owner --no-privileges -d postgres postgres.dump - Repeat for ClickHouse (
gpg --decrypt | tar -xz, thenclickhouse-client --query "INSERT INTO db.table FORMAT Native"per file). - Boot the stack and verify the last day of events is visible in the UI.
8. Monitoring health
Out-of-the-box checks (no external monitoring required):
| Check | Command |
|---|---|
| Web up | curl -I https://sentry.boiler.saas-ctrl.com |
| Relay healthy | curl https://sentry.boiler.saas-ctrl.com/api/relay/healthcheck/ready/ |
| Postgres reachable | docker compose exec postgres pg_isready |
| ClickHouse reachable | docker compose exec clickhouse clickhouse-client --query "SELECT 1" |
| Kafka topics | docker compose exec kafka kafka-topics --bootstrap-server kafka:9092 --list |
| Workers running | docker compose ps worker cron snuba-consumer |
| Backup heartbeat | docker compose exec backup cat /srv/backup/logs/heartbeat.txt |
| Last backup landed | aws s3 ls --endpoint-url "$SENTRY_S3_BACKUP_ENDPOINT" "s3://$SENTRY_S3_BACKUP_BUCKET/sentry/$(date -u +%Y-%m-%d)/" |
For continuous monitoring, point Coolify's Uptime/Heartbeat plugin (or
Healthchecks.io) at the relay/healthcheck/ready/ URL above.
9. Volume sizing guidance
With the staging host's 32 GB RAM / 75 GB disk:
| Component | Disk (90 d retention, small team) | RAM |
|---|---|---|
| Postgres | 2–5 GB | 1–2 GB |
| ClickHouse | 20–40 GB | 4–8 GB |
| Kafka | 5–10 GB | 1–2 GB |
| Symbolicator cache | 2–5 GB | 0.5–1 GB |
| Redis | <500 MB | <500 MB |
| Sentry worker pool | n/a | 2–4 GB |
If ClickHouse usage approaches 60 GB, lower retention via sentry cleanup --days 30 and reduce --days further if needed.
Hardening Checklist
The checklist below is mandatory before declaring this deployment production-ready. Each item maps to a setting we ship; verifying them is a one-time operator pass plus a quarterly review.
- Public signup disabled.
sentry.conf.pysetsSENTRY_FEATURES['auth:register'] = False. Verify:curl -I https://sentry.boiler.saas-ctrl.com/auth/register/→404. - Admin 2FA enabled. In the UI: User Settings → Security → 2FA (TOTP). Recovery codes printed and stored offline.
- Default admin password rotated. The bootstrap password from
.envis replaced via UI after first login. The.envvalue becomes a recovery secret only; rotate again every 90 days. - Relay rate-limit active.
relay/config.ymlcaps each project at 100 events/sec. Verify by sending a burst and watching for429responses. - Per-project allowed-domains list filled. Project Settings →
Client Keys → Allowed Domains. Set for
boilerplate-webto:https://boiler.saas-ctrl.com,https://www.boiler.saas-ctrl.com,https://admin.boiler.saas-ctrl.com. Leave empty for theboilerplate-apiandboilerplate-workerprojects (server-side DSNs only). - Postgres / ClickHouse / Kafka / Redis NOT public.
docker-compose.override.ymlusesports: !reset []to strip host-port bindings inherited from upstream. Verify:ss -tlnp | grep -E '5432|9000|9092|6379'on the host shows no matches. - Backups encrypted.
backup.shuses GPG (recipient-key ifSENTRY_BACKUP_GPG_RECIPIENTis set, AES256 symmetric withSENTRY_SECRET_KEYotherwise). Verify:aws s3 cpof a recent dump returns a file with header bytes8c 0d(GPG-armored) or85 02(binary GPG). - Symbolicator: no external sym sources.
sentry.conf.pysetsSYMBOLICATOR_SYMSOURCES = []. Verify:docker compose exec symbolicator cat /etc/symbolicator/config.yml | grep sourcesshows no Microsoft / Mozilla / Apple entries. - SENTRY_SECRET_KEY stored offline. Lost key = unreadable backups. Copy from Coolify vault to a hardware-backed password manager (1Password, BitWarden Premium with TOTP, YubiKey-backed pass).
- Traefik dashboard port 8080 firewalled. See
docs/improvement-plan/archive/STAGING-ACCESS.mdopen item; close at Hetzner Cloud Firewall level or front it with basic-auth. .envpermissions ≤ 0600.ls -l infra/sentry/.env. The installer enforces 0600 when it copies the file into the upstream tree.
Re-run this checklist (a) after every patch upgrade, (b) at least once per quarter, (c) before opening Sentry to additional users.
CI Secrets
The CI pipelines in .github/workflows/_sentry-release.yml and
.gitea/workflows/_sentry-release.yml need three Sentry secrets to tag
releases, upload source-maps, correlate commits, and mark deploys. The
main CI build uploads apps/web/build/client as the
web-client-sourcemaps workflow artifact; the reusable web release job
downloads that artifact before calling scripts/ci/sentry-release.sh.
The release job must fail if the source-map directory is absent, so a
fresh checkout cannot silently publish a web release without artifacts.
The web Vite build can also upload source-maps directly when
SENTRY_AUTH_TOKEN is intentionally present in that build environment.
The default CI path keeps the token scoped to the reusable release job
and passes source-maps through the workflow artifact instead.
1. Mint an Auth-Token in the Sentry UI
- Sign in at https://sentry.boiler.saas-ctrl.com as the admin user.
- Open Settings → Account → API → Auth Tokens (this is a user-scoped token; Sentry deliberately does not expose org-scoped integration tokens in single-organisation mode).
- Click Create New Token, give it a descriptive name (e.g.
ci-release-token-2026-05), and select the single scopeproject:releases. Do not selectorg:admin,org:write, ormember:admin. - Copy the token immediately — Sentry only shows it once.
2. Store secrets in GitHub Actions
Navigate to Settings → Secrets and variables → Actions in the
GitHub mirror (github.com/<org>/boilerplate) and add three repository
secrets:
| Name | Value |
|---|---|
SENTRY_AUTH_TOKEN |
The token minted in step 1 |
SENTRY_ORG |
sentry (the auto-generated slug from single-org install) |
SENTRY_URL |
https://sentry.boiler.saas-ctrl.com |
Once configured, _sentry-release.yml is invoked once per project
(api/worker/web) at the end of every CI run via workflow_call.
Only the web invocation passes sourcemaps-artifact: web-client-sourcemaps; api and worker releases pass sourcemaps-path: none.
3. Store secrets in Gitea Actions
In Gitea (git.tool-ctrl.de/toolctrl-labs/boilerplate) open
Repository Settings → Actions → Secrets and create the same three
secrets (SENTRY_AUTH_TOKEN, SENTRY_ORG, SENTRY_URL) with
identical values. Gitea's secret scoping rules mirror GitHub's, so the
reusable workflow at .gitea/workflows/_sentry-release.yml (and the
composite-action fallback at .gitea/actions/sentry-release/action.yml
for runner versions < 1.21) will pick them up automatically.
The deploy workflow (.gitea/workflows/deploy.yml) reads the same
secrets in its Mark Sentry deploy step; if the secrets are absent
the step skips silently so existing deploy paths continue to function
during the rollout window.
4. Rotation
- Rotate every 6 months. Add a recurring calendar entry for the operator on-call.
- Rotation procedure:
- Mint a new token (step 1) with the same
project:releasesscope. - Update
SENTRY_AUTH_TOKENin both GitHub and Gitea secret stores within the same maintenance window. - Wait for one CI run on each system to confirm the new token works (look for a fresh release in the Sentry UI).
- Revoke the old token via the Sentry UI (Settings → Account → API → Auth Tokens → trash icon).
- Mint a new token (step 1) with the same
- Next scheduled rotation: 2026-11-17. Update this date after each rotation.
5. Local development
Releases are a CI-only concern. Local developers do not need
SENTRY_AUTH_TOKEN; the web Vite build only uploads source-maps when
the token is present, and the api/worker bootstrap modules never call
the Releases API.
If you want to test the helper locally, mint a personal token with
project:releases scope and run:
SENTRY_AUTH_TOKEN=<token> \
SENTRY_ORG=sentry \
SENTRY_URL=https://sentry.boiler.saas-ctrl.com \
bash scripts/ci/sentry-release.sh \
--project boilerplate-api \
--environment preview \
--release "$(git rev-parse HEAD)" \
--sourcemaps none
Delete the test release afterwards with
sentry-cli releases delete <release>.