services: db: image: postgres:16-alpine restart: unless-stopped environment: - POSTGRES_DB=minder - POSTGRES_USER=minder - POSTGRES_PASSWORD=minder volumes: - postgres_data:/var/lib/postgresql/data - ./schema.sql:/docker-entrypoint-initdb.d/schema.sql:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U minder -d minder"] interval: 5s timeout: 5s retries: 5 adminer: image: adminer:latest restart: unless-stopped ports: - "8081:8080" environment: - ADMINER_DEFAULT_SERVER=db - ADMINER_DEFAULT_DB=minder - ADMINER_DEFAULT_USERNAME=minder - ADMINER_DEFAULT_PASSWORD=minder - ADMINER_DESIGN=pepa-linha # Single public entry point. nginx is the only service that publishes a host # port; it reverse-proxies the whole app (UI + /api + /ws) to `minder`. Change # the left side of "8080:80" to move the one exposed port. nginx: image: nginx:1.27-alpine restart: unless-stopped command: ["start-dev", "--import-realm"] environment: - KC_DB=postgres - KC_DB_URL=jdbc:postgresql://db:5432/keycloak - KC_DB_USERNAME=minder - KC_DB_PASSWORD=minder # Full URL fixes the token issuer to http://localhost:8082/realms/minder for # ALL clients (browser AND in-compose backchannel), so it matches what Minder # validates (KEYCLOAK_PUBLIC_URL). Without a fixed hostname, a token fetched # via keycloak:8080 gets issuer localhost:8080 → issuer-mismatch 401. - KC_HOSTNAME=http://localhost:8082 - KC_HTTP_ENABLED=true - KEYCLOAK_ADMIN=admin - KEYCLOAK_ADMIN_PASSWORD=${KEYCLOAK_ADMIN_PASSWORD:-admin} # Substituted into realm-export.json's minder-module client secret at # --import-realm (Keycloak replaces ${VAR} via System.getenv by default). # Must match MINDER_MODULE_CLIENT_SECRET on module-template-web/worker. - MINDER_MODULE_CLIENT_SECRET=${MINDER_MODULE_CLIENT_SECRET:-CHANGE-ME-IN-ENV} ports: - "8080:80" volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro depends_on: - minder minder: build: . restart: unless-stopped ports: - "${MINDER_HOST_PORT:-8080}:8080" environment: - OPENAI_API_KEY=${OPENAI_API_KEY} - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} - MINDER_API_BASE_URL=${MINDER_API_BASE_URL} - MINDER_MODEL=${MINDER_MODEL} - DATABASE_URL=${DATABASE_URL:-postgresql://minder:minder@db:5432/minder} - MINDER_REDIS_URL=redis://redis:6379/0 - MINDER_DISABLED_MODULES=${MINDER_DISABLED_MODULES:-} # Speech-to-text backend (voice input). Hard-coded to the internal `asr` # sidecar so it is correct regardless of the host-oriented value in .env. - MINDER_ASR_BASE_URL=http://asr:9000/v1 # Externally-hosted Keycloak — both URLs point at the public instance # (browser and backend reach it over the same HTTPS endpoint). Values come # from .env; issuer is validated against KEYCLOAK_PUBLIC_URL. - AUTH_MODE=${AUTH_MODE:-keycloak} - KEYCLOAK_URL=${KEYCLOAK_URL} - KEYCLOAK_PUBLIC_URL=${KEYCLOAK_PUBLIC_URL} - KEYCLOAK_REALM=${KEYCLOAK_REALM:-minder} - KEYCLOAK_BACKEND_CLIENT_ID=${KEYCLOAK_BACKEND_CLIENT_ID:-minder-backend} - KEYCLOAK_BACKEND_CLIENT_SECRET=${KEYCLOAK_BACKEND_CLIENT_SECRET} extra_hosts: - "host.docker.internal:host-gateway" volumes: - minder_data:/home/minder/.minder # Mount modules so new/edited modules appear without rebuilding the image - ./modules:/app/modules # Redis — backs the TaskIQ broker, job state, blackboard, and message bus. # Loopback-published so a HOST-run backend (uvicorn on the host, not in-compose) # can reach it at localhost:6379. In-compose services use redis:6379. redis: image: redis:7-alpine restart: unless-stopped ports: - "127.0.0.1:${REDIS_HOST_PORT:-6379}:6379" healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 5s timeout: 3s retries: 10 # CPU speech-to-text sidecar — nvidia/nemotron-3.5-asr-streaming-0.6b via HF # transformers, exposing an OpenAI-compatible /v1/audio/transcriptions. No GPU # needed (push-to-talk latency ~2-6s per short clip). Swap MINDER_ASR_BASE_URL # to a managed/GPU endpoint to replace it with zero code change. Not gated by a # profile, so it starts with the normal `docker compose up`. # asr: # build: ./services/asr # ports: # - "9000:9000" # volumes: # - asr_models:/models # healthcheck: # test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:9000/health')"] # interval: 15s # timeout: 5s # retries: 5 # start_period: 60s # restart: unless-stopped # ---- module_template (full-stack SDK reference) ------------------------- # MinIO — S3-compatible media store for module_template (own bucket). minio: image: minio/minio:latest command: ["server", "/data", "--console-address", ":9001"] environment: - MINIO_ROOT_USER=${MT_S3_ACCESS_KEY:-minioadmin} - MINIO_ROOT_PASSWORD=${MT_S3_SECRET_KEY:-minioadmin} ports: # Host 9002 → container 9000 (S3 API); asr already uses host 9000. In-compose # services still reach MinIO at minio:9000. Console on 9001. - "9002:9000" - "9001:9001" volumes: - module_template_media:/data restart: unless-stopped # ---- Modules run as INDEPENDENT compose projects (runtime registration) ---- # Module app/worker services are NOT defined here. Each module has its own # docker-compose.yml that joins the shared `minder_net` network and announces # to a running Minder — start it and it appears live in Minder; stop it and it # disappears. `minio` above stays as shared module infra (it is not a Minder # module, so it does not "appear" in Minder). # docker compose -f modules/produce/docker-compose.yml up -d --build # docker compose -f modules/module_template/docker-compose.yml up -d --build volumes: minder_data: postgres_data: asr_models: module_template_media: # Stable network name so INDEPENDENT module compose projects can join it as # `external`. Every core service (base + local override) lands on `minder_net`; # modules reach `minder`/`db`/`redis`/`minio`/`keycloak` by name across projects. networks: default: name: minder_net