# GlitchTip Self-Hosted Deployment Self-hosted Sentry-compatible error tracking. Deployed at `/opt/glitchtip/` on rayserver, proxied via nginx at `https://shopproquote.graj-media.com/glitchtip/`. ## Docker Compose ```yaml # /opt/glitchtip/docker-compose.yml services: db: image: postgres:15 restart: unless-stopped environment: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} volumes: - pgdata:/var/lib/postgresql/data redis: image: redis:7 restart: unless-stopped web: image: glitchtip/glitchtip:latest restart: unless-stopped depends_on: [db, redis] environment: DATABASE_URL: "postgresql://postgres:${POSTGRES_PASSWORD}@db:5432/postgres" REDIS_URL: "redis://redis:6379/1" SECRET_KEY: ${SECRET_KEY} EMAIL_URL: "consolemail://" PORT: 8000 DEFAULT_FROM_EMAIL: "glitchtip@localhost" GLITCHTIP_EMBED_WORKER: "true" # CRITICAL — see pitfalls ports: - "127.0.0.1:8001:8000" # 8001 host — 8000 is taken by Portainer volumes: - uploads:/app/uploads volumes: pgdata: uploads: ``` `.env` (mode 0600): ``` POSTGRES_PASSWORD= SECRET_KEY= ``` ## nginx Location (added to existing site block) ```nginx location /glitchtip/ { # Sentry SDK envelope URL: /glitchtip/1/api/1/envelope/ # This rewrite strips the project ID so GlitchTip gets /api/1/envelope/ rewrite ^/glitchtip/[0-9]+/(api/.*)$ /$1 break; rewrite ^/glitchtip/[0-9]+/(store/.*)$ /api/1/$1 break; rewrite ^/glitchtip/(.*)$ /$1 break; proxy_pass http://127.0.0.1:8001; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; } ``` **Critical ordering:** The `api/` and `store/` rewrites MUST come BEFORE the general `^/glitchtip/(.*)$` rewrite. Nginx tries them first — if a request matches `/glitchtip/1/api/1/envelope/`, it hits the `api/` rewrite and becomes `/api/1/envelope/`. The general rewrite would incorrectly strip the path to `/1/api/1/envelope/`. ## First-Run Setup ### 1. Migrate With `GLITCHTIP_EMBED_WORKER=true` (as specified in the compose file above), migrations auto-run on startup — the all-in-one start.sh calls `python manage.py migrate --no-input --skip-checks` automatically. No manual step needed. If deploying without embedded worker, run manually: ```bash docker exec glitchtip-web-1 python manage.py migrate ``` ### 2. Suppress ALLOWED_HOSTS warning (optional but recommended) Add to docker-compose environment: ```yaml ALLOWED_HOSTS: "shopproquote.graj-media.com,127.0.0.1" ``` Without this, GlitchTip logs `RuntimeWarning: ALLOWED_HOSTS is the wildcard default. Restrict to known hostnames` on every startup. The warning is harmless in development but should be set for production deployments. ### 3. Create superuser via Django shell ```bash docker exec glitchtip-web-1 python manage.py shell -c " from django.apps import apps U = apps.get_model('users', 'User') u = U.objects.filter(email='admin@localhost').first() u or U.objects.create_superuser(email='admin@localhost', password='') " ``` **Pitfall:** Direct module imports like `from organizations.models import Organization` fail with `RuntimeError: Model class doesn't declare an explicit app_label`. Always use `apps.get_model('app_label', 'ModelName')` in GlitchTip's shell — the app registry isn't fully loaded for direct imports. ### 3. Create organization + project via Django shell ```bash docker exec glitchtip-web-1 python manage.py shell -c " from django.apps import apps Org = apps.get_model('organizations_ext', 'Organization') OrgUser = apps.get_model('organizations_ext', 'OrganizationUser') OrgOwner = apps.get_model('organizations_ext', 'OrganizationOwner') Project = apps.get_model('projects', 'Project') ProjectKey = apps.get_model('projects', 'ProjectKey') User = apps.get_model('users', 'User') u = User.objects.get(email='admin@localhost') org, created = Org.objects.get_or_create(name='SPQ', slug='spq') if created: ou = OrgUser.objects.create(organization=org, user=u, role=4) # 4 = OWNER OrgOwner.objects.create(organization=org, organization_user=ou) proj, _ = Project.objects.get_or_create(name='spq-frontend', slug='spq-frontend', organization=org, defaults={'platform': 'javascript-react'}) pk = ProjectKey.objects.filter(project=proj).first() or ProjectKey.objects.create(project=proj) print(f'DSN: http://{pk.public_key}@127.0.0.1:8001/{proj.id}') " ``` **Pitfall:** `OrgUser.role` is NOT NULL — you must pass `role=4` (OWNER). Omitting it raises `IntegrityError: null value in column "role"`. **Pitfall:** `ProjectKey` has no `dsn_secret` attribute. The DSN is constructed from `pk.public_key` (a UUID) + the project's numeric ID: `http://@/`. ### 4. Verify event ingestion ```bash # Send a test event via the Sentry store endpoint curl -s -X POST http://127.0.0.1:8001/api/1/store/ \ -H "X-Sentry-Auth: Sentry sentry_key=,sentry_version=7" \ -H "Content-Type: application/json" \ -d '{"event_id":"","timestamp":"2026-07-07T22:51:00Z","level":"error","message":"test","platform":"javascript"}' # → {"event_id":"...","task_id":null} HTTP 200 ``` Then check the issues table: ```bash docker exec glitchtip-web-1 python manage.py shell -c " from django.apps import apps I = apps.get_model('issue_events', 'Issue') print(f'Issues: {I.objects.count()}') " ``` If the count stays 0 after the API returned 200, the worker isn't running — see the `GLITCHTIP_EMBED_WORKER` pitfall below. ## Frontend Integration (@sentry/react) ```ts // src/main.tsx — BEFORE createRoot, but dynamic-imported to keep initial bundle small import { setErrorReporter } from './lib/userMessages'; const DSN = import.meta.env.VITE_GLITCHTIP_DSN as string | undefined; if (DSN) { import('@sentry/react').then((Sentry) => { Sentry.init({ dsn: DSN, environment: import.meta.env.VITE_ENV || 'development', tracesSampleRate: 0.1, }); setErrorReporter((err, ctx) => { Sentry.captureException(err, { extra: ctx }); }); }).catch(() => { /* non-critical */ }); } ``` The `setErrorReporter` hook (already in `src/lib/userMessages.ts`) routes `reportError()` calls into Sentry. Dynamic import keeps @sentry/react out of the initial bundle — it only loads when the DSN env var is set (production) AND the first error triggers the import. Zero impact on dev or initial load. `.env.production`: ``` VITE_GLITCHTIP_DSN=https://@shopproquote.graj-media.com/glitchtip/ ``` The DSN uses the public HTTPS URL (via nginx proxy), not the internal `127.0.0.1:8001`, because the browser sends events directly. ## Pitfalls (GlitchTip v6.2.0) - **GLITCHTIP_EMBED_WORKER=true is REQUIRED.** Without it, GlitchTip runs as web-only (SERVER_ROLE=web). The API accepts events (HTTP 200, returns event_id) but nothing processes them into issues — the Issue table stays empty. Set this env var in docker-compose.yml and recreate the container with --force-recreate. - **nginx rewrite ORDERING matters.** The Sentry SDK sends events to /glitchtip//api/1/envelope/. The api/ rewrite MUST come BEFORE the general ^/glitchtip/(.*)$ rewrite. If the general rewrite hits first, the path becomes /1/api/1/envelope/ which GlitchTip doesn't serve (returns 400). - **ALLOWED_HOSTS should be set** to suppress the startup warning. Without it, every container restart logs "RuntimeWarning: ALLOWED_HOSTS is the wildcard default". Set to shopproquote.graj-media.com,127.0.0.1 for the SPQ deployment. - **Port 8000 is taken by Portainer** on rayserver. Use 127.0.0.1:8001:8000. Always ss -tlnp | grep :8000 before deploying. - **With GLITCHTIP_EMBED_WORKER=true, migrations auto-run** on startup. The all-in-one start.sh calls python manage.py migrate --no-input --skip-checks automatically. No manual migrate step needed when using the compose file above. - **Django shell imports need apps.get_model().** Direct from organizations.models import Organization raises RuntimeError. Use apps.get_model('organizations_ext', 'Organization') (note the _ext suffix on the app label). - **OrgUser.role is NOT NULL.** Pass role=4 (OWNER) when creating. Without it: IntegrityError: null value in column "role". - **ProjectKey.dsn_secret does not exist.** The DSN is http://@/. Check pk._meta.get_fields() to see available attributes — public_key is the Sentry key, project.id is the numeric project ID. - **Event ID must be a valid UUID (hex, no dashes) for the /api/1/store/ endpoint.** Non-UUID strings return 422 uuid_parsing error. Use python3 -c "import uuid; print(uuid.uuid4().hex)". - **Sentry envelope format** (/api/1/envelope/): three newline-delimited JSON objects — header {"event_id":"...","sent_at":"..."}, item header {"type":"event"}, and the event payload. Content-Type: text/plain;charset=UTF-8. The store endpoint (/api/1/store/) is simpler for single events.