All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
- Remove ./docker/postgres/init-scripts bind mount from postgres service - Fixes: 'bind source path does not exist' error in Portainer - Init scripts are already baked into postgres image at build time Portainer can't access repository files when deploying stacks, so bind mounts to local paths don't work. The postgres image already includes init scripts via Dockerfile COPY. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
393 lines
12 KiB
YAML
393 lines
12 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)
|
|
# - 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 - 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}
|
|
# 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 - 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
|
|
# 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 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}
|
|
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}
|
|
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 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
|