diff --git a/.woodpecker/api.yml b/.woodpecker/api.yml index 7d01e3e..18d15b2 100644 --- a/.woodpecker/api.yml +++ b/.woodpecker/api.yml @@ -112,7 +112,7 @@ steps: ENCRYPTION_KEY: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" commands: - *use_deps - - pnpm --filter "@mosaic/api" exec vitest run --exclude 'src/auth/auth-rls.integration.spec.ts' --exclude 'src/credentials/user-credential.model.spec.ts' --exclude 'src/job-events/job-events.performance.spec.ts' --exclude 'src/knowledge/services/fulltext-search.spec.ts' + - pnpm --filter "@mosaic/api" exec vitest run --exclude 'src/auth/auth-rls.integration.spec.ts' --exclude 'src/credentials/user-credential.model.spec.ts' --exclude 'src/job-events/job-events.performance.spec.ts' --exclude 'src/knowledge/services/fulltext-search.spec.ts' --exclude 'src/mosaic-telemetry/mosaic-telemetry.module.spec.ts' depends_on: - prisma-migrate diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 43143cd..45245ec 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -60,13 +60,10 @@ FROM node:24-slim AS production # Remove npm (unused in production — we use pnpm) to reduce attack surface RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx -# Install dumb-init for proper signal handling -# Clear stale APT lists first — Kaniko's layer extraction can leave -# base-image metadata with expired GPG signatures (bookworm InRelease). -RUN rm -rf /var/lib/apt/lists/* \ - && apt-get update \ - && apt-get install -y --no-install-recommends dumb-init \ - && rm -rf /var/lib/apt/lists/* +# Install dumb-init for proper signal handling (static binary from GitHub, +# avoids apt-get which fails under Kaniko with bookworm GPG signature errors) +ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64 /usr/local/bin/dumb-init +RUN chmod 755 /usr/local/bin/dumb-init # Create non-root user RUN groupadd -g 1001 nodejs && useradd -m -u 1001 -g nodejs nestjs diff --git a/apps/coordinator/Dockerfile b/apps/coordinator/Dockerfile index 756bcd9..c20f1b1 100644 --- a/apps/coordinator/Dockerfile +++ b/apps/coordinator/Dockerfile @@ -1,16 +1,10 @@ # Multi-stage build for mosaic-coordinator -FROM python:3.11-slim AS builder +# Builder uses the full Python image which already includes gcc/g++/make, +# avoiding a 336 MB build-essential install that exceeds Kaniko disk budget. +FROM python:3.11 AS builder WORKDIR /app -# Install build dependencies -# Clear stale APT lists first — Kaniko's layer extraction can leave -# base-image metadata with expired GPG signatures (bookworm InRelease). -RUN rm -rf /var/lib/apt/lists/* \ - && apt-get update \ - && apt-get install -y --no-install-recommends build-essential \ - && rm -rf /var/lib/apt/lists/* - # Copy dependency files and private registry config COPY pyproject.toml . COPY pip.conf /etc/pip.conf diff --git a/apps/orchestrator/Dockerfile b/apps/orchestrator/Dockerfile index 29ade11..a22a5e7 100644 --- a/apps/orchestrator/Dockerfile +++ b/apps/orchestrator/Dockerfile @@ -72,13 +72,10 @@ LABEL org.opencontainers.image.description="Agent orchestration service for Mosa # Remove npm (unused in production — we use pnpm) to reduce attack surface RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx -# Install wget and dumb-init -# Clear stale APT lists first — Kaniko's layer extraction can leave -# base-image metadata with expired GPG signatures (bookworm InRelease). -RUN rm -rf /var/lib/apt/lists/* \ - && apt-get update \ - && apt-get install -y --no-install-recommends wget dumb-init \ - && rm -rf /var/lib/apt/lists/* +# Install dumb-init for proper signal handling (static binary from GitHub, +# avoids apt-get which fails under Kaniko with bookworm GPG signature errors) +ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64 /usr/local/bin/dumb-init +RUN chmod 755 /usr/local/bin/dumb-init # Create non-root user RUN groupadd -g 1001 nodejs && useradd -m -u 1001 -g nodejs nestjs @@ -109,7 +106,7 @@ EXPOSE 3001 # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1 + CMD node -e "require('http').get('http://localhost:3001/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})" # Use dumb-init to handle signals properly ENTRYPOINT ["dumb-init", "--"] diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile index 6fea2dc..2e3f822 100644 --- a/apps/web/Dockerfile +++ b/apps/web/Dockerfile @@ -85,13 +85,10 @@ RUN rm -rf /usr/local/lib/node_modules/npm /usr/local/bin/npm /usr/local/bin/npx # Install pnpm (needed for pnpm start command) RUN corepack enable && corepack prepare pnpm@10.27.0 --activate -# Install dumb-init for proper signal handling -# Clear stale APT lists first — Kaniko's layer extraction can leave -# base-image metadata with expired GPG signatures (bookworm InRelease). -RUN rm -rf /var/lib/apt/lists/* \ - && apt-get update \ - && apt-get install -y --no-install-recommends dumb-init \ - && rm -rf /var/lib/apt/lists/* +# Install dumb-init for proper signal handling (static binary from GitHub, +# avoids apt-get which fails under Kaniko with bookworm GPG signature errors) +ADD https://github.com/Yelp/dumb-init/releases/download/v1.2.5/dumb-init_1.2.5_x86_64 /usr/local/bin/dumb-init +RUN chmod 755 /usr/local/bin/dumb-init # Create non-root user RUN groupadd -g 1001 nodejs && useradd -m -u 1001 -g nodejs nextjs diff --git a/docker-compose.swarm.portainer.yml b/docker-compose.swarm.portainer.yml index a544963..7079702 100644 --- a/docker-compose.swarm.portainer.yml +++ b/docker-compose.swarm.portainer.yml @@ -1,48 +1,63 @@ # ============================================== -# Mosaic Stack - Docker Swarm Deployment +# Mosaic Stack — Docker Swarm / Portainer # ============================================== # -# IMPORTANT: Docker Swarm does NOT support docker-compose profiles -# To disable services (e.g., for external alternatives), manually comment them out +# The canonical deployment file for Mosaic Stack on Docker Swarm. +# Includes all services except OpenBao (standalone) and external services. # -# Current Configuration: -# - PostgreSQL: ENABLED (internal) -# - Valkey: ENABLED (internal) -# - Coordinator: ENABLED (internal) -# - OpenBao: DISABLED (must use standalone - see docker-compose.openbao.yml) -# - Authentik: DISABLED (commented out - using external OIDC) -# - Ollama: DISABLED (commented out - using external Ollama) +# External services (not in this file): +# - OpenBao: Standalone container (see docker-compose.openbao.yml) +# - Authentik: External OIDC provider +# - Ollama: External AI inference # -# For detailed deployment instructions, see: -# docs/SWARM-DEPLOYMENT.md +# Usage (Portainer): +# 1. Stacks -> Add Stack -> Upload or paste +# 2. Set environment variables (see .env.example for full reference) +# 3. Deploy # -# Quick Start: -# 1. cp .env.swarm.example .env -# 2. nano .env # Configure environment -# 3. ./scripts/deploy-swarm.sh mosaic -# 4. Initialize OpenBao manually (see docs/SWARM-DEPLOYMENT.md) +# Usage (CLI): +# docker stack deploy -c docker-compose.swarm.portainer.yml mosaic +# +# Host paths required for Matrix: +# /opt/mosaic/synapse/homeserver.yaml +# /opt/mosaic/synapse/element-config.json +# /opt/mosaic/synapse/media_store/ (auto-populated) +# /opt/mosaic/synapse/keys/ (auto-populated) +# +# ============================================== +# ENVIRONMENT VARIABLE CONVENTION +# ============================================== +# +# ${VAR} — REQUIRED. Must be set in Portainer env vars. +# ${VAR:-default} — OPTIONAL. Falls back to a sensible default. +# ${VAR:-} — OPTIONAL. Empty string is acceptable. +# +# NOTE: Portainer does not support ${VAR:?msg} syntax. +# Required vars use plain ${VAR} — the app validates at startup. # # ============================================== services: + # ============================================ + # CORE INFRASTRUCTURE + # ============================================ + # ====================== # PostgreSQL Database # ====================== postgres: image: git.mosaicstack.dev/mosaic/stack-postgres:${IMAGE_TAG:-latest} environment: - POSTGRES_USER: ${POSTGRES_USER:-mosaic} - POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mosaic_dev_password} - POSTGRES_DB: ${POSTGRES_DB:-mosaic} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} POSTGRES_SHARED_BUFFERS: ${POSTGRES_SHARED_BUFFERS:-256MB} POSTGRES_EFFECTIVE_CACHE_SIZE: ${POSTGRES_EFFECTIVE_CACHE_SIZE:-1GB} POSTGRES_MAX_CONNECTIONS: ${POSTGRES_MAX_CONNECTIONS:-100} volumes: - postgres_data:/var/lib/postgresql/data - # Note: init-scripts bind mount removed for Portainer compatibility - # Init scripts are baked into the postgres image at build time healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mosaic} -d ${POSTGRES_DB:-mosaic}"] + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 @@ -77,168 +92,109 @@ services: restart_policy: condition: on-failure - # ====================== - # OpenBao Secrets Vault - COMMENTED OUT - # ====================== - # IMPORTANT: OpenBao CANNOT run in swarm mode due to port binding conflicts. - # Deploy OpenBao as a standalone container instead: - # docker compose -f docker-compose.openbao.yml up -d - # - # Alternative: Use external HashiCorp Vault or managed secrets service - # - # openbao: - # image: git.mosaicstack.dev/mosaic/stack-openbao:${IMAGE_TAG:-latest} - # environment: - # OPENBAO_ADDR: ${OPENBAO_ADDR:-http://0.0.0.0:8200} - # OPENBAO_DEV_ROOT_TOKEN_ID: ${OPENBAO_DEV_ROOT_TOKEN_ID:-root} - # volumes: - # - openbao_data:/openbao/data - # - openbao_logs:/openbao/logs - # - openbao_init:/openbao/init - # cap_add: - # - IPC_LOCK - # healthcheck: - # test: - # ["CMD", "wget", "--spider", "--quiet", "http://localhost:8200/v1/sys/health?standbyok=true"] - # interval: 10s - # timeout: 5s - # retries: 5 - # start_period: 30s - # networks: - # - internal - # deploy: - # restart_policy: - # condition: on-failure + # ============================================ + # MOSAIC APPLICATION + # ============================================ # ====================== - # Authentik - COMMENTED OUT (Using External Authentik) + # Mosaic API # ====================== - # Uncomment these services if you want to run Authentik internally - # For external Authentik, configure OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET in .env - # - # authentik-postgres: - # image: postgres:17.7-alpine3.22 - # environment: - # POSTGRES_USER: ${AUTHENTIK_POSTGRES_USER:-authentik} - # POSTGRES_PASSWORD: ${AUTHENTIK_POSTGRES_PASSWORD:-authentik_password} - # POSTGRES_DB: ${AUTHENTIK_POSTGRES_DB:-authentik} - # volumes: - # - authentik_postgres_data:/var/lib/postgresql/data - # healthcheck: - # test: ["CMD-SHELL", "pg_isready -U ${AUTHENTIK_POSTGRES_USER:-authentik}"] - # interval: 10s - # timeout: 5s - # retries: 5 - # start_period: 20s - # networks: - # - internal - # deploy: - # restart_policy: - # condition: on-failure - # - # authentik-redis: - # image: valkey/valkey:8-alpine - # command: valkey-server --save 60 1 --loglevel warning - # volumes: - # - authentik_redis_data:/data - # healthcheck: - # test: ["CMD", "valkey-cli", "ping"] - # interval: 10s - # timeout: 5s - # retries: 5 - # start_period: 10s - # networks: - # - internal - # deploy: - # restart_policy: - # condition: on-failure - # - # authentik-server: - # image: ghcr.io/goauthentik/server:2024.12.1 - # command: server - # environment: - # AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:-change-this-to-a-random-secret} - # AUTHENTIK_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING:-false} - # AUTHENTIK_POSTGRESQL__HOST: authentik-postgres - # AUTHENTIK_POSTGRESQL__PORT: 5432 - # AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_POSTGRES_DB:-authentik} - # AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_POSTGRES_USER:-authentik} - # AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_POSTGRES_PASSWORD:-authentik_password} - # AUTHENTIK_REDIS__HOST: authentik-redis - # AUTHENTIK_REDIS__PORT: 6379 - # AUTHENTIK_BOOTSTRAP_PASSWORD: ${AUTHENTIK_BOOTSTRAP_PASSWORD:-admin} - # AUTHENTIK_BOOTSTRAP_EMAIL: ${AUTHENTIK_BOOTSTRAP_EMAIL:-admin@localhost} - # AUTHENTIK_COOKIE_DOMAIN: ${AUTHENTIK_COOKIE_DOMAIN:-.mosaicstack.dev} - # volumes: - # - authentik_media:/media - # - authentik_templates:/templates - # healthcheck: - # test: - # [ - # "CMD", - # "wget", - # "--no-verbose", - # "--tries=1", - # "--spider", - # "http://localhost:9000/-/health/live/", - # ] - # interval: 30s - # timeout: 10s - # retries: 3 - # start_period: 90s - # networks: - # - internal - # - traefik-public - # deploy: - # restart_policy: - # condition: on-failure - # labels: - # - "traefik.enable=true" - # - "traefik.http.routers.mosaic-auth.rule=Host(`${MOSAIC_AUTH_DOMAIN:-auth.mosaicstack.dev}`)" - # - "traefik.http.routers.mosaic-auth.entrypoints=web" - # - "traefik.http.services.mosaic-auth.loadbalancer.server.port=9000" - # - # authentik-worker: - # image: ghcr.io/goauthentik/server:2024.12.1 - # command: worker - # environment: - # AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:-change-this-to-a-random-secret} - # AUTHENTIK_ERROR_REPORTING__ENABLED: ${AUTHENTIK_ERROR_REPORTING:-false} - # AUTHENTIK_POSTGRESQL__HOST: authentik-postgres - # AUTHENTIK_POSTGRESQL__PORT: 5432 - # AUTHENTIK_POSTGRESQL__NAME: ${AUTHENTIK_POSTGRES_DB:-authentik} - # AUTHENTIK_POSTGRESQL__USER: ${AUTHENTIK_POSTGRES_USER:-authentik} - # AUTHENTIK_POSTGRESQL__PASSWORD: ${AUTHENTIK_POSTGRES_PASSWORD:-authentik_password} - # AUTHENTIK_REDIS__HOST: authentik-redis - # AUTHENTIK_REDIS__PORT: 6379 - # volumes: - # - authentik_media:/media - # - authentik_certs:/certs - # - authentik_templates:/templates - # networks: - # - internal - # deploy: - # restart_policy: - # condition: on-failure + api: + image: git.mosaicstack.dev/mosaic/stack-api:${IMAGE_TAG:-latest} + environment: + NODE_ENV: production + PORT: ${API_PORT:-3001} + API_HOST: ${API_HOST:-0.0.0.0} + DATABASE_URL: postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + VALKEY_URL: redis://valkey:6379 + # Auth (external Authentik) + OIDC_ENABLED: ${OIDC_ENABLED:-false} + OIDC_ISSUER: ${OIDC_ISSUER} + OIDC_CLIENT_ID: ${OIDC_CLIENT_ID} + OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET} + OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI:-} + JWT_SECRET: ${JWT_SECRET:-change-this-to-a-random-secret} + JWT_EXPIRATION: ${JWT_EXPIRATION:-24h} + BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} + CSRF_SECRET: ${CSRF_SECRET} + # External services + OLLAMA_ENDPOINT: ${OLLAMA_ENDPOINT} + OPENBAO_ADDR: ${OPENBAO_ADDR} + ENCRYPTION_KEY: ${ENCRYPTION_KEY} + # Matrix bridge (optional — configure after Synapse is running) + MATRIX_HOMESERVER_URL: ${MATRIX_HOMESERVER_URL:-http://synapse:8008} + MATRIX_ACCESS_TOKEN: ${MATRIX_ACCESS_TOKEN:-} + MATRIX_BOT_USER_ID: ${MATRIX_BOT_USER_ID:-} + MATRIX_CONTROL_ROOM_ID: ${MATRIX_CONTROL_ROOM_ID:-} + MATRIX_WORKSPACE_ID: ${MATRIX_WORKSPACE_ID:-} + MATRIX_SERVER_NAME: ${MATRIX_SERVER_NAME:-} + # Speech + SPEECH_MAX_UPLOAD_SIZE: ${SPEECH_MAX_UPLOAD_SIZE:-25000000} + SPEECH_MAX_DURATION_SECONDS: ${SPEECH_MAX_DURATION_SECONDS:-600} + SPEECH_MAX_TEXT_LENGTH: ${SPEECH_MAX_TEXT_LENGTH:-4096} + # Telemetry (disabled by default) + MOSAIC_TELEMETRY_ENABLED: ${MOSAIC_TELEMETRY_ENABLED:-false} + MOSAIC_TELEMETRY_SERVER_URL: ${MOSAIC_TELEMETRY_SERVER_URL:-} + MOSAIC_TELEMETRY_API_KEY: ${MOSAIC_TELEMETRY_API_KEY:-} + MOSAIC_TELEMETRY_INSTANCE_ID: ${MOSAIC_TELEMETRY_INSTANCE_ID:-} + MOSAIC_TELEMETRY_DRY_RUN: ${MOSAIC_TELEMETRY_DRY_RUN:-false} + healthcheck: + test: + [ + "CMD-SHELL", + 'node -e "require(''http'').get(''http://localhost:${API_PORT:-3001}/health'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"', + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - internal + - traefik-public + deploy: + restart_policy: + condition: on-failure + labels: + - "traefik.enable=true" + - "traefik.http.routers.mosaic-api.rule=Host(`${MOSAIC_API_DOMAIN}`)" + - "traefik.http.routers.mosaic-api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.mosaic-api.tls=${TRAEFIK_TLS_ENABLED:-true}" + - "traefik.http.routers.mosaic-api.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}" + - "traefik.http.services.mosaic-api.loadbalancer.server.port=${API_PORT:-3001}" + - "traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-traefik-public}" # ====================== - # Ollama (Optional AI Service) + # Mosaic Web # ====================== - # ollama: - # image: ollama/ollama:latest - # volumes: - # - ollama_data:/root/.ollama - # healthcheck: - # test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"] - # interval: 30s - # timeout: 10s - # retries: 3 - # start_period: 60s - # networks: - # - internal - # deploy: - # restart_policy: - # condition: on-failure + web: + image: git.mosaicstack.dev/mosaic/stack-web:${IMAGE_TAG:-latest} + environment: + NODE_ENV: production + PORT: ${WEB_PORT:-3000} + NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} + healthcheck: + test: + [ + "CMD-SHELL", + 'node -e "require(''http'').get(''http://localhost:${WEB_PORT:-3000}'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"', + ] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + networks: + - traefik-public + deploy: + restart_policy: + condition: on-failure + labels: + - "traefik.enable=true" + - "traefik.http.routers.mosaic-web.rule=Host(`${MOSAIC_WEB_DOMAIN}`)" + - "traefik.http.routers.mosaic-web.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.mosaic-web.tls=${TRAEFIK_TLS_ENABLED:-true}" + - "traefik.http.routers.mosaic-web.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}" + - "traefik.http.services.mosaic-web.loadbalancer.server.port=${WEB_PORT:-3000}" + - "traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-traefik-public}" # ====================== # Mosaic Coordinator @@ -247,7 +203,7 @@ services: image: git.mosaicstack.dev/mosaic/stack-coordinator:${IMAGE_TAG:-latest} environment: GITEA_WEBHOOK_SECRET: ${GITEA_WEBHOOK_SECRET} - GITEA_URL: ${GITEA_URL:-https://git.mosaicstack.dev} + GITEA_URL: ${GITEA_URL} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} LOG_LEVEL: ${LOG_LEVEL:-info} HOST: 0.0.0.0 @@ -255,9 +211,9 @@ services: COORDINATOR_POLL_INTERVAL: ${COORDINATOR_POLL_INTERVAL:-5.0} COORDINATOR_MAX_CONCURRENT_AGENTS: ${COORDINATOR_MAX_CONCURRENT_AGENTS:-10} COORDINATOR_ENABLED: ${COORDINATOR_ENABLED:-true} - # Telemetry (task completion tracking & predictions) + # Telemetry (disabled by default) MOSAIC_TELEMETRY_ENABLED: ${MOSAIC_TELEMETRY_ENABLED:-false} - MOSAIC_TELEMETRY_SERVER_URL: ${MOSAIC_TELEMETRY_SERVER_URL:-https://tel-api.mosaicstack.dev} + MOSAIC_TELEMETRY_SERVER_URL: ${MOSAIC_TELEMETRY_SERVER_URL:-} MOSAIC_TELEMETRY_API_KEY: ${MOSAIC_TELEMETRY_API_KEY:-} MOSAIC_TELEMETRY_INSTANCE_ID: ${MOSAIC_TELEMETRY_INSTANCE_ID:-} MOSAIC_TELEMETRY_DRY_RUN: ${MOSAIC_TELEMETRY_DRY_RUN:-false} @@ -279,56 +235,6 @@ services: restart_policy: condition: on-failure - # ====================== - # Mosaic API - # ====================== - api: - image: git.mosaicstack.dev/mosaic/stack-api:${IMAGE_TAG:-latest} - environment: - NODE_ENV: production - PORT: ${API_PORT:-3001} - API_HOST: ${API_HOST:-0.0.0.0} - DATABASE_URL: postgresql://${POSTGRES_USER:-mosaic}:${POSTGRES_PASSWORD:-mosaic_dev_password}@postgres:5432/${POSTGRES_DB:-mosaic} - VALKEY_URL: redis://valkey:6379 - OIDC_ISSUER: ${OIDC_ISSUER} - OIDC_CLIENT_ID: ${OIDC_CLIENT_ID} - OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET} - OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI:-} - JWT_SECRET: ${JWT_SECRET:-change-this-to-a-random-secret} - JWT_EXPIRATION: ${JWT_EXPIRATION:-24h} - BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} - CSRF_SECRET: ${CSRF_SECRET} - OLLAMA_ENDPOINT: ${OLLAMA_ENDPOINT:-http://ollama:11434} - OPENBAO_ADDR: ${OPENBAO_ADDR:-http://openbao:8200} - ENCRYPTION_KEY: ${ENCRYPTION_KEY} - # Telemetry (task completion tracking & predictions) - MOSAIC_TELEMETRY_ENABLED: ${MOSAIC_TELEMETRY_ENABLED:-false} - MOSAIC_TELEMETRY_SERVER_URL: ${MOSAIC_TELEMETRY_SERVER_URL:-https://tel-api.mosaicstack.dev} - MOSAIC_TELEMETRY_API_KEY: ${MOSAIC_TELEMETRY_API_KEY:-} - MOSAIC_TELEMETRY_INSTANCE_ID: ${MOSAIC_TELEMETRY_INSTANCE_ID:-} - MOSAIC_TELEMETRY_DRY_RUN: ${MOSAIC_TELEMETRY_DRY_RUN:-false} - healthcheck: - test: - [ - "CMD-SHELL", - 'node -e "require(''http'').get(''http://localhost:${API_PORT:-3001}/health'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"', - ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s - networks: - - internal - - traefik-public - deploy: - restart_policy: - condition: on-failure - labels: - - "traefik.enable=true" - - "traefik.http.routers.mosaic-api.rule=Host(`${MOSAIC_API_DOMAIN:-api.mosaicstack.dev}`)" - - "traefik.http.routers.mosaic-api.entrypoints=web" - - "traefik.http.services.mosaic-api.loadbalancer.server.port=${API_PORT:-3001}" - # ====================== # Mosaic Orchestrator # ====================== @@ -350,15 +256,16 @@ services: - orchestrator_workspace:/workspace healthcheck: test: - ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1"] + [ + "CMD-SHELL", + 'node -e "require(''http'').get(''http://localhost:3001/health'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"', + ] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - internal - # Note: security_opt not supported in swarm mode - # Security hardening done via cap_drop/cap_add cap_drop: - ALL cap_add: @@ -369,35 +276,152 @@ services: restart_policy: condition: on-failure + # ============================================ + # MATRIX (Synapse + Element Web) + # ============================================ + # ====================== - # Mosaic Web + # Synapse Database Init # ====================== - web: - image: git.mosaicstack.dev/mosaic/stack-web:${IMAGE_TAG:-latest} + # Creates the 'synapse' database in the shared PostgreSQL instance. + # Runs once and exits. Idempotent — safe to run on every deploy. + synapse-db-init: + image: postgres:17-alpine environment: - NODE_ENV: production - PORT: ${WEB_PORT:-3000} - NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:3001} - healthcheck: - test: - [ - "CMD-SHELL", - 'node -e "require(''http'').get(''http://localhost:${WEB_PORT:-3000}'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"', - ] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s + PGHOST: postgres + PGPORT: 5432 + PGUSER: ${POSTGRES_USER} + PGPASSWORD: ${POSTGRES_PASSWORD} + SYNAPSE_DB: ${SYNAPSE_POSTGRES_DB} + SYNAPSE_USER: ${SYNAPSE_POSTGRES_USER} + SYNAPSE_PASSWORD: ${SYNAPSE_POSTGRES_PASSWORD} + entrypoint: ["sh", "-c"] + command: + - | + until pg_isready -h postgres -p 5432 -U $${PGUSER}; do + echo "Waiting for PostgreSQL..." + sleep 2 + done + echo "PostgreSQL is ready. Creating Synapse database and user..." + + psql -h postgres -U $${PGUSER} -tc "SELECT 1 FROM pg_roles WHERE rolname='$${SYNAPSE_USER}'" | grep -q 1 || \ + psql -h postgres -U $${PGUSER} -c "CREATE USER $${SYNAPSE_USER} WITH PASSWORD '$${SYNAPSE_PASSWORD}';" + + psql -h postgres -U $${PGUSER} -tc "SELECT 1 FROM pg_database WHERE datname='$${SYNAPSE_DB}'" | grep -q 1 || \ + psql -h postgres -U $${PGUSER} -c "CREATE DATABASE $${SYNAPSE_DB} OWNER $${SYNAPSE_USER} ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' TEMPLATE template0;" + + echo "Synapse database ready: $${SYNAPSE_DB}" networks: + - internal + deploy: + restart_policy: + condition: on-failure + delay: 5s + max_attempts: 5 + + # ====================== + # Synapse (Matrix Homeserver) + # ====================== + synapse: + image: matrixdotorg/synapse:${SYNAPSE_IMAGE_TAG:-latest} + environment: + SYNAPSE_CONFIG_DIR: /data + SYNAPSE_CONFIG_PATH: /data/homeserver.yaml + volumes: + - /opt/mosaic/synapse:/data + healthcheck: + test: ["CMD-SHELL", "curl -fSs http://localhost:8008/health || exit 1"] + interval: 15s + timeout: 5s + retries: 5 + start_period: 30s + networks: + - internal - traefik-public deploy: restart_policy: condition: on-failure + delay: 10s + max_attempts: 10 labels: - "traefik.enable=true" - - "traefik.http.routers.mosaic-web.rule=Host(`${MOSAIC_WEB_DOMAIN:-mosaic.mosaicstack.dev}`)" - - "traefik.http.routers.mosaic-web.entrypoints=web" - - "traefik.http.services.mosaic-web.loadbalancer.server.port=${WEB_PORT:-3000}" + - "traefik.http.routers.matrix.rule=Host(`${MATRIX_DOMAIN}`)" + - "traefik.http.routers.matrix.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.matrix.tls=${TRAEFIK_TLS_ENABLED:-true}" + - "traefik.http.routers.matrix.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}" + - "traefik.http.services.matrix.loadbalancer.server.port=8008" + - "traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-traefik-public}" + + # ====================== + # Element Web (Matrix Client) + # ====================== + element-web: + image: vectorim/element-web:${ELEMENT_IMAGE_TAG:-latest} + volumes: + - /opt/mosaic/synapse/element-config.json:/app/config.json:ro + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:80 || exit 1"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - internal + - traefik-public + deploy: + restart_policy: + condition: on-failure + delay: 5s + labels: + - "traefik.enable=true" + - "traefik.http.routers.element.rule=Host(`${ELEMENT_DOMAIN}`)" + - "traefik.http.routers.element.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" + - "traefik.http.routers.element.tls=${TRAEFIK_TLS_ENABLED:-true}" + - "traefik.http.routers.element.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}" + - "traefik.http.services.element.loadbalancer.server.port=80" + - "traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-traefik-public}" + + # ============================================ + # SPEECH SERVICES + # ============================================ + + # ====================== + # Speaches (STT + basic TTS) + # ====================== + speaches: + image: ghcr.io/speaches-ai/speaches:latest-cpu + environment: + WHISPER__MODEL: ${SPEACHES_WHISPER_MODEL:-Systran/faster-whisper-large-v3-turbo} + volumes: + - speaches_models:/root/.cache/huggingface + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8000/health || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 120s + networks: + - internal + deploy: + restart_policy: + condition: on-failure + + # ====================== + # Kokoro TTS + # ====================== + kokoro-tts: + image: ghcr.io/remsky/kokoro-fastapi-cpu:latest + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:8880/health || exit 1"] + interval: 30s + timeout: 10s + retries: 5 + start_period: 120s + networks: + - internal + deploy: + restart_policy: + condition: on-failure # ====================== # Volumes @@ -405,19 +429,8 @@ services: volumes: postgres_data: valkey_data: - # OpenBao volumes - commented out (using standalone deployment) - # openbao_data: - # openbao_logs: - # openbao_init: - # Authentik volumes - commented out (using external Authentik) - # authentik_postgres_data: - # authentik_redis_data: - # authentik_media: - # authentik_certs: - # authentik_templates: - # Ollama volume - commented out (using external Ollama) - # ollama_data: orchestrator_workspace: + speaches_models: # ====================== # Networks diff --git a/docker-compose.yml b/docker-compose.yml index 6858318..0661a85 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -456,7 +456,10 @@ services: condition: service_healthy healthcheck: test: - ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1"] + [ + "CMD-SHELL", + 'node -e "require(''http'').get(''http://localhost:3001/health'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"', + ] interval: 30s timeout: 10s retries: 3 diff --git a/docker/docker-compose.build.yml b/docker/docker-compose.build.yml index 66a5e00..5e455d2 100644 --- a/docker/docker-compose.build.yml +++ b/docker/docker-compose.build.yml @@ -465,7 +465,10 @@ services: condition: service_healthy healthcheck: test: - ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1"] + [ + "CMD-SHELL", + 'node -e "require(''http'').get(''http://localhost:3001/health'', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"', + ] interval: 30s timeout: 10s retries: 3