Files
stack/docker-compose.swarm.yml
Jason Woltje 0ca3945061 fix(api): resolve Docker startup failures (secrets, Redis, Prisma)
- Pass BETTER_AUTH_SECRET through all 6 docker-compose files to API container
- Fix BullModule to parse VALKEY_URL instead of VALKEY_HOST/VALKEY_PORT,
  matching all other Redis consumers in the codebase
- Migrate Prisma encryption from removed $use() middleware to $extends()
  client extensions (Prisma 6.x compatibility), keeping extends PrismaClient
  pattern with only account and llmProviderInstance getters overridden

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:04:04 -06:00

450 lines
14 KiB
YAML

# ==============================================
# 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 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: 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