services: # ====================== # PostgreSQL Database # ====================== postgres: image: mosaic-stack-postgres: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 - ./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: - 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 allkeys-lru - --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: mosaic-stack-openbao:latest env_file: .env 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 # ====================== # Authentik PostgreSQL # ====================== authentik-postgres: image: postgres:17-alpine 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 # ====================== 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 # ====================== 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 # ====================== 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 API # ====================== api: image: mosaic-stack-api: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} OLLAMA_ENDPOINT: ${OLLAMA_ENDPOINT:-http://ollama:11434} OPENBAO_ADDR: ${OPENBAO_ADDR:-http://openbao:8200} 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: mosaic-stack-orchestrator: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: mosaic-stack-web: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_postgres_data: authentik_redis_data: authentik_media: authentik_certs: authentik_templates: ollama_data: orchestrator_workspace: # ====================== # Networks # ====================== networks: internal: driver: overlay traefik-public: external: true