feat(swarm): add coordinator service and reorganize compose files
- Add coordinator service to docker-compose.swarm.portainer.yml and docker-compose.swarm.yml with full environment config and healthcheck - Add ANTHROPIC_API_KEY and coordinator settings to .env.swarm.example - Move docker-compose.override.yml.example and docker-compose.prod.yml into docker/ directory - Add *.bak to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -130,6 +130,14 @@ GITEA_REPO_NAME=stack
|
|||||||
GITEA_WEBHOOK_SECRET=REPLACE_WITH_RANDOM_WEBHOOK_SECRET
|
GITEA_WEBHOOK_SECRET=REPLACE_WITH_RANDOM_WEBHOOK_SECRET
|
||||||
COORDINATOR_API_KEY=REPLACE_WITH_RANDOM_API_KEY_MINIMUM_32_CHARS
|
COORDINATOR_API_KEY=REPLACE_WITH_RANDOM_API_KEY_MINIMUM_32_CHARS
|
||||||
|
|
||||||
|
# ======================
|
||||||
|
# Coordinator Service
|
||||||
|
# ======================
|
||||||
|
ANTHROPIC_API_KEY=REPLACE_WITH_ANTHROPIC_API_KEY
|
||||||
|
COORDINATOR_POLL_INTERVAL=5.0
|
||||||
|
COORDINATOR_MAX_CONCURRENT_AGENTS=10
|
||||||
|
COORDINATOR_ENABLED=true
|
||||||
|
|
||||||
# ======================
|
# ======================
|
||||||
# Rate Limiting
|
# Rate Limiting
|
||||||
# ======================
|
# ======================
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -35,6 +35,7 @@ Thumbs.db
|
|||||||
.env.test.local
|
.env.test.local
|
||||||
.env.production.local
|
.env.production.local
|
||||||
.env.bak.*
|
.env.bak.*
|
||||||
|
*.bak
|
||||||
|
|
||||||
# Credentials (never commit)
|
# Credentials (never commit)
|
||||||
.admin-credentials
|
.admin-credentials
|
||||||
|
|||||||
415
docker-compose.swarm.portainer.yml
Normal file
415
docker-compose.swarm.portainer.yml
Normal file
@@ -0,0 +1,415 @@
|
|||||||
|
# ==============================================
|
||||||
|
# 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}
|
||||||
|
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
|
||||||
|
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 - COMMENTED OUT
|
||||||
|
# ======================
|
||||||
|
# IMPORTANT: OpenBao CANNOT run in swarm mode due to port binding conflicts.
|
||||||
|
# Deploy OpenBao as a standalone container instead:
|
||||||
|
# docker compose -f docker-compose.openbao.yml up -d
|
||||||
|
#
|
||||||
|
# Alternative: Use external HashiCorp Vault or managed secrets service
|
||||||
|
#
|
||||||
|
# openbao:
|
||||||
|
# image: git.mosaicstack.dev/mosaic/stack-openbao:${IMAGE_TAG:-latest}
|
||||||
|
# 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 - 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-alpine
|
||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# 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
|
||||||
|
# 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}
|
||||||
|
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}
|
||||||
|
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: git.mosaicstack.dev/mosaic/stack-orchestrator:${IMAGE_TAG:-latest}
|
||||||
|
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}
|
||||||
|
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 volumes - commented out (using standalone deployment)
|
||||||
|
# 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
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
# Current Configuration:
|
# Current Configuration:
|
||||||
# - PostgreSQL: ENABLED (internal)
|
# - PostgreSQL: ENABLED (internal)
|
||||||
# - Valkey: ENABLED (internal)
|
# - Valkey: ENABLED (internal)
|
||||||
|
# - Coordinator: ENABLED (internal)
|
||||||
# - OpenBao: DISABLED (must use standalone - see docker-compose.openbao.yml)
|
# - OpenBao: DISABLED (must use standalone - see docker-compose.openbao.yml)
|
||||||
# - Authentik: DISABLED (commented out - using external OIDC)
|
# - Authentik: DISABLED (commented out - using external OIDC)
|
||||||
# - Ollama: DISABLED (commented out - using external Ollama)
|
# - Ollama: DISABLED (commented out - using external Ollama)
|
||||||
@@ -230,17 +231,51 @@ services:
|
|||||||
# ======================
|
# ======================
|
||||||
# Ollama (Optional AI Service)
|
# Ollama (Optional AI Service)
|
||||||
# ======================
|
# ======================
|
||||||
ollama:
|
# ollama:
|
||||||
image: ollama/ollama:latest
|
# 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
|
env_file: .env
|
||||||
volumes:
|
environment:
|
||||||
- ollama_data:/root/.ollama
|
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:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:11434/api/tags"]
|
test:
|
||||||
|
[
|
||||||
|
"CMD",
|
||||||
|
"python",
|
||||||
|
"-c",
|
||||||
|
"import urllib.request; urllib.request.urlopen('http://localhost:8000/health')",
|
||||||
|
]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
start_period: 60s
|
start_period: 5s
|
||||||
networks:
|
networks:
|
||||||
- internal
|
- internal
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
179
docker/docker-compose.prod.yml
Normal file
179
docker/docker-compose.prod.yml
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
# Production Docker Compose - Uses pre-built images from Gitea Packages
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# - Images built and pushed to git.mosaicstack.dev/mosaic/*
|
||||||
|
# - .env file configured with production values
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# docker compose -f docker-compose.prod.yml up -d
|
||||||
|
#
|
||||||
|
# For Portainer:
|
||||||
|
# - Stack → Add Stack → Repository
|
||||||
|
# - Compose file: docker-compose.prod.yml
|
||||||
|
|
||||||
|
services:
|
||||||
|
# ======================
|
||||||
|
# PostgreSQL Database
|
||||||
|
# ======================
|
||||||
|
postgres:
|
||||||
|
image: git.mosaicstack.dev/mosaic/postgres:latest
|
||||||
|
container_name: mosaic-postgres
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${POSTGRES_USER:-mosaic}
|
||||||
|
POSTGRES_PASSWORD: ${POSTGRES_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
|
||||||
|
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
|
||||||
|
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
|
||||||
|
volumes:
|
||||||
|
- valkey_data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "valkey-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
networks:
|
||||||
|
- mosaic-internal
|
||||||
|
labels:
|
||||||
|
- "com.mosaic.service=cache"
|
||||||
|
- "com.mosaic.description=Valkey Redis-compatible cache"
|
||||||
|
|
||||||
|
# ======================
|
||||||
|
# Mosaic API
|
||||||
|
# ======================
|
||||||
|
api:
|
||||||
|
image: git.mosaicstack.dev/mosaic/api:latest
|
||||||
|
container_name: mosaic-api
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: ${API_PORT:-3001}
|
||||||
|
API_HOST: ${API_HOST:-0.0.0.0}
|
||||||
|
DATABASE_URL: postgresql://${POSTGRES_USER:-mosaic}:${POSTGRES_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}
|
||||||
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
|
JWT_EXPIRATION: ${JWT_EXPIRATION:-24h}
|
||||||
|
OLLAMA_ENDPOINT: ${OLLAMA_ENDPOINT:-http://ollama:11434}
|
||||||
|
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.enable=${TRAEFIK_ENABLE:-false}"
|
||||||
|
- "traefik.http.routers.mosaic-api.rule=Host(`${MOSAIC_API_DOMAIN:-api.mosaicstack.dev}`)"
|
||||||
|
- "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}"
|
||||||
|
- "traefik.http.routers.mosaic-api.tls.certresolver=${TRAEFIK_CERTRESOLVER:-}"
|
||||||
|
|
||||||
|
# ======================
|
||||||
|
# Mosaic Web
|
||||||
|
# ======================
|
||||||
|
web:
|
||||||
|
image: git.mosaicstack.dev/mosaic/web:latest
|
||||||
|
container_name: mosaic-web
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
NODE_ENV: production
|
||||||
|
PORT: ${WEB_PORT:-3000}
|
||||||
|
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-https://api.mosaicstack.dev}
|
||||||
|
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.enable=${TRAEFIK_ENABLE:-false}"
|
||||||
|
- "traefik.http.routers.mosaic-web.rule=Host(`${MOSAIC_WEB_DOMAIN:-app.mosaicstack.dev}`)"
|
||||||
|
- "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}"
|
||||||
|
- "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
|
||||||
|
|
||||||
|
# ======================
|
||||||
|
# Networks
|
||||||
|
# ======================
|
||||||
|
networks:
|
||||||
|
mosaic-internal:
|
||||||
|
name: mosaic-internal
|
||||||
|
driver: bridge
|
||||||
|
mosaic-public:
|
||||||
|
name: mosaic-public
|
||||||
|
driver: bridge
|
||||||
Reference in New Issue
Block a user