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

367 lines
14 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:
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}
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 is not configured yet, so we auto-confirm signups (skips email
# verification). Switch to "false" once SMTP_PASS is set.
GOTRUE_MAILER_AUTOCONFIRM: "true"
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"
IMGPROXY_LOCAL_FILESYSTEM_ROOT: /
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:
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
- traefik.http.middlewares.disclosure-studio-auth.basicauth.usersfile=/dev/null
# Studio is sensitive — protect with basic auth. We use the dashboard creds via labels:
# Generate htpasswd format with: htpasswd -nbB admin <pass>
# ─── 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
# Chat agent
CLAUDE_CODE_OAUTH_TOKEN: ${CLAUDE_CODE_OAUTH_TOKEN}
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
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