dograh/docker-compose.yaml

342 lines
12 KiB
YAML
Raw Normal View History

# Dograh deployment stack — driven by the helper scripts, not by a bare
# `docker compose up`.
#
# This stack needs a generated .env (secrets, public host/URL) and, for the
# remote/TURN profiles, runtime nginx/coturn config rendered by the dograh-init
# service. The setup scripts create those for you — start with them:
#
# Local: ./start_docker.sh (Windows: .\start_docker.ps1)
# Remote server: sudo ./setup_remote.sh then ./remote_up.sh
#
# Running `docker compose up` against a fresh checkout will fail or come up
# misconfigured (e.g. OSS_JWT_SECRET is required). Full guide:
# https://docs.dograh.com/deployment/docker
2025-09-09 14:37:32 +05:30
services:
postgres:
image: pgvector/pgvector:pg17
2025-09-09 14:37:32 +05:30
environment:
POSTGRES_USER: postgres
# Sourced from .env. Defaults to "postgres"
# NOTE: changing this on an existing install does NOT
# re-key the database — the password is baked into the volume on first init.
# You can manually change the password using psql in the container
POSTGRES_PASSWORD: "${POSTGRES_PASSWORD:-postgres}"
2025-09-09 14:37:32 +05:30
POSTGRES_DB: postgres
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 3s
timeout: 3s
retries: 10
networks:
- app-network
redis:
image: redis:7
ports:
- "6379:6379"
command: >
--requirepass ${REDIS_PASSWORD:-redissecret}
2025-09-09 14:37:32 +05:30
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-redissecret}", "ping"]
2025-09-09 14:37:32 +05:30
interval: 3s
timeout: 10s
retries: 10
networks:
- app-network
minio:
image: minio/minio
container_name: minio
command: server /data --console-address ":9001"
environment:
2026-06-21 13:44:31 +05:30
MINIO_ROOT_USER: "${MINIO_ROOT_USER:-minioadmin}"
MINIO_ROOT_PASSWORD: "${MINIO_ROOT_PASSWORD:-minioadmin}"
MINIO_API_CORS_ALLOW_ORIGIN: "*"
2025-09-09 14:37:32 +05:30
ports:
- "127.0.0.1:9000:9000" # Bind to localhost explicitly
- "127.0.0.1:9001:9001"
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
dograh-init:
image: bash:5.2
container_name: dograh_init
profiles: ["remote", "local-turn"]
environment:
ENVIRONMENT: "${ENVIRONMENT:-local}"
SERVER_IP: "${SERVER_IP:-}"
PUBLIC_HOST: "${PUBLIC_HOST:-}"
PUBLIC_BASE_URL: "${PUBLIC_BASE_URL:-}"
BACKEND_API_ENDPOINT: "${BACKEND_API_ENDPOINT:-http://localhost:8000}"
MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT:-http://localhost:9000}"
TURN_HOST: "${TURN_HOST:-}"
TURN_SECRET: "${TURN_SECRET:-}"
FASTAPI_WORKERS: "${FASTAPI_WORKERS:-1}"
volumes:
- ./scripts:/workspace/scripts:ro
- ./deploy:/workspace/deploy:ro
- ./certs:/certs:ro
- nginx-generated:/generated/nginx
- coturn-generated:/generated/coturn
command:
- /workspace/scripts/run_dograh_init.sh
nginx:
image: nginx:alpine
container_name: nginx_https
profiles: ["remote"]
depends_on:
dograh-init:
condition: service_completed_successfully
ui:
condition: service_started
ports:
- "80:80"
- "443:443"
volumes:
- nginx-generated:/etc/nginx/conf.d:ro
- ./certs:/etc/nginx/certs:ro
networks:
- app-network
coturn:
image: coturn/coturn:4.8.0
container_name: coturn
restart: unless-stopped
profiles: ["remote", "local-turn"]
depends_on:
dograh-init:
condition: service_completed_successfully
ports:
- "3478:3478/udp"
- "3478:3478/tcp"
- "5349:5349/udp"
- "5349:5349/tcp"
- "49152-49200:49152-49200/udp"
volumes:
- coturn-generated:/etc/coturn:ro
command:
- -c
- /etc/coturn/turnserver.conf
networks:
- app-network
2025-09-09 14:37:32 +05:30
api:
image: ${REGISTRY:-dograhai}/dograh-api:latest
2025-09-09 14:37:32 +05:30
environment:
# Core application config
ENVIRONMENT: "${ENVIRONMENT:-local}"
2025-09-09 14:37:32 +05:30
LOG_LEVEL: "INFO"
feat(auth): gate OSS signup behind ENABLE_SIGNUP flag (#514) * feat(auth): gate OSS signup behind ENABLE_SIGNUP flag ## Problem The `POST /api/v1/auth/signup` endpoint is unconditionally exposed on every OSS install. Operators running an invite-only deployment (private customer instances, staging environments, internal-only tenants) have no way to disable public account creation without patching the codebase. The UI also shows the "Sign up" link on `/auth/login` regardless of whether signup is available, so a locked-down deployment leaves broken navigation on the login page. ## Fix Introduce a single `ENABLE_SIGNUP` env var (default `true` — no behavior change for existing installs) that controls signup end-to-end: - **Backend** — `api/constants.ENABLE_SIGNUP` is read at module load. The signup handler returns 403 when it's false. Also exposed on `GET /api/v1/health` as `signup_enabled: bool` so the UI can mirror the operator's choice at runtime instead of at bundle-build time. - **UI** — `getSignupEnabled()` in `lib/auth/config.ts` proxies the health field, `/api/config/auth` surfaces it to the browser, the login page conditionally renders the "Sign up" link via a one-shot `fetch("/api/config/auth")` in `useEffect`, and the middleware redirects `/auth/signup` → `/auth/login` when disabled (fires before Next.js can serve the statically-prerendered signup page). - **Helm** — `config.enableSignup` (default `true`) is rendered into the ConfigMap as `ENABLE_SIGNUP` so operators can flip it via `--set config.enableSignup=false` at install/upgrade time. Fallbacks default to `signupEnabled: true` in every layer so a fresh install "just works" and matches the backend default. * address review: rollout on ConfigMap change, cache TTL, no signup-link flash Four review points on #514: **P1 — ConfigMap Change Skips Rollout** (`configmap.yaml`). `helm upgrade --set config.enableSignup=false` updated the ConfigMap but did NOT roll the api pods, so running processes kept the ENABLE_SIGNUP env from startup and continued serving the old signup behavior — including divergence between replicas mid-upgrade. Fix: add the standard `checksum/config` pod-template annotation on the four backend Deployments that `envFrom` the ConfigMap (`web`, `arq-worker`, `ari-manager`, `campaign-orchestrator`). Verified with `helm template`: all four Deployments share the same checksum on any given render, and flipping `config.enableSignup` changes the checksum uniformly so kubectl sees a pod-template diff and rolls all four. **P1 — Signup Flag Stays Cached (server)** (`ui/src/lib/auth/config.ts`). Module-scoped cache had no TTL. `revalidate: 300` was passed on the underlying `fetch()` but the in-memory short-circuit above ran first, so the value never refreshed until the UI pod restarted. Fix: add `AUTH_CONFIG_TTL_MS = 5 * 60 * 1000` (matching the fetch revalidate hint) so the module cache and the Next fetch cache stay in sync. Backend flag flips propagate within 5 minutes without a pod restart. **P1 — Middleware Redirect Uses Stale State** (`ui/src/middleware.ts`). Same shape as above — a separate module cache with no expiry could keep redirecting `/auth/signup → /auth/login` after signup was re-enabled, or keep serving the statically-prerendered signup page after lockdown. Fix: same `SERVER_CONFIG_TTL_MS = 5 * 60 * 1000` TTL on the middleware cache. **P2 — Signup link flash on login page** (`ui/src/app/auth/login/page.tsx`). Initial `signupEnabled` state was `null`, so `{signupEnabled && ...}` hid the link on first paint and it popped in after the fetch resolved — a CLS on every login-page load on stock installs where signup is enabled. Fix: initialise the state to `true` (matches the backend default). The fetch still overrides to `false` when the operator has actually disabled signup, so the lockdown UI behavior is unchanged; only the happy-path flash is gone. * simplify signup flag: drop TTL caches and middleware redirect Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * resolve signup flag server-side to avoid signup link flicker Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: prabhat pankaj <prabhatiitbhu@gmail.com> Co-authored-by: Abhishek Kumar <abhishek@a6k.me> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-13 14:08:25 +05:30
# Set to "false" in .env to disable public signup (invite-only installs).
ENABLE_SIGNUP: "${ENABLE_SIGNUP:-true}"
# Public origin for this deployment. The API derives BACKEND_API_ENDPOINT,
# MINIO_PUBLIC_ENDPOINT and TURN_HOST from PUBLIC_BASE_URL / PUBLIC_HOST when
# they are not set explicitly (see api/constants.py), so a standard remote
# install only needs PUBLIC_BASE_URL + PUBLIC_HOST in .env.
PUBLIC_BASE_URL: "${PUBLIC_BASE_URL:-}"
PUBLIC_HOST: "${PUBLIC_HOST:-}"
# Optional explicit override of the public URL the backend builds webhook /
# embed links from. Defaults to PUBLIC_BASE_URL. When the value is non-public
# (localhost or a private/reserved IP), the API resolves a running Cloudflare
# tunnel's URL at runtime instead (see api/utils/common.py).
BACKEND_API_ENDPOINT: "${BACKEND_API_ENDPOINT:-}"
2025-09-09 14:37:32 +05:30
# Database configuration (using containerized postgres)
DATABASE_URL: "postgresql+asyncpg://postgres:${POSTGRES_PASSWORD:-postgres}@postgres:5432/postgres"
2025-09-09 14:37:32 +05:30
# Redis configuration (using containerized redis)
REDIS_URL: "redis://:${REDIS_PASSWORD:-redissecret}@redis:6379"
2025-09-09 14:37:32 +05:30
# Storage configuration - bundled MinIO by default. Set ENABLE_AWS_S3=true
# in .env to make the API use AWS S3 or another S3-compatible server.
ENABLE_AWS_S3: "${ENABLE_AWS_S3:-false}"
2025-09-09 14:37:32 +05:30
# S3 backend configuration. Compose's .env file is used for interpolation,
# but those values are not automatically injected into containers, so pass
# the S3 settings through explicitly.
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID:-}"
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY:-}"
AWS_SESSION_TOKEN: "${AWS_SESSION_TOKEN:-}"
S3_BUCKET: "${S3_BUCKET:-}"
S3_REGION: "${S3_REGION:-us-east-1}"
# For a non-AWS S3-compatible server, also set these in Compose's .env
# S3_ENDPOINT_URL e.g. https://s3.example.com
# S3_SIGNATURE_VERSION set "s3v4" if the server requires SigV4 (e.g. rustfs)
# S3_ADDRESSING_STYLE set "path" if the server / TLS cert requires path-style
# The S3 backend issues real presigned URLs, so the bucket can stay private.
S3_ENDPOINT_URL: "${S3_ENDPOINT_URL:-}"
S3_SIGNATURE_VERSION: "${S3_SIGNATURE_VERSION:-}"
S3_ADDRESSING_STYLE: "${S3_ADDRESSING_STYLE:-}"
2025-09-09 14:37:32 +05:30
# MinIO
MINIO_ENDPOINT: "minio:9000"
# Full URL (with scheme) browsers use to reach MinIO. Defaults to
# PUBLIC_BASE_URL for remote deployments (nginx proxies /voice-audio/) and to
# http://localhost:9000 for local; override only for a separate object store.
MINIO_PUBLIC_ENDPOINT: "${MINIO_PUBLIC_ENDPOINT:-}"
2026-06-21 13:44:31 +05:30
MINIO_ACCESS_KEY: "${MINIO_ROOT_USER:-minioadmin}"
MINIO_SECRET_KEY: "${MINIO_ROOT_PASSWORD:-minioadmin}"
2025-09-09 14:37:32 +05:30
MINIO_BUCKET: "voice-audio"
MINIO_SECURE: "false"
# Number of uvicorn worker processes (each is its own process bound to a
# distinct port starting at 8000). dograh-init renders nginx upstreams
# from this value and nginx load-balances across them with least_conn.
FASTAPI_WORKERS: "${FASTAPI_WORKERS:-1}"
2026-06-18 18:56:58 +05:30
# Trust X-Forwarded-* headers from any peer so uvicorn honors nginx's
# `X-Forwarded-Proto: https`. nginx runs as its own container and reaches
# uvicorn from a Docker-network IP (not loopback), but uvicorn trusts only
# 127.0.0.1 by default — so without this it ignores the header, request.url
# comes back as http://…, and inbound webhook signature checks fail for
# every provider at once (telephony providers sign the public https URL, so
# the recomputed signature won't match). "*" trusts all peers; the api port
# (8000) is published, so firewall/harden it at the host — or narrow this
# to your Docker bridge subnet — if that exposure matters to you.
FORWARDED_ALLOW_IPS: "*"
# Langfuse — credentials can be set here or per-organization via the UI
# at /settings. Tracing is automatically active when credentials are
# available; uncomment to set defaults for all organizations.
2025-09-09 14:37:32 +05:30
# LANGFUSE_SECRET_KEY: ""
# LANGFUSE_PUBLIC_KEY: ""
# LANGFUSE_HOST: ""
2025-09-09 14:37:32 +05:30
# TURN server configuration (for WebRTC NAT traversal in remote server)
# Uses time-limited credentials via TURN REST API (HMAC-SHA1)
TURN_HOST: "${TURN_HOST:-}"
TURN_SECRET: "${TURN_SECRET:-}"
FORCE_TURN_RELAY: "${FORCE_TURN_RELAY:-false}"
OSS_JWT_SECRET: "${OSS_JWT_SECRET:?OSS_JWT_SECRET must be set to a strong secret}"
# Telemetry
ENABLE_TELEMETRY: "${ENABLE_TELEMETRY:-true}"
POSTHOG_API_KEY: "phc_ItizB1dP6yv7ZYobbcqrpxTdbomDA8hJFSEmAMdYvIr"
POSTHOG_HOST: "https://us.i.posthog.com"
2025-09-09 14:37:32 +05:30
ports:
- "8000:8000"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy
healthcheck:
test:
[
"CMD-SHELL",
'python -c "import urllib.request; urllib.request.urlopen(''http://localhost:8000/api/v1/health'').read()"',
]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- app-network
ui:
image: ${REGISTRY:-dograhai}/dograh-ui:latest
2025-09-09 14:37:32 +05:30
environment:
2026-06-18 17:34:48 +05:30
# Bind the Next.js standalone server to all interfaces
HOSTNAME: "0.0.0.0"
2025-09-09 14:37:32 +05:30
# Server-side URL (SSR, internal Docker network)
BACKEND_URL: "${BACKEND_URL:-http://api:8000}"
NODE_ENV: "oss"
2026-06-18 17:34:48 +05:30
# Flag to enable/ disable posthog
ENABLE_TELEMETRY: "${ENABLE_TELEMETRY:-true}"
2025-09-09 14:37:32 +05:30
# Posthog
POSTHOG_KEY: "phc_ItizB1dP6yv7ZYobbcqrpxTdbomDA8hJFSEmAMdYvIr"
POSTHOG_HOST: "https://us.posthog.com"
2025-09-09 14:37:32 +05:30
ports:
- "3010:3010"
2025-09-09 14:37:32 +05:30
depends_on:
api:
condition: service_healthy
healthcheck:
test:
[
"CMD-SHELL",
2026-06-18 17:34:48 +05:30
"wget --no-verbose --tries=1 --spider http://127.0.0.1:3010 || exit 1",
2025-09-09 14:37:32 +05:30
]
interval: 30s
timeout: 10s
retries: 3
start_period: 30s
networks:
- app-network
# Cloudflare tunnel for inbound webhook / WSS reachability when the host has no
# usable public IP (behind NAT or a firewall). Gated behind the "tunnel" profile
# so public-IP installs (served directly by nginx) never start it, and the api
# service no longer hard-depends on it. Two modes, chosen by the token:
# - CLOUDFLARE_TUNNEL_TOKEN set -> named tunnel with a stable hostname. Point
# its ingress at http://api:8000 in the Cloudflare dashboard and set
# BACKEND_API_ENDPOINT in .env to that hostname.
# - token unset -> quick tunnel with an ephemeral
# *.trycloudflare.com URL the API discovers from the metrics endpoint
# (api/utils/tunnel.py). Convenient for local dev.
cloudflared:
image: cloudflare/cloudflared:latest
container_name: cloudflared-tunnel
profiles: ["tunnel"]
restart: unless-stopped
environment:
# cloudflared automatically picks up the token from TUNNEL_TOKEN when
# running `tunnel run`. Leave empty to fall back to a quick tunnel.
TUNNEL_TOKEN: "${CLOUDFLARE_TUNNEL_TOKEN:-}"
# The cloudflared image is distroless (no `sh`), so a `sh -c` conditional
# entrypoint can't run. The image's own entrypoint is already
# `cloudflared --no-autoupdate`; pick the mode via a single command instead:
# - token set -> set CLOUDFLARED_COMMAND="tunnel run" in .env for a named
# tunnel (cloudflared reads TUNNEL_TOKEN from the env above).
# - token unset -> the default below runs a quick tunnel with an ephemeral
# *.trycloudflare.com URL the API discovers from the metrics endpoint.
command: ${CLOUDFLARED_COMMAND:-tunnel --url http://api:8000 --metrics 0.0.0.0:2000}
ports:
- "2000:2000" # metrics endpoint (quick-tunnel URL discovery)
networks:
- app-network
2025-09-09 14:37:32 +05:30
volumes:
postgres_data:
redis_data:
minio-data:
driver: local
nginx-generated:
driver: local
coturn-generated:
driver: local
2025-09-09 14:37:32 +05:30
networks:
app-network:
driver: bridge