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>
515 lines
21 KiB
YAML
515 lines
21 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
|
|
|
|
# ─── 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
|