Files
stack/docker-compose.swarm.yml
Jason Woltje 3485ab7883
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
fix(swarm): remove postgres init-scripts bind mount for Portainer
- 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>
2026-02-08 20:29:25 -06:00

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