disclosure-bureau/infra/disclosure-stack/docker-compose.yml

516 lines
21 KiB
YAML
Raw Normal View History

# Disclosure Bureau — full deployment stack.
# Routed via the host's existing plegal-traefik (network: traefik-public).
# Internal services share the disclosure-internal network and are NOT exposed
# to the host. Public services (web, kong, studio, search) get Traefik labels.
name: disclosure
networks:
internal:
name: disclosure-internal
driver: bridge
traefik:
name: traefik-public
external: true
volumes:
db-data:
storage-data:
meili-data:
hf-cache:
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI W0 — security hardening (5 fixes verified live on disclosure.top) - middleware: gate /api/admin/* same as /admin/* (F1) - imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2) - studio: real basic-auth label (bcrypt hash, middleware reference) (F3) - relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4) - migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2) W1 — observability + resilience + autocomplete - studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck - compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env - claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS) - openrouter.ts: retry with exponential backoff + Retry-After + in-memory circuit breaker (promotes FALLBACK after CB_THRESHOLD failures) - lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper - middleware: mints correlation_id, stamps x-correlation-id response header, emits structured http_request log per /api/* call - messages/route.ts: switch to structured logger - 60_meili_index.py: push documents + chunks into Meilisearch - /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50 - search-autocomplete.tsx: debounced dropdown wired into search-panel W1.2 — Glitchtip + Forgejo self-hosted - compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2) - compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988 - @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts) - /api/admin/throw smoke endpoint (gated by W0-F1 middleware) - Synthetic event ingestion verified at glitchtip.disclosure.top - forgejo.disclosure.top up, repo discadmin/disclosure-bureau created, runner registered (labels: ubuntu-latest, docker) - .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python syntax + compose validation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
glitchtip-redis-data:
glitchtip-uploads:
forgejo-data:
forgejo-runner-config:
services:
# ─── Database ─────────────────────────────────────────────────────────────
db:
container_name: disclosure-db
image: supabase/postgres:15.8.1.060
restart: unless-stopped
networks: [internal]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -h localhost"]
interval: 10s
timeout: 5s
retries: 12
environment:
POSTGRES_HOST: /var/run/postgresql
POSTGRES_PORT: 5432
POSTGRES_DB: postgres
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
PGPASSWORD: ${POSTGRES_PASSWORD}
JWT_SECRET: ${JWT_SECRET}
JWT_EXP: 3600
POSTGRES_INITDB_ARGS: "--data-checksums"
command:
- postgres
- -c
- shared_buffers=${POSTGRES_SHARED_BUFFERS:-384MB}
- -c
- work_mem=${POSTGRES_WORK_MEM:-12MB}
- -c
- maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-96MB}
- -c
- max_connections=${POSTGRES_MAX_CONNECTIONS:-80}
volumes:
- db-data:/var/lib/postgresql/data
# ─── Auth (GoTrue) ────────────────────────────────────────────────────────
auth:
container_name: disclosure-auth
image: supabase/gotrue:v2.170.0
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
environment:
GOTRUE_API_HOST: 0.0.0.0
GOTRUE_API_PORT: 9999
API_EXTERNAL_URL: https://${DOMAIN_API}
GOTRUE_DB_DRIVER: postgres
GOTRUE_DB_DATABASE_URL: postgres://supabase_auth_admin:${POSTGRES_PASSWORD}@db:5432/postgres?search_path=auth
GOTRUE_SITE_URL: https://${DOMAIN_MAIN}
ship: synthesize 158 entities, AG-UI artifacts, chat persistence, auth flow Fase 3 onda 2 — entity synthesis at scale: - scripts/synthesize/20_entity_summary.py: queries DB for entities with total_mentions ≥ threshold + top-K verbatim chunk snippets via entity_mentions JOIN, prompts Sonnet (Holmes-Watson voice, bilingual), writes narrative_summary EN+PT-BR + summary_status=synthesized. Ran on 187 candidates (mentions ≥ 20) → 158 OK · 1 err · 29 skipped (no snippets). Combined with anchor curation: 20 curated + 158 synthesized = 178 entities with real narrative (vs 0 a day ago). Fase 4 — chat with typed artifacts + persistence: - lib/chat/agui.ts: AG-UI v1 typed Artifact union (citation, crop_image, entity_card, evidence_card, hypothesis_card, case_card, navigation_offer) alongside the existing event types. - lib/chat/tools.ts + openrouter.ts: hybrid_search emits up to 6 citation + crop_image artifacts per query. Provider collects them and returns in done.artifacts so the route can persist. - api/sessions/[id]/messages: persist artifacts to messages.citations. - components/chat-bubble.tsx: ArtifactCard renders inline cards (citation, crop_image, entity_card, navigation_offer) for streamed and persisted messages. activeId now persisted in localStorage so navigation between pages keeps the same conversation. New sessions are lazy (only when user has zero). loadMessages hydrates tools + artifacts from server. CRUD UI: rename (✎) + archive (🗑) buttons per session in the list. Home search: - doc-list-filters: input now fires hybrid_search (rerank=0 for speed) in parallel with the local title filter; chunk hits render above the doc grid with snippet + score + classification. - api/search/hybrid: accept ?rerank=0 to skip the cross-encoder (1.3s vs 60s). Auth flow: - infra: SMTP_HOST=mail.spacemail.com:587 + DMARC published; mail now lands in inbox. GOTRUE_MAILER_AUTOCONFIRM=false (real email verification). - kong.yml: proxy /auth/callback on api.disclosure.top → web:3000 so PKCE email links don't 404 at the gateway. - web/app/auth/callback: handle both ?code= (OAuth) and ?token=&type= (PKCE); redirect to the public site host before verifyOtp so the session cookie lands on the right domain. Audit deliverables: - .nirvana/outputs/disclosure-bureau/.../systems-atelier/: 5 docs (code analysis, tech debt, discovery brief, system arch, 5 ADRs) authored by sa-principal that produced this roadmap. Kept in-tree for traceability.
2026-05-18 06:52:59 +00:00
# Explicit external URL so confirmation links land on the public site
# (Next.js /auth/callback), not on the Kong gateway host.
GOTRUE_API_EXTERNAL_URL: https://${DOMAIN_MAIN}
GOTRUE_URI_ALLOW_LIST: https://${DOMAIN_MAIN},https://www.${DOMAIN_MAIN}
GOTRUE_DISABLE_SIGNUP: "false"
GOTRUE_JWT_ADMIN_ROLES: service_role
GOTRUE_JWT_AUD: authenticated
GOTRUE_JWT_DEFAULT_GROUP_NAME: authenticated
GOTRUE_JWT_EXP: 3600
GOTRUE_JWT_SECRET: ${JWT_SECRET}
GOTRUE_EXTERNAL_EMAIL_ENABLED: "true"
# SMTP configured (Spacemail) → email confirmation required for signups.
GOTRUE_MAILER_AUTOCONFIRM: "false"
GOTRUE_MAILER_OTP_EXP: 3600
GOTRUE_SMTP_HOST: ${SMTP_HOST}
GOTRUE_SMTP_PORT: ${SMTP_PORT}
GOTRUE_SMTP_USER: ${SMTP_USER}
GOTRUE_SMTP_PASS: ${SMTP_PASS}
GOTRUE_SMTP_ADMIN_EMAIL: ${SMTP_FROM}
GOTRUE_SMTP_SENDER_NAME: ${SMTP_FROM_NAME}
GOTRUE_MAILER_URLPATHS_INVITE: /auth/callback
GOTRUE_MAILER_URLPATHS_CONFIRMATION: /auth/callback
GOTRUE_MAILER_URLPATHS_RECOVERY: /auth/callback
GOTRUE_MAILER_URLPATHS_EMAIL_CHANGE: /auth/callback
# ─── PostgREST ────────────────────────────────────────────────────────────
rest:
container_name: disclosure-rest
image: postgrest/postgrest:v12.2.8
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
environment:
PGRST_DB_URI: postgres://authenticator:${POSTGRES_PASSWORD}@db:5432/postgres
PGRST_DB_SCHEMAS: public,storage
PGRST_DB_ANON_ROLE: anon
PGRST_JWT_SECRET: ${JWT_SECRET}
PGRST_DB_USE_LEGACY_GUCS: "false"
PGRST_APP_SETTINGS_JWT_SECRET: ${JWT_SECRET}
PGRST_APP_SETTINGS_JWT_EXP: 3600
# ─── Realtime ─────────────────────────────────────────────────────────────
realtime:
container_name: disclosure-realtime
image: supabase/realtime:v2.30.34
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
environment:
PORT: 4000
DB_HOST: db
DB_PORT: 5432
DB_USER: supabase_admin
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_NAME: postgres
DB_ENC_KEY: ${VAULT_ENC_KEY}
API_JWT_SECRET: ${JWT_SECRET}
SECRET_KEY_BASE: ${SECRET_KEY_BASE}
ERL_AFLAGS: -proto_dist inet_tcp
DNS_NODES: "''"
RLIMIT_NOFILE: "10000"
APP_NAME: realtime
SEED_SELF_HOST: "true"
RUN_JANITOR: "true"
# ─── Storage ──────────────────────────────────────────────────────────────
storage:
container_name: disclosure-storage
image: supabase/storage-api:v1.14.3
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
rest: { condition: service_started }
imgproxy: { condition: service_started }
environment:
ANON_KEY: ${ANON_KEY}
SERVICE_KEY: ${SERVICE_ROLE_KEY}
POSTGREST_URL: http://rest:3000
PGRST_JWT_SECRET: ${JWT_SECRET}
DATABASE_URL: postgres://supabase_storage_admin:${POSTGRES_PASSWORD}@db:5432/postgres
FILE_SIZE_LIMIT: 52428800
STORAGE_BACKEND: file
FILE_STORAGE_BACKEND_PATH: /var/lib/storage
TENANT_ID: stub
REGION: stub
GLOBAL_S3_BUCKET: stub
ENABLE_IMAGE_TRANSFORMATION: "true"
IMGPROXY_URL: http://imgproxy:5001
volumes:
- storage-data:/var/lib/storage
imgproxy:
container_name: disclosure-imgproxy
image: darthsim/imgproxy:v3.8.0
restart: unless-stopped
networks: [internal]
environment:
IMGPROXY_BIND: ":5001"
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI W0 — security hardening (5 fixes verified live on disclosure.top) - middleware: gate /api/admin/* same as /admin/* (F1) - imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2) - studio: real basic-auth label (bcrypt hash, middleware reference) (F3) - relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4) - migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2) W1 — observability + resilience + autocomplete - studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck - compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env - claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS) - openrouter.ts: retry with exponential backoff + Retry-After + in-memory circuit breaker (promotes FALLBACK after CB_THRESHOLD failures) - lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper - middleware: mints correlation_id, stamps x-correlation-id response header, emits structured http_request log per /api/* call - messages/route.ts: switch to structured logger - 60_meili_index.py: push documents + chunks into Meilisearch - /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50 - search-autocomplete.tsx: debounced dropdown wired into search-panel W1.2 — Glitchtip + Forgejo self-hosted - compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2) - compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988 - @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts) - /api/admin/throw smoke endpoint (gated by W0-F1 middleware) - Synthetic event ingestion verified at glitchtip.disclosure.top - forgejo.disclosure.top up, repo discadmin/disclosure-bureau created, runner registered (labels: ubuntu-latest, docker) - .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python syntax + compose validation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
# W0-F2: tighten filesystem root from "/" (whole VPS) to the Storage
# backend mount only. Imgproxy never reads outside Storage objects.
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /var/lib/storage
IMGPROXY_USE_ETAG: "true"
IMGPROXY_ENABLE_WEBP_DETECTION: "true"
volumes:
- storage-data:/var/lib/storage
# ─── pg-meta + Studio ─────────────────────────────────────────────────────
meta:
container_name: disclosure-meta
image: supabase/postgres-meta:v0.83.2
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
environment:
PG_META_PORT: 8080
PG_META_DB_HOST: db
PG_META_DB_PORT: 5432
PG_META_DB_NAME: postgres
PG_META_DB_USER: supabase_admin
PG_META_DB_PASSWORD: ${POSTGRES_PASSWORD}
studio:
container_name: disclosure-studio
image: supabase/studio:20241202-71e5240
restart: unless-stopped
networks: [internal, traefik]
depends_on:
meta: { condition: service_started }
environment:
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI W0 — security hardening (5 fixes verified live on disclosure.top) - middleware: gate /api/admin/* same as /admin/* (F1) - imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2) - studio: real basic-auth label (bcrypt hash, middleware reference) (F3) - relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4) - migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2) W1 — observability + resilience + autocomplete - studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck - compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env - claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS) - openrouter.ts: retry with exponential backoff + Retry-After + in-memory circuit breaker (promotes FALLBACK after CB_THRESHOLD failures) - lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper - middleware: mints correlation_id, stamps x-correlation-id response header, emits structured http_request log per /api/* call - messages/route.ts: switch to structured logger - 60_meili_index.py: push documents + chunks into Meilisearch - /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50 - search-autocomplete.tsx: debounced dropdown wired into search-panel W1.2 — Glitchtip + Forgejo self-hosted - compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2) - compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988 - @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts) - /api/admin/throw smoke endpoint (gated by W0-F1 middleware) - Synthetic event ingestion verified at glitchtip.disclosure.top - forgejo.disclosure.top up, repo discadmin/disclosure-bureau created, runner registered (labels: ubuntu-latest, docker) - .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python syntax + compose validation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
# W1: Next.js standalone server binds to the container hostname by
# default, leaving 127.0.0.1 unreachable — the Docker healthcheck
# (fetch 127.0.0.1:3000/api/profile) then loops on ECONNREFUSED and
# the service never goes healthy. HOSTNAME=0.0.0.0 forces it to bind
# on all interfaces so both the loopback and the docker IP respond.
HOSTNAME: 0.0.0.0
STUDIO_PG_META_URL: http://meta:8080
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
DEFAULT_ORGANIZATION_NAME: "Disclosure Bureau"
DEFAULT_PROJECT_NAME: "disclosure"
SUPABASE_URL: http://kong:8000
SUPABASE_PUBLIC_URL: https://${DOMAIN_API}
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
AUTH_JWT_SECRET: ${JWT_SECRET}
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-studio.rule=Host(`${DOMAIN_STUDIO}`)
- traefik.http.routers.disclosure-studio.entrypoints=websecure
- traefik.http.routers.disclosure-studio.tls=true
- traefik.http.routers.disclosure-studio.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-studio.loadbalancer.server.port=3000
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI W0 — security hardening (5 fixes verified live on disclosure.top) - middleware: gate /api/admin/* same as /admin/* (F1) - imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2) - studio: real basic-auth label (bcrypt hash, middleware reference) (F3) - relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4) - migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2) W1 — observability + resilience + autocomplete - studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck - compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env - claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS) - openrouter.ts: retry with exponential backoff + Retry-After + in-memory circuit breaker (promotes FALLBACK after CB_THRESHOLD failures) - lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper - middleware: mints correlation_id, stamps x-correlation-id response header, emits structured http_request log per /api/* call - messages/route.ts: switch to structured logger - 60_meili_index.py: push documents + chunks into Meilisearch - /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50 - search-autocomplete.tsx: debounced dropdown wired into search-panel W1.2 — Glitchtip + Forgejo self-hosted - compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2) - compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988 - @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts) - /api/admin/throw smoke endpoint (gated by W0-F1 middleware) - Synthetic event ingestion verified at glitchtip.disclosure.top - forgejo.disclosure.top up, repo discadmin/disclosure-bureau created, runner registered (labels: ubuntu-latest, docker) - .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python syntax + compose validation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
# W0-F3: real basic auth (was effectively disabled with usersfile=/dev/null).
# The user/password is DASHBOARD_USERNAME / DASHBOARD_PASSWORD from .env;
# the bcrypt hash below was generated with $$ doubled for compose escaping.
# Rotate by regenerating: htpasswd -nbB <user> <pass> (then double every $).
- traefik.http.middlewares.disclosure-studio-auth.basicauth.users=admin:$$2b$$05$$tFLAMGNWX7xDbVyQ/O0G1.ruLwm3Le1.ErgdUTB9IYeJeH2FHd4ha
- traefik.http.routers.disclosure-studio.middlewares=disclosure-studio-auth@docker
# ─── Kong API gateway ─────────────────────────────────────────────────────
kong:
container_name: disclosure-kong
image: kong:2.8.1
restart: unless-stopped
networks: [internal, traefik]
depends_on:
auth: { condition: service_started }
rest: { condition: service_started }
realtime: { condition: service_started }
storage: { condition: service_started }
environment:
KONG_DATABASE: "off"
# Read rendered config (envsubst happens at startup, see entrypoint below)
KONG_DECLARATIVE_CONFIG: /tmp/kong.yml
KONG_DNS_ORDER: LAST,A,CNAME
KONG_PLUGINS: request-transformer,cors,key-auth,acl,basic-auth
KONG_NGINX_PROXY_PROXY_BUFFER_SIZE: 160k
KONG_NGINX_PROXY_PROXY_BUFFERS: 64 160k
SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_KEY: ${SERVICE_ROLE_KEY}
DASHBOARD_USERNAME: ${DASHBOARD_USERNAME}
DASHBOARD_PASSWORD: ${DASHBOARD_PASSWORD}
# Kong declarative config does NOT do env substitution by itself. We
# render the template into /tmp/kong.yml at container start so the JWT
# keys land literally in the config Kong actually loads.
user: root
entrypoint:
- /bin/sh
- -c
- |
apk add --no-cache gettext >/dev/null 2>&1 || true
envsubst < /usr/local/kong/kong.yml.tmpl > /tmp/kong.yml
chown kong:kong /tmp/kong.yml
exec /docker-entrypoint.sh kong docker-start
volumes:
- ./kong.yml:/usr/local/kong/kong.yml.tmpl:ro
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-api.rule=Host(`${DOMAIN_API}`)
- traefik.http.routers.disclosure-api.entrypoints=websecure
- traefik.http.routers.disclosure-api.tls=true
- traefik.http.routers.disclosure-api.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-api.loadbalancer.server.port=8000
# ─── Meilisearch ──────────────────────────────────────────────────────────
meilisearch:
container_name: disclosure-meili
image: getmeili/meilisearch:v1.10
restart: unless-stopped
networks: [internal, traefik]
environment:
MEILI_MASTER_KEY: ${MEILI_MASTER_KEY}
MEILI_NO_ANALYTICS: "true"
MEILI_ENV: production
MEILI_MAX_INDEXING_MEMORY: ${MEILI_MAX_INDEXING_MEMORY:-512MB}
volumes:
- meili-data:/meili_data
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-search.rule=Host(`${DOMAIN_SEARCH}`)
- traefik.http.routers.disclosure-search.entrypoints=websecure
- traefik.http.routers.disclosure-search.tls=true
- traefik.http.routers.disclosure-search.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-search.loadbalancer.server.port=7700
# ─── Next.js web (Disclosure Bureau frontend) ─────────────────────────────
web:
container_name: disclosure-web
build:
context: /data/disclosure/web # rsynced from laptop, see scripts/sync-data.sh
dockerfile: Dockerfile
args:
NEXT_PUBLIC_SUPABASE_URL: https://${DOMAIN_API}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
NEXT_PUBLIC_SITE_URL: https://${DOMAIN_MAIN}
restart: unless-stopped
networks: [internal, traefik]
depends_on:
kong: { condition: service_started }
environment:
NODE_ENV: production
NODE_OPTIONS: ${NEXT_NODE_OPTIONS:---max-old-space-size=768}
NEXT_PUBLIC_SUPABASE_URL: https://${DOMAIN_API}
NEXT_PUBLIC_SUPABASE_ANON_KEY: ${ANON_KEY}
SUPABASE_SERVICE_ROLE_KEY: ${SERVICE_ROLE_KEY}
NEXT_PUBLIC_SITE_URL: https://${DOMAIN_MAIN}
UFO_ROOT: /data/ufo
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI W0 — security hardening (5 fixes verified live on disclosure.top) - middleware: gate /api/admin/* same as /admin/* (F1) - imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2) - studio: real basic-auth label (bcrypt hash, middleware reference) (F3) - relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4) - migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2) W1 — observability + resilience + autocomplete - studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck - compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env - claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS) - openrouter.ts: retry with exponential backoff + Retry-After + in-memory circuit breaker (promotes FALLBACK after CB_THRESHOLD failures) - lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper - middleware: mints correlation_id, stamps x-correlation-id response header, emits structured http_request log per /api/* call - messages/route.ts: switch to structured logger - 60_meili_index.py: push documents + chunks into Meilisearch - /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50 - search-autocomplete.tsx: debounced dropdown wired into search-panel W1.2 — Glitchtip + Forgejo self-hosted - compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2) - compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988 - @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts) - /api/admin/throw smoke endpoint (gated by W0-F1 middleware) - Synthetic event ingestion verified at glitchtip.disclosure.top - forgejo.disclosure.top up, repo discadmin/disclosure-bureau created, runner registered (labels: ubuntu-latest, docker) - .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python syntax + compose validation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
# W1-TD#10: bump pg pool from default 5 to 20 (chat agent + hybrid_search
# can saturate the smaller pool under concurrent load).
PG_POOL_MAX: ${PG_POOL_MAX:-20}
# Chat agent (W1-F8: CLAUDE_CODE_OAUTH_TOKEN only injected when the
# provider actually uses it — default provider is openrouter, so the token
# stays absent from this container's env unless CHAT_PROVIDER=claude-code).
CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN_FOR_WEB:-}
CLAUDE_CODE_MODEL: ${CLAUDE_CODE_MODEL}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
OPENROUTER_MODEL: ${OPENROUTER_MODEL}
OPENROUTER_FALLBACK_MODEL: ${OPENROUTER_FALLBACK_MODEL}
CHAT_PROVIDER: ${CHAT_PROVIDER}
# Meilisearch (used by /api/search)
MEILISEARCH_URL: http://meilisearch:7700
MEILISEARCH_API_KEY: ${MEILI_MASTER_KEY}
# Embed service (used by /lib/retrieval)
EMBED_SERVICE_URL: http://embed:8000
# pgvector + chunks (hybrid_search)
DATABASE_URL: postgres://postgres:${POSTGRES_PASSWORD}@db:5432/postgres
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI W0 — security hardening (5 fixes verified live on disclosure.top) - middleware: gate /api/admin/* same as /admin/* (F1) - imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2) - studio: real basic-auth label (bcrypt hash, middleware reference) (F3) - relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4) - migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2) W1 — observability + resilience + autocomplete - studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck - compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env - claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS) - openrouter.ts: retry with exponential backoff + Retry-After + in-memory circuit breaker (promotes FALLBACK after CB_THRESHOLD failures) - lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper - middleware: mints correlation_id, stamps x-correlation-id response header, emits structured http_request log per /api/* call - messages/route.ts: switch to structured logger - 60_meili_index.py: push documents + chunks into Meilisearch - /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50 - search-autocomplete.tsx: debounced dropdown wired into search-panel W1.2 — Glitchtip + Forgejo self-hosted - compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2) - compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988 - @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts) - /api/admin/throw smoke endpoint (gated by W0-F1 middleware) - Synthetic event ingestion verified at glitchtip.disclosure.top - forgejo.disclosure.top up, repo discadmin/disclosure-bureau created, runner registered (labels: ubuntu-latest, docker) - .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python syntax + compose validation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
# W1.2 — Glitchtip error monitoring (DSN issued by manage.py bootstrap)
SENTRY_DSN: ${GLITCHTIP_WEB_DSN}
NEXT_PUBLIC_SENTRY_DSN: ${GLITCHTIP_WEB_DSN}
volumes:
- ${DATA_WIKI}:/data/ufo/wiki:ro
- ${DATA_PROCESSING}:/data/ufo/processing:ro
- ${DATA_RAW}:/data/ufo/raw:ro
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-web.rule=Host(`app.${DOMAIN_MAIN}`) || Host(`${DOMAIN_MAIN}`) || Host(`www.${DOMAIN_MAIN}`)
- traefik.http.routers.disclosure-web.entrypoints=websecure
- traefik.http.routers.disclosure-web.tls=true
- traefik.http.routers.disclosure-web.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-web.loadbalancer.server.port=3000
# www → apex redirect
- traefik.http.middlewares.disclosure-www-redir.redirectregex.regex=^https?://www\.${DOMAIN_MAIN}/(.*)
- traefik.http.middlewares.disclosure-www-redir.redirectregex.replacement=https://${DOMAIN_MAIN}/$${1}
- traefik.http.middlewares.disclosure-www-redir.redirectregex.permanent=true
# ─── BGE-M3 embedding + reranker service (CPU only) ───────────────────────
embed:
container_name: disclosure-embed
build:
context: ../embed-service
restart: unless-stopped
networks: [internal]
environment:
DEVICE: cpu
EMBED_MODEL: BAAI/bge-m3
RERANK_MODEL: BAAI/bge-reranker-v2-m3
HF_HUB_DOWNLOAD_TIMEOUT: 600
volumes:
- hf-cache:/cache
healthcheck:
test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8000/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 180s
deploy:
resources:
limits:
memory: 3g
W0+W1+W1.2: security hardening, observability, autocomplete, glitchtip, forgejo CI W0 — security hardening (5 fixes verified live on disclosure.top) - middleware: gate /api/admin/* same as /admin/* (F1) - imgproxy: tighten LOCAL_FILESYSTEM_ROOT from / to /var/lib/storage (F2) - studio: real basic-auth label (bcrypt hash, middleware reference) (F3) - relations: ENABLE ROW LEVEL SECURITY + public SELECT policy (F4) - migration 0003: fold is_searchable + hybrid_search update into canonical (TD#2) W1 — observability + resilience + autocomplete - studio: HOSTNAME=0.0.0.0 so Next.js binds on loopback for healthcheck - compose: PG_POOL_MAX=20, CLAUDE_CODE_OAUTH_TOKEN gated by separate env - claude-code.ts: subprocess timeout configurable (CLAUDE_CODE_TIMEOUT_MS) - openrouter.ts: retry with exponential backoff + Retry-After + in-memory circuit breaker (promotes FALLBACK after CB_THRESHOLD failures) - lib/logger.ts: pino logger (NDJSON prod / pretty dev) + withRequest helper - middleware: mints correlation_id, stamps x-correlation-id response header, emits structured http_request log per /api/* call - messages/route.ts: switch to structured logger - 60_meili_index.py: push documents + chunks into Meilisearch - /api/search/autocomplete: parallel meili search (docs + chunks), 5-8ms p50 - search-autocomplete.tsx: debounced dropdown wired into search-panel W1.2 — Glitchtip + Forgejo self-hosted - compose: glitchtip-redis + glitchtip-web + glitchtip-worker (v4.2) - compose: forgejo + forgejo-runner (server v9, runner v6) with group_add=988 - @sentry/nextjs SDK wired (instrumentation.ts + sentry.{client,server}.config.ts) - /api/admin/throw smoke endpoint (gated by W0-F1 middleware) - Synthetic event ingestion verified at glitchtip.disclosure.top - forgejo.disclosure.top up, repo discadmin/disclosure-bureau created, runner registered (labels: ubuntu-latest, docker) - .forgejo/workflows/ci.yml: typecheck + lint + build + npm audit + python syntax + compose validation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:18:42 +00:00
# ─── Glitchtip — self-hosted Sentry-compatible error monitor (W1.2) ───────
glitchtip-redis:
container_name: disclosure-glitchtip-redis
image: redis:7-alpine
restart: unless-stopped
networks: [internal]
volumes:
- glitchtip-redis-data:/data
command: redis-server --appendonly yes
glitchtip-web:
container_name: disclosure-glitchtip-web
image: glitchtip/glitchtip:v4.2
restart: unless-stopped
networks: [internal, traefik]
depends_on:
db: { condition: service_healthy }
glitchtip-redis: { condition: service_started }
environment:
DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASSWORD}@db:5432/glitchtip
SECRET_KEY: ${GLITCHTIP_SECRET_KEY}
REDIS_URL: redis://glitchtip-redis:6379/0
PORT: "8080"
GLITCHTIP_DOMAIN: ${GLITCHTIP_DOMAIN}
DEFAULT_FROM_EMAIL: ${GLITCHTIP_DEFAULT_FROM_EMAIL}
EMAIL_URL: consolemail://
ENABLE_USER_REGISTRATION: "false" # bootstrap admin via manage.py
ENABLE_ORGANIZATION_CREATION: "false"
CELERY_WORKER_AUTOSCALE: "1,3"
CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
volumes:
- glitchtip-uploads:/code/uploads
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-glitchtip.rule=Host(`glitchtip.disclosure.top`)
- traefik.http.routers.disclosure-glitchtip.entrypoints=websecure
- traefik.http.routers.disclosure-glitchtip.tls=true
- traefik.http.routers.disclosure-glitchtip.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-glitchtip.loadbalancer.server.port=8080
glitchtip-worker:
container_name: disclosure-glitchtip-worker
image: glitchtip/glitchtip:v4.2
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
glitchtip-redis: { condition: service_started }
environment:
DATABASE_URL: postgres://glitchtip:${GLITCHTIP_DB_PASSWORD}@db:5432/glitchtip
SECRET_KEY: ${GLITCHTIP_SECRET_KEY}
REDIS_URL: redis://glitchtip-redis:6379/0
GLITCHTIP_DOMAIN: ${GLITCHTIP_DOMAIN}
DEFAULT_FROM_EMAIL: ${GLITCHTIP_DEFAULT_FROM_EMAIL}
EMAIL_URL: consolemail://
CELERY_WORKER_AUTOSCALE: "1,3"
CELERY_WORKER_MAX_TASKS_PER_CHILD: "10000"
volumes:
- glitchtip-uploads:/code/uploads
command: ./bin/run-celery-with-beat.sh
# ─── Forgejo — self-hosted Git + Actions CI (W1.2) ────────────────────────
forgejo:
container_name: disclosure-forgejo
image: codeberg.org/forgejo/forgejo:9
restart: unless-stopped
networks: [internal, traefik]
depends_on:
db: { condition: service_healthy }
environment:
USER_UID: "1000"
USER_GID: "1000"
FORGEJO__database__DB_TYPE: postgres
FORGEJO__database__HOST: db:5432
FORGEJO__database__NAME: forgejo
FORGEJO__database__USER: forgejo
FORGEJO__database__PASSWD: ${FORGEJO_DB_PASSWORD}
FORGEJO__server__DOMAIN: ${FORGEJO_DOMAIN}
FORGEJO__server__ROOT_URL: https://${FORGEJO_DOMAIN}
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_DOMAIN}
FORGEJO__service__DISABLE_REGISTRATION: "true" # admin invites only
FORGEJO__actions__ENABLED: "true"
FORGEJO__security__INSTALL_LOCK: "true"
volumes:
- forgejo-data:/data
labels:
- traefik.enable=true
- traefik.docker.network=traefik-public
- traefik.http.routers.disclosure-forgejo.rule=Host(`forgejo.disclosure.top`)
- traefik.http.routers.disclosure-forgejo.entrypoints=websecure
- traefik.http.routers.disclosure-forgejo.tls=true
- traefik.http.routers.disclosure-forgejo.tls.certresolver=letsencrypt
- traefik.http.services.disclosure-forgejo.loadbalancer.server.port=3000
forgejo-runner:
container_name: disclosure-forgejo-runner
image: code.forgejo.org/forgejo/runner:6
restart: unless-stopped
networks: [internal]
# GID of the docker group on the host — lets the runner (uid 1000) talk
# to the docker socket without running as root.
group_add:
- "988"
depends_on:
forgejo: { condition: service_started }
environment:
FORGEJO_INSTANCE_URL: http://forgejo:3000
FORGEJO_RUNNER_REGISTRATION_TOKEN: ${FORGEJO_RUNNER_TOKEN}
FORGEJO_RUNNER_NAME: disclosure-runner
volumes:
- forgejo-runner-config:/data
- /var/run/docker.sock:/var/run/docker.sock
command:
- sh
- -c
- |
sleep 10
if [ ! -f /data/.runner ]; then
forgejo-runner register --no-interactive --instance "$$FORGEJO_INSTANCE_URL" --token "$$FORGEJO_RUNNER_REGISTRATION_TOKEN" --name "$$FORGEJO_RUNNER_NAME" --labels 'ubuntu-latest:docker://node:20-bookworm,docker:host'
fi
forgejo-runner daemon