Files
hermes-config/skills/self-hosting/docker-service-deployment/references/glitchtip-deployment.md
T
2026-07-12 10:17:17 -04:00

203 lines
9.0 KiB
Markdown

# 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=<openssl rand -hex 24>
SECRET_KEY=<openssl rand -hex 24>
```
## 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='<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://<public_key>@<host>/<project_id>`.
### 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=<public_key>,sentry_version=7" \
-H "Content-Type: application/json" \
-d '{"event_id":"<uuid-without-dashes>","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://<public_key>@shopproquote.graj-media.com/glitchtip/<project_id>
```
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/<project_id>/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://<public_key>@<host>/<project_id>. 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.