services: # ====================== # PostgreSQL Database # ====================== postgres: image: git.mosaicstack.dev/mosaic/stack-postgres:${IMAGE_TAG:-dev} 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 noeviction - --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:2025.10.2 # 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:2025.10.2 # 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: image: git.mosaicstack.dev/mosaic/stack-openbao:${IMAGE_TAG:-dev} 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: image: git.mosaicstack.dev/mosaic/stack-openbao:${IMAGE_TAG:-dev} 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: image: git.mosaicstack.dev/mosaic/stack-api:${IMAGE_TAG:-dev} 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} # Better Auth BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} # 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: image: git.mosaicstack.dev/mosaic/stack-orchestrator:${IMAGE_TAG:-dev} 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: image: git.mosaicstack.dev/mosaic/stack-web:${IMAGE_TAG:-dev} 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