disclosure-bureau/infra/disclosure-stack/docker-compose.yml
Luiz Gustavo 189a771cbe
Some checks failed
CI / Web — typecheck + lint + build (push) Failing after 38s
CI / Scripts — Python smoke (push) Failing after 3s
CI / Web — npm audit (push) Failing after 33s
CI / Retrieval — golden set (Recall@5 + MRR) (push) Failing after 4s
W3.1-W3.4: Investigation Bureau foundation — migrations, runtime, Locard
Migrations:
- 0004_investigation_bureau.sql: 7 new tables (investigation_jobs + evidence,
  hypotheses, contradictions, witnesses, gaps, residual_uncertainties), id
  sequences, pg_notify trigger on investigation_jobs, RLS read-only public,
  investigator role with least-privilege grants (no service_role).
- 0005_investigator_write_policies.sql: fixup adding RLS INSERT/UPDATE
  policies bound to investigator + service_role + postgres (RLS with only a
  SELECT policy was silently blocking the worker's claim UPDATE).

investigator-runtime/ (new Bun + TS container):
- src/main.ts: LISTEN/NOTIFY poller, claim-with-SKIP-LOCKED, drain pool,
  healthcheck file, graceful SIGTERM shutdown.
- src/orchestrator.ts: chief-detective dispatch (evidence_chain → Locard).
  Marks job failed when all per-item outputs error; surfaces first errors.
- src/lib/{env,pg,audit,ids,claude}.ts: typed config (gate #8), pool +
  dedicated LISTEN client, NDJSON audit, sequence allocator (E-NNNN etc),
  claude -p subprocess with quota detection (api_error_status=429).
- src/tools/write_evidence.ts: schema-validate (grade A/B/C custody steps),
  resolve chunk_pk via FK, verify verbatim_excerpt actually appears in
  chunk content, INSERT + render case/evidence/E-NNNN.md + audit.
- src/detectives/locard.ts: load chunk → call Claude with locard.md system
  prompt → parse strict JSON → call writeEvidence locally.
- Dockerfile installs `claude` CLI (OAuth) at build time.

Compose:
- new `investigator` service builds from investigator-runtime/, connects
  with low-privilege role, mounts case/ RW and wiki/+raw/ RO, 512m mem cap.

Web:
- /api/admin/investigate/test (POST+GET) gated by middleware (W0-F1).
  POST creates a job, GET polls status. For W3.6 it becomes the chat tool.

End-to-end smoke: INSERT job → pg_notify → claim → Locard dispatch →
claude subprocess invoked. Auth works (CLI v2.1.150). Currently quota
exhausted (weekly limit · resets 3pm UTC) — pipeline catches the typed
isQuota error, marks job failed with surfaced reason. Architecture proven;
quota reset enables real evidence creation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 19:49:33 -03:00

555 lines
23 KiB
YAML

# 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:
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}
# 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-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:
# 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-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
# 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
# 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
# ─── Investigation Bureau runtime — W3.1+ ─────────────────────────────────
#
# 8 detectives + chief-detective orchestrator. Listens on Postgres
# LISTEN/NOTIFY (channel `investigation_jobs`), spawns `claude -p`
# subprocesses (Sonnet via OAuth Max 20x) to produce evidence, hypotheses,
# contradictions, etc. Writes go through gated tools that validate schema
# + chunk references before INSERT.
#
# Connects with the LOW-PRIVILEGE `investigator` role (not service_role).
# Mounts case/ as RW and wiki/ as RO. Reads its OAuth token from env.
investigator:
container_name: disclosure-investigator
build:
context: /data/disclosure/investigator-runtime # synced from laptop, mirrors web/ pattern
dockerfile: Dockerfile
restart: unless-stopped
networks: [internal]
depends_on:
db: { condition: service_healthy }
embed: { condition: service_healthy }
environment:
DATABASE_URL: postgres://investigator:${INVESTIGATOR_DB_PASSWORD}@db:5432/postgres
EMBED_SERVICE_URL: http://embed:8000
CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN}
CLAUDE_MODEL: ${INVESTIGATOR_MODEL:-sonnet}
MAX_PARALLEL_WORKERS: ${INVESTIGATOR_MAX_PARALLEL_WORKERS:-2}
BUDGET_CAP_USD_PER_JOB: ${INVESTIGATOR_BUDGET_CAP_USD_PER_JOB:-1.00}
JOB_TIMEOUT_SECONDS: ${INVESTIGATOR_JOB_TIMEOUT_SECONDS:-300}
CASE_ROOT: /data/ufo/case
WIKI_ROOT: /data/ufo/wiki
AUDIT_LOG: /data/ufo/case/audit.jsonl
volumes:
- ${DATA_RAW}:/data/ufo/raw:ro
- ${DATA_WIKI}:/data/ufo/wiki:ro
- ${CASE_ROOT:-/data/disclosure/case}:/data/ufo/case
deploy:
resources:
limits:
memory: 512m
# ─── 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
# ─── 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