# ============================================== # Mosaic Stack - Docker Swarm Deployment # ============================================== # # IMPORTANT: Docker Swarm does NOT support docker-compose profiles # To disable services (e.g., for external alternatives), manually comment them out # # 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) # # For detailed deployment instructions, see: # docs/SWARM-DEPLOYMENT.md # # 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) # # ============================================== services: # ====================== # PostgreSQL Database # ====================== postgres: image: git.mosaicstack.dev/mosaic/stack-postgres:${IMAGE_TAG:-latest} env_file: .env environment: POSTGRES_USER: ${POSTGRES_USER:-mosaic} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-mosaic_dev_password} POSTGRES_DB: ${POSTGRES_DB:-mosaic} 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}"] interval: 10s timeout: 5s retries: 5 start_period: 30s networks: - internal deploy: restart_policy: condition: on-failure # ====================== # Valkey Cache # ====================== valkey: image: valkey/valkey:8-alpine env_file: .env command: - valkey-server - --maxmemory ${VALKEY_MAXMEMORY:-256mb} - --maxmemory-policy noeviction - --appendonly yes volumes: - valkey_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 # ====================== # OpenBao Secrets Vault # ====================== openbao: image: git.mosaicstack.dev/mosaic/stack-openbao:${IMAGE_TAG:-latest} command: server -config=/openbao/config/config.hcl env_file: .env environment: OPENBAO_ADDR: http://0.0.0.0:8200 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&uninitcode=200&sealedcode=200", ] interval: 10s timeout: 5s retries: 5 start_period: 30s networks: - internal deploy: restart_policy: condition: on-failure # ====================== # OpenBao Init Sidecar # ====================== # Auto-initializes and unseals OpenBao on first run. # The init script has built-in retry logic (waits for OpenBao API). openbao-init: image: git.mosaicstack.dev/mosaic/stack-openbao:${IMAGE_TAG:-latest} command: /openbao/init.sh env_file: .env environment: VAULT_ADDR: http://openbao:8200 volumes: - openbao_init:/openbao/init networks: - internal deploy: restart_policy: condition: on-failure max_attempts: 5 delay: 10s # ====================== # Authentik - COMMENTED OUT (Using External Authentik) # ====================== # 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 # env_file: .env # 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 # env_file: .env # 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 # env_file: .env # 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 # env_file: .env # 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 # ====================== # Ollama (Optional AI Service) # ====================== # ollama: # image: ollama/ollama:latest # env_file: .env # 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 # ====================== # Mosaic Coordinator # ====================== coordinator: image: git.mosaicstack.dev/mosaic/stack-coordinator:${IMAGE_TAG:-latest} env_file: .env environment: GITEA_WEBHOOK_SECRET: ${GITEA_WEBHOOK_SECRET} GITEA_URL: ${GITEA_URL:-https://git.mosaicstack.dev} ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY} LOG_LEVEL: ${LOG_LEVEL:-info} HOST: 0.0.0.0 PORT: 8000 COORDINATOR_POLL_INTERVAL: ${COORDINATOR_POLL_INTERVAL:-5.0} COORDINATOR_MAX_CONCURRENT_AGENTS: ${COORDINATOR_MAX_CONCURRENT_AGENTS:-10} COORDINATOR_ENABLED: ${COORDINATOR_ENABLED:-true} healthcheck: test: [ "CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')", ] interval: 30s timeout: 10s retries: 3 start_period: 5s networks: - internal deploy: restart_policy: condition: on-failure # ====================== # Mosaic API # ====================== api: image: git.mosaicstack.dev/mosaic/stack-api:${IMAGE_TAG:-latest} env_file: .env 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:-http://localhost:3001/auth/callback} JWT_SECRET: ${JWT_SECRET:-change-this-to-a-random-secret} JWT_EXPIRATION: ${JWT_EXPIRATION:-24h} BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET} OLLAMA_ENDPOINT: ${OLLAMA_ENDPOINT:-http://ollama:11434} OPENBAO_ADDR: ${OPENBAO_ADDR:-http://openbao:8200} ORCHESTRATOR_URL: ${ORCHESTRATOR_URL:-http://orchestrator:3001} ENCRYPTION_KEY: ${ENCRYPTION_KEY} 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 # ====================== orchestrator: image: git.mosaicstack.dev/mosaic/stack-orchestrator:${IMAGE_TAG:-latest} env_file: .env user: "1000:1000" environment: NODE_ENV: production ORCHESTRATOR_PORT: 3001 VALKEY_URL: redis://valkey:6379 CLAUDE_API_KEY: ${CLAUDE_API_KEY} DOCKER_SOCKET: /var/run/docker.sock GIT_USER_NAME: "Mosaic Orchestrator" GIT_USER_EMAIL: "orchestrator@mosaicstack.dev" KILLSWITCH_ENABLED: "true" SANDBOX_ENABLED: "true" volumes: - /var/run/docker.sock:/var/run/docker.sock:ro - orchestrator_workspace:/workspace 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: - internal # Note: security_opt not supported in swarm mode # Security hardening done via cap_drop/cap_add cap_drop: - ALL cap_add: - NET_BIND_SERVICE tmpfs: - /tmp:noexec,nosuid,size=100m deploy: restart_policy: condition: on-failure # ====================== # Mosaic Web # ====================== web: image: git.mosaicstack.dev/mosaic/stack-web:${IMAGE_TAG:-latest} env_file: .env 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 networks: - traefik-public deploy: restart_policy: condition: on-failure 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}" # ====================== # Volumes # ====================== volumes: postgres_data: valkey_data: 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: # ====================== # Networks # ====================== networks: internal: driver: overlay traefik-public: external: true