# Staging compose overlay (reference only). # # Coolify on the hivegrid-staging host does NOT consume this file directly — # each service is provisioned through the Coolify UI. This overlay is the # human-readable source of truth for how the staging stack is meant to be # wired (PgBouncer in front of Postgres, apps depending on it being healthy) # and can be applied locally for end-to-end smoke tests: # # docker compose -f docker-compose.yml -f docker-compose.staging.yml up \ # postgres pgbouncer api web worker-general worker-mail worker-import worker-webhook worker-plugin # # See docs/ops/pgbouncer.md for the matching Coolify deploy procedure. services: postgres: image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777 environment: POSTGRES_DB: ${POSTGRES_DB:-hivegrid_validation} POSTGRES_USER: ${POSTGRES_USER:-hivegrid} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-hivegrid} # Required so pg_authid stores SCRAM hashes that pgbouncer can copy # into its userlist.txt. md5 would force a config-time rehash. POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256 --auth-local=scram-sha-256" command: - postgres - -c - password_encryption=scram-sha-256 # Defensive max_connections so PgBouncer's default_pool_size (25) # multiplied across app stacks stays well below the cap. - -c - max_connections=200 volumes: - postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-hivegrid} -d ${POSTGRES_DB:-hivegrid_validation}"] interval: 10s timeout: 3s retries: 10 start_period: 20s restart: unless-stopped pgbouncer: depends_on: postgres: condition: service_healthy # Explicit one-shot authority. Run with: # docker compose -f docker-compose.yml -f docker-compose.staging.yml --profile migrate run --rm migrator # DIRECT_URL exists only in this short-lived container. Seeding is not part # of the non-superuser migration authority. migrator: build: context: . target: web entrypoint: ["./start.sh", "migrate"] environment: - DIRECT_URL=${MIGRATOR_DIRECT_URL:?MIGRATOR_DIRECT_URL is required} depends_on: postgres: condition: service_healthy profiles: ["migrate"] restart: "no" api: build: context: . target: api environment: - DATABASE_URL=${APP_DATABASE_URL:?APP_DATABASE_URL is required} - DATABASE_RUNTIME_ROLE=hivegrid_app - RLS_RUNTIME_MODE=required - REDIS_URL=${REDIS_URL:-redis://redis:6379/0} - CHAT_GATEWAY_AUTH_SECRET=${CHAT_GATEWAY_AUTH_SECRET:?CHAT_GATEWAY_AUTH_SECRET is required} - CLAMD_HOST=${CLAMD_HOST:-clamav} - CLAMD_PORT=${CLAMD_PORT:-3310} - CLAMD_TIMEOUT_MS=${CLAMD_TIMEOUT_MS:-30000} - STORAGE_ENDPOINT=${STORAGE_ENDPOINT:-} - STORAGE_REGION=${STORAGE_REGION:-} - STORAGE_BUCKET=${STORAGE_BUCKET:-} - STORAGE_ACCESS_KEY_ID=${STORAGE_ACCESS_KEY_ID:-} - STORAGE_SECRET_ACCESS_KEY=${STORAGE_SECRET_ACCESS_KEY:-} - STORAGE_SESSION_TOKEN=${STORAGE_SESSION_TOKEN:-} - STORAGE_FORCE_PATH_STYLE=${STORAGE_FORCE_PATH_STYLE:-false} depends_on: pgbouncer: condition: service_healthy redis: condition: service_healthy clamav: condition: service_healthy web: build: context: . target: web environment: - DATABASE_URL=${APP_DATABASE_URL:?APP_DATABASE_URL is required} - DATABASE_RUNTIME_ROLE=hivegrid_app - RLS_RUNTIME_MODE=required # Dedicated callback-only capability. Never forward this DSN to API, # Hocuspocus, workers, or the one-shot migrator. - AUTHENTIK_ADMIN_JIT_DATABASE_URL=${AUTHENTIK_ADMIN_JIT_DATABASE_URL:-} - REDIS_URL=${REDIS_URL:?REDIS_URL is required} - BULLMQ_REDIS_URL=${BULLMQ_REDIS_URL:-redis://redis:6379/0} - SCAN_QUEUE_ENABLED=${SCAN_QUEUE_ENABLED:-true} - CLAMD_HOST=${CLAMD_HOST:-clamav} - CLAMD_PORT=${CLAMD_PORT:-3310} - CLAMD_TIMEOUT_MS=${CLAMD_TIMEOUT_MS:-30000} - STORAGE_ENDPOINT=${STORAGE_ENDPOINT:?STORAGE_ENDPOINT is required} - STORAGE_REGION=${STORAGE_REGION:?STORAGE_REGION is required} - STORAGE_BUCKET=${STORAGE_BUCKET:?STORAGE_BUCKET is required} - STORAGE_ACCESS_KEY_ID=${WEB_STORAGE_ACCESS_KEY_ID:?WEB_STORAGE_ACCESS_KEY_ID is required} - STORAGE_SECRET_ACCESS_KEY=${WEB_STORAGE_SECRET_ACCESS_KEY:?WEB_STORAGE_SECRET_ACCESS_KEY is required} - STORAGE_SESSION_TOKEN=${WEB_STORAGE_SESSION_TOKEN:-} - STORAGE_FORCE_PATH_STYLE=${STORAGE_FORCE_PATH_STYLE:-false} - GDPR_EXPORT_SSE_C_KEY_BASE64=${GDPR_EXPORT_SSE_C_KEY_BASE64:?GDPR_EXPORT_SSE_C_KEY_BASE64 is required} - AIRCALL_CLIENT_ID=${AIRCALL_CLIENT_ID:-} - AIRCALL_CLIENT_SECRET=${AIRCALL_CLIENT_SECRET:-} - AIRCALL_REDIRECT_URI=${AIRCALL_REDIRECT_URI:-} - AIRCALL_WEBHOOK_URL=${AIRCALL_WEBHOOK_URL:-} - CHAT_GATEWAY_AUTH_SECRET=${CHAT_GATEWAY_AUTH_SECRET:?CHAT_GATEWAY_AUTH_SECRET is required} - WEB_PUSH_VAPID_PUBLIC_KEY=${WEB_PUSH_VAPID_PUBLIC_KEY:?WEB_PUSH_VAPID_PUBLIC_KEY is required} depends_on: pgbouncer: condition: service_healthy redis: condition: service_healthy clamav: condition: service_healthy # Five group-specific workers receive only the NOBYPASSRLS worker DSN. # Migrator and seed credentials are intentionally absent. worker-general: environment: - DATABASE_URL=${WORKER_DATABASE_URL:?WORKER_DATABASE_URL is required} - DATABASE_RUNTIME_ROLE=hivegrid_worker - RLS_RUNTIME_MODE=required - SCAN_QUEUE_ENABLED=true - CLAMAV_HOST=${CLAMAV_HOST:-clamav} - CLAMAV_PORT=${CLAMAV_PORT:-3310} - CLAMAV_TIMEOUT_MS=${CLAMAV_TIMEOUT_MS:-30000} - STORAGE_ENDPOINT=${STORAGE_ENDPOINT:?STORAGE_ENDPOINT is required} - STORAGE_REGION=${STORAGE_REGION:?STORAGE_REGION is required} - STORAGE_BUCKET=${STORAGE_BUCKET:?STORAGE_BUCKET is required} - STORAGE_ACCESS_KEY_ID=${WORKER_GENERAL_STORAGE_ACCESS_KEY_ID:?WORKER_GENERAL_STORAGE_ACCESS_KEY_ID is required} - STORAGE_SECRET_ACCESS_KEY=${WORKER_GENERAL_STORAGE_SECRET_ACCESS_KEY:?WORKER_GENERAL_STORAGE_SECRET_ACCESS_KEY is required} - STORAGE_SESSION_TOKEN=${WORKER_GENERAL_STORAGE_SESSION_TOKEN:-} - STORAGE_FORCE_PATH_STYLE=${STORAGE_FORCE_PATH_STYLE:-false} - STORAGE_QUARANTINE_BUCKET=${STORAGE_QUARANTINE_BUCKET:-} - MEILI_URL=${MEILI_URL:?MEILI_URL is required} - MEILI_API_KEY=${MEILI_API_KEY:?MEILI_API_KEY is required} - EXPORT_RETENTION_QUEUE_ENABLED=true - OUTBOX_DISPATCHER_ENABLED=true - OUTBOX_DISPATCHER_REQUIRED=true - LAGO_API_URL=${LAGO_API_URL:?LAGO_API_URL is required} - LAGO_API_KEY=${LAGO_API_KEY:?LAGO_API_KEY is required} - LAGO_TIMEOUT_MS=${LAGO_TIMEOUT_MS:-10000} - MOLLIE_API_KEY=${MOLLIE_API_KEY:?MOLLIE_API_KEY is required} - MOLLIE_PROFILE_ID=${MOLLIE_PROFILE_ID:-} - MOLLIE_TIMEOUT_MS=${MOLLIE_TIMEOUT_MS:-10000} - BILLING_MOLLIE_WEBHOOK_URL=${BILLING_MOLLIE_WEBHOOK_URL:?BILLING_MOLLIE_WEBHOOK_URL is required} - WORKFLOW_TRIGGER_OUTBOX_ENABLED=true - WORKFLOW_TRIGGER_OUTBOX_REQUIRED=true - KWP_HIVEGRID_TENANT_ID=${KWP_HIVEGRID_TENANT_ID:-} depends_on: pgbouncer: condition: service_healthy worker-mail: environment: - DATABASE_URL=${WORKER_DATABASE_URL:?WORKER_DATABASE_URL is required} - DATABASE_RUNTIME_ROLE=hivegrid_worker - RLS_RUNTIME_MODE=required depends_on: pgbouncer: condition: service_healthy worker-import: environment: - DATABASE_URL=${WORKER_DATABASE_URL:?WORKER_DATABASE_URL is required} - DATABASE_RUNTIME_ROLE=hivegrid_worker - RLS_RUNTIME_MODE=required - IMPORT_QUEUE_ENABLED=false - IMPORT_QUEUE_RUNTIME_READY=false - PORTAL_GDPR_EXPORT_QUEUE_ENABLED=true - STORAGE_ENDPOINT=${STORAGE_ENDPOINT:?STORAGE_ENDPOINT is required} - STORAGE_REGION=${STORAGE_REGION:?STORAGE_REGION is required} - STORAGE_BUCKET=${STORAGE_BUCKET:?STORAGE_BUCKET is required} - STORAGE_ACCESS_KEY_ID=${WORKER_IMPORT_STORAGE_ACCESS_KEY_ID:?WORKER_IMPORT_STORAGE_ACCESS_KEY_ID is required} - STORAGE_SECRET_ACCESS_KEY=${WORKER_IMPORT_STORAGE_SECRET_ACCESS_KEY:?WORKER_IMPORT_STORAGE_SECRET_ACCESS_KEY is required} - STORAGE_SESSION_TOKEN=${WORKER_IMPORT_STORAGE_SESSION_TOKEN:-} - STORAGE_FORCE_PATH_STYLE=${STORAGE_FORCE_PATH_STYLE:-false} - GDPR_EXPORT_SSE_C_KEY_BASE64=${GDPR_EXPORT_SSE_C_KEY_BASE64:?GDPR_EXPORT_SSE_C_KEY_BASE64 is required} - KWP_HIVEGRID_TENANT_ID=${KWP_HIVEGRID_TENANT_ID:-} depends_on: pgbouncer: condition: service_healthy worker-webhook: environment: - DATABASE_URL=${WORKER_DATABASE_URL:?WORKER_DATABASE_URL is required} - DATABASE_RUNTIME_ROLE=hivegrid_worker - RLS_RUNTIME_MODE=required - WEB_PUSH_VAPID_SUBJECT=${WEB_PUSH_VAPID_SUBJECT:?WEB_PUSH_VAPID_SUBJECT is required} - WEB_PUSH_VAPID_PUBLIC_KEY=${WEB_PUSH_VAPID_PUBLIC_KEY:?WEB_PUSH_VAPID_PUBLIC_KEY is required} - WEB_PUSH_VAPID_PRIVATE_KEY=${WEB_PUSH_VAPID_PRIVATE_KEY:?WEB_PUSH_VAPID_PRIVATE_KEY is required} depends_on: pgbouncer: condition: service_healthy worker-plugin: environment: - DATABASE_URL=${WORKER_DATABASE_URL:?WORKER_DATABASE_URL is required} - DATABASE_RUNTIME_ROLE=hivegrid_worker - RLS_RUNTIME_MODE=required depends_on: pgbouncer: condition: service_healthy hocuspocus: environment: - NODE_ENV=production - HOCUSPOCUS_PORT=1234 - HOCUSPOCUS_HOST=0.0.0.0 - HOCUSPOCUS_AUTH_SECRET=${HOCUSPOCUS_AUTH_SECRET:-} - DATABASE_URL=${APP_DATABASE_URL:?APP_DATABASE_URL is required} - DATABASE_RUNTIME_ROLE=hivegrid_app - RLS_RUNTIME_MODE=required volumes: postgres-data: