services: # ====================== # PostgreSQL Database # ====================== postgres: build: context: ./docker/postgres dockerfile: Dockerfile container_name: mosaic-postgres restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-mosaic} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mosaic_dev_password} POSTGRES_DB: ${POSTGRES_DB:-mosaic} # Performance tuning POSTGRES_SHARED_BUFFERS: ${POSTGRES_SHARED_BUFFERS:-256MB} POSTGRES_EFFECTIVE_CACHE_SIZE: ${POSTGRES_EFFECTIVE_CACHE_SIZE:-1GB} POSTGRES_MAX_CONNECTIONS: ${POSTGRES_MAX_CONNECTIONS:-100} ports: - "${POSTGRES_PORT:-5432}:5432" volumes: - postgres_data:/var/lib/postgresql/data - ./docker/postgres/init-scripts:/docker-entrypoint-initdb.d:ro healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-mosaic} -d ${POSTGRES_DB:-mosaic}"] interval: 10s timeout: 5s retries: 5 start_period: 30s networks: - mosaic-internal profiles: - database - full labels: - "com.mosaic.service=database" - "com.mosaic.description=PostgreSQL 17 with pgvector" # ====================== # Valkey Cache # ====================== valkey: image: valkey/valkey:8-alpine container_name: mosaic-valkey restart: unless-stopped command: - valkey-server - --maxmemory ${VALKEY_MAXMEMORY:-256mb} - --maxmemory-policy allkeys-lru - --appendonly yes ports: - "${VALKEY_PORT:-6379}:6379" volumes: - valkey_data:/data healthcheck: test: ["CMD", "valkey-cli", "ping"] interval: 10s timeout: 5s retries: 5 start_period: 10s networks: - mosaic-internal profiles: - cache - full labels: - "com.mosaic.service=cache" - "com.mosaic.description=Valkey Redis-compatible cache" # ====================== # Authentik PostgreSQL # ====================== authentik-postgres: image: postgres:17.7-alpine3.22 container_name: mosaic-authentik-postgres restart: unless-stopped 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: - mosaic-internal profiles: - authentik - full labels: - "com.mosaic.service=auth-database" - "com.mosaic.description=Authentik PostgreSQL database" # ====================== # Authentik Redis # ====================== authentik-redis: image: valkey/valkey:8-alpine container_name: mosaic-authentik-redis restart: unless-stopped 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: - mosaic-internal profiles: - authentik - full labels: - "com.mosaic.service=auth-cache" - "com.mosaic.description=Authentik Redis cache" # ====================== # Authentik Server # ====================== authentik-server: image: ghcr.io/goauthentik/server:2024.12.1 container_name: mosaic-authentik-server restart: unless-stopped 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:-.localhost} ports: - "${AUTHENTIK_PORT_HTTP:-9000}:9000" - "${AUTHENTIK_PORT_HTTPS:-9443}:9443" volumes: - authentik_media:/media - authentik_templates:/templates depends_on: authentik-postgres: condition: service_healthy authentik-redis: condition: service_healthy healthcheck: test: [ "CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9000/-/health/live/", ] interval: 30s timeout: 10s retries: 3 start_period: 90s networks: - mosaic-internal - mosaic-public profiles: - authentik - full labels: - "com.mosaic.service=auth-server" - "com.mosaic.description=Authentik OIDC server" # Traefik labels (activated when TRAEFIK_MODE=bundled or upstream) - "traefik.enable=${TRAEFIK_ENABLE:-false}" - "traefik.http.routers.mosaic-auth.rule=Host(`${MOSAIC_AUTH_DOMAIN:-auth.mosaic.local}`)" - "traefik.http.routers.mosaic-auth.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" - "traefik.http.routers.mosaic-auth.tls=${TRAEFIK_TLS_ENABLED:-true}" - "traefik.http.services.mosaic-auth.loadbalancer.server.port=9000" - "traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-mosaic-public}" # Let's Encrypt (if enabled) - "traefik.http.routers.mosaic-auth.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}" # ====================== # Authentik Worker # ====================== authentik-worker: image: ghcr.io/goauthentik/server:2024.12.1 container_name: mosaic-authentik-worker restart: unless-stopped 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 depends_on: authentik-postgres: condition: service_healthy authentik-redis: condition: service_healthy networks: - mosaic-internal profiles: - authentik - full labels: - "com.mosaic.service=auth-worker" - "com.mosaic.description=Authentik background worker" # ====================== # Ollama (Optional AI Service) # ====================== ollama: image: ollama/ollama:latest container_name: mosaic-ollama restart: unless-stopped ports: - "${OLLAMA_PORT:-11434}:11434" 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: - mosaic-internal profiles: - ollama - full labels: - "com.mosaic.service=ai" - "com.mosaic.description=Ollama LLM service" # Uncomment if you have GPU support # deploy: # resources: # reservations: # devices: # - driver: nvidia # count: 1 # capabilities: [gpu] # ====================== # OpenBao Secrets Management (Optional) # ====================== openbao: build: context: ./docker/openbao dockerfile: Dockerfile container_name: mosaic-openbao restart: unless-stopped user: root ports: - "127.0.0.1:${OPENBAO_PORT:-8200}:8200" volumes: - openbao_data:/openbao/data - openbao_init:/openbao/init environment: VAULT_ADDR: http://0.0.0.0:8200 SKIP_SETCAP: "true" command: ["bao", "server", "-config=/openbao/config/config.hcl"] cap_add: - IPC_LOCK healthcheck: test: ["CMD-SHELL", "nc -z 127.0.0.1 8200 || exit 1"] interval: 10s timeout: 5s retries: 5 start_period: 10s networks: - mosaic-internal profiles: - openbao - full labels: - "com.mosaic.service=secrets" - "com.mosaic.description=OpenBao secrets management" openbao-init: build: context: ./docker/openbao dockerfile: Dockerfile container_name: mosaic-openbao-init restart: unless-stopped user: root volumes: - openbao_init:/openbao/init environment: VAULT_ADDR: http://openbao:8200 command: ["/openbao/init.sh"] depends_on: openbao: condition: service_healthy networks: - mosaic-internal profiles: - openbao - full labels: - "com.mosaic.service=secrets-init" - "com.mosaic.description=OpenBao auto-initialization sidecar" # ====================== # Traefik Reverse Proxy (Optional - Bundled Mode) # ====================== # Enable with: COMPOSE_PROFILES=traefik-bundled or --profile traefik-bundled # Set TRAEFIK_MODE=bundled in .env traefik: image: traefik:v3.2 container_name: mosaic-traefik restart: unless-stopped command: - "--configFile=/etc/traefik/traefik.yml" ports: - "${TRAEFIK_HTTP_PORT:-80}:80" - "${TRAEFIK_HTTPS_PORT:-443}:443" - "${TRAEFIK_DASHBOARD_PORT:-8080}:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - ./docker/traefik/traefik.yml:/etc/traefik/traefik.yml:ro - ./docker/traefik/dynamic:/etc/traefik/dynamic:ro - traefik_letsencrypt:/letsencrypt environment: - TRAEFIK_ACME_EMAIL=${TRAEFIK_ACME_EMAIL:-} networks: - mosaic-public profiles: - traefik-bundled - full labels: - "com.mosaic.service=reverse-proxy" - "com.mosaic.description=Traefik reverse proxy and load balancer" healthcheck: test: ["CMD", "traefik", "healthcheck", "--ping"] interval: 30s timeout: 10s retries: 3 start_period: 20s # ====================== # Mosaic API # ====================== api: build: context: . dockerfile: ./apps/api/Dockerfile args: - NODE_ENV=production container_name: mosaic-api restart: unless-stopped environment: NODE_ENV: production # API Configuration - PORT is what NestJS reads PORT: ${API_PORT:-3001} API_HOST: ${API_HOST:-0.0.0.0} # Database DATABASE_URL: postgresql://${POSTGRES_USER:-mosaic}:${POSTGRES_PASSWORD:-mosaic_dev_password}@postgres:5432/${POSTGRES_DB:-mosaic} # Cache VALKEY_URL: redis://valkey:6379 # Authentication OIDC_ISSUER: ${OIDC_ISSUER} OIDC_CLIENT_ID: ${OIDC_CLIENT_ID} OIDC_CLIENT_SECRET: ${OIDC_CLIENT_SECRET} OIDC_REDIRECT_URI: ${OIDC_REDIRECT_URI:-http://localhost:3001/auth/callback} # JWT JWT_SECRET: ${JWT_SECRET:-change-this-to-a-random-secret} JWT_EXPIRATION: ${JWT_EXPIRATION:-24h} # Ollama (optional) OLLAMA_ENDPOINT: ${OLLAMA_ENDPOINT:-http://ollama:11434} # OpenBao (optional) OPENBAO_ADDR: ${OPENBAO_ADDR:-http://openbao:8200} volumes: - openbao_init:/openbao/init:ro ports: - "${API_PORT:-3001}:${API_PORT:-3001}" depends_on: postgres: condition: service_healthy valkey: condition: service_healthy 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: - mosaic-internal - mosaic-public labels: - "com.mosaic.service=api" - "com.mosaic.description=Mosaic NestJS API" # Traefik labels (activated when TRAEFIK_MODE=bundled or upstream) - "traefik.enable=${TRAEFIK_ENABLE:-false}" - "traefik.http.routers.mosaic-api.rule=Host(`${MOSAIC_API_DOMAIN:-api.mosaic.local}`)" - "traefik.http.routers.mosaic-api.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" - "traefik.http.routers.mosaic-api.tls=${TRAEFIK_TLS_ENABLED:-true}" - "traefik.http.services.mosaic-api.loadbalancer.server.port=${API_PORT:-3001}" - "traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-mosaic-public}" # Let's Encrypt (if enabled) - "traefik.http.routers.mosaic-api.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}" # ====================== # Mosaic Orchestrator # ====================== orchestrator: build: context: . dockerfile: ./apps/orchestrator/Dockerfile container_name: mosaic-orchestrator restart: unless-stopped # Run as non-root user (node:node, UID 1000) user: "1000:1000" environment: NODE_ENV: production # Orchestrator Configuration ORCHESTRATOR_PORT: 3001 # Valkey VALKEY_URL: redis://valkey:6379 # Claude API CLAUDE_API_KEY: ${CLAUDE_API_KEY} # Docker DOCKER_SOCKET: /var/run/docker.sock # Git GIT_USER_NAME: "Mosaic Orchestrator" GIT_USER_EMAIL: "orchestrator@mosaicstack.dev" # Security KILLSWITCH_ENABLED: true SANDBOX_ENABLED: true ports: - "3002:3001" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - orchestrator_workspace:/workspace depends_on: valkey: condition: service_healthy api: condition: service_healthy healthcheck: test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3001/health || exit 1"] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - mosaic-internal # Security hardening security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE read_only: false # Cannot be read-only due to workspace writes tmpfs: - /tmp:noexec,nosuid,size=100m labels: - "com.mosaic.service=orchestrator" - "com.mosaic.description=Mosaic Agent Orchestrator" - "com.mosaic.security=hardened" - "com.mosaic.security.non-root=true" - "com.mosaic.security.capabilities=minimal" # ====================== # Mosaic Web # ====================== web: build: context: . dockerfile: ./apps/web/Dockerfile args: - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:3001} container_name: mosaic-web restart: unless-stopped environment: NODE_ENV: production PORT: ${WEB_PORT:-3000} NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-http://localhost:3001} ports: - "${WEB_PORT:-3000}:${WEB_PORT:-3000}" depends_on: api: condition: service_healthy 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: - mosaic-public labels: - "com.mosaic.service=web" - "com.mosaic.description=Mosaic Next.js Web App" # Traefik labels (activated when TRAEFIK_MODE=bundled or upstream) - "traefik.enable=${TRAEFIK_ENABLE:-false}" - "traefik.http.routers.mosaic-web.rule=Host(`${MOSAIC_WEB_DOMAIN:-mosaic.local}`)" - "traefik.http.routers.mosaic-web.entrypoints=${TRAEFIK_ENTRYPOINT:-websecure}" - "traefik.http.routers.mosaic-web.tls=${TRAEFIK_TLS_ENABLED:-true}" - "traefik.http.services.mosaic-web.loadbalancer.server.port=${WEB_PORT:-3000}" - "traefik.docker.network=${TRAEFIK_DOCKER_NETWORK:-mosaic-public}" # Let's Encrypt (if enabled) - "traefik.http.routers.mosaic-web.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}" # ====================== # Volumes # ====================== volumes: postgres_data: name: mosaic-postgres-data driver: local valkey_data: name: mosaic-valkey-data driver: local authentik_postgres_data: name: mosaic-authentik-postgres-data driver: local authentik_redis_data: name: mosaic-authentik-redis-data driver: local authentik_media: name: mosaic-authentik-media driver: local authentik_certs: name: mosaic-authentik-certs driver: local authentik_templates: name: mosaic-authentik-templates driver: local ollama_data: name: mosaic-ollama-data driver: local openbao_data: name: mosaic-openbao-data driver: local openbao_init: name: mosaic-openbao-init driver: local traefik_letsencrypt: name: mosaic-traefik-letsencrypt driver: local orchestrator_workspace: name: mosaic-orchestrator-workspace driver: local # ====================== # Networks # ====================== networks: # Internal network for database/cache isolation # Note: NOT marked as 'internal: true' because API needs external access # for Authentik OIDC and external Ollama services mosaic-internal: name: mosaic-internal driver: bridge # Public network for services that need external access mosaic-public: name: mosaic-public driver: bridge