From ed92bb54024a1e1db4d834c278784d65cd252bf6 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Sun, 8 Feb 2026 01:18:04 -0600 Subject: [PATCH] feat(#swarm): Add Docker Swarm deployment with AI provider configuration - Add setup-wizard.sh for interactive configuration - Add docker-compose.swarm.yml optimized for swarm deployment - Make CLAUDE_API_KEY optional based on AI_PROVIDER setting - Support multiple AI providers: Ollama, Claude API, OpenAI - Add BETTER_AUTH_SECRET to .env.example - Update deploy-swarm.sh to validate AI provider config - Add comprehensive documentation (DOCKER-SWARM.md, SWARM-QUICKREF.md) Changes: - AI_PROVIDER env var controls which AI backend to use - Ollama is default (no API key required) - Claude API and OpenAI require respective API keys - Deployment script validates based on selected provider - Removed Authentik services from swarm compose (using external) - Configured for upstream Traefik integration --- .env.example | 32 ++++ .env.swarm.example | 147 +++++++++++++++ DOCKER-SWARM.md | 299 ++++++++++++++++++++++++++++++ README.md | 40 +++++ SWARM-QUICKREF.md | 323 +++++++++++++++++++++++++++++++++ deploy-swarm.sh | 101 +++++++++++ docker-compose.swarm.yml | 380 +++++++++++++++++++++++++++++++++++++++ setup-wizard.sh | 221 +++++++++++++++++++++++ 8 files changed, 1543 insertions(+) create mode 100644 .env.swarm.example create mode 100644 DOCKER-SWARM.md create mode 100644 SWARM-QUICKREF.md create mode 100755 deploy-swarm.sh create mode 100644 docker-compose.swarm.yml create mode 100755 setup-wizard.sh diff --git a/.env.example b/.env.example index 8309044..a647c1e 100644 --- a/.env.example +++ b/.env.example @@ -90,6 +90,14 @@ AUTHENTIK_PORT_HTTPS=9443 JWT_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS JWT_EXPIRATION=24h +# ====================== +# BetterAuth Configuration +# ====================== +# CRITICAL: Generate a random secret key with at least 32 characters +# This is used by BetterAuth for session management and CSRF protection +# Example: openssl rand -base64 32 +BETTER_AUTH_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS + # ====================== # Encryption (Credential Security) # ====================== @@ -264,6 +272,30 @@ RATE_LIMIT_STORAGE=redis # Health endpoints (/health/*) remain unauthenticated ORCHESTRATOR_API_KEY=REPLACE_WITH_RANDOM_API_KEY_MINIMUM_32_CHARS +# ====================== +# AI Provider Configuration +# ====================== +# Choose the AI provider for orchestrator agents +# Options: ollama, claude, openai +# Default: ollama (no API key required) +AI_PROVIDER=ollama + +# Ollama Configuration (when AI_PROVIDER=ollama) +# For local Ollama: http://localhost:11434 +# For remote Ollama: http://your-ollama-server:11434 +OLLAMA_MODEL=llama3.1:latest + +# Claude API Configuration (when AI_PROVIDER=claude) +# OPTIONAL: Only required if AI_PROVIDER=claude +# Get your API key from: https://console.anthropic.com/ +# Note: Claude Max subscription users should use AI_PROVIDER=ollama instead +# CLAUDE_API_KEY=sk-ant-... + +# OpenAI API Configuration (when AI_PROVIDER=openai) +# OPTIONAL: Only required if AI_PROVIDER=openai +# Get your API key from: https://platform.openai.com/api-keys +# OPENAI_API_KEY=sk-... + # ====================== # Logging & Debugging # ====================== diff --git a/.env.swarm.example b/.env.swarm.example new file mode 100644 index 0000000..6bc6fc4 --- /dev/null +++ b/.env.swarm.example @@ -0,0 +1,147 @@ +# ============================================== +# Mosaic Stack - Docker Swarm Configuration +# ============================================== +# Copy this file to .env for Docker Swarm deployment + +# ====================== +# Application Ports (Internal) +# ====================== +API_PORT=3001 +API_HOST=0.0.0.0 +WEB_PORT=3000 + +# ====================== +# Domain Configuration (Traefik) +# ====================== +# These domains must be configured in your DNS or /etc/hosts +MOSAIC_API_DOMAIN=api.mosaicstack.dev +MOSAIC_WEB_DOMAIN=mosaic.mosaicstack.dev +MOSAIC_AUTH_DOMAIN=auth.mosaicstack.dev + +# ====================== +# Web Configuration +# ====================== +# Use the Traefik domain for the API URL +NEXT_PUBLIC_APP_URL=http://mosaic.mosaicstack.dev +NEXT_PUBLIC_API_URL=http://api.mosaicstack.dev + +# ====================== +# PostgreSQL Database +# ====================== +DATABASE_URL=postgresql://mosaic:REPLACE_WITH_SECURE_PASSWORD@postgres:5432/mosaic +POSTGRES_USER=mosaic +POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD +POSTGRES_DB=mosaic +POSTGRES_PORT=5432 + +# PostgreSQL Performance Tuning +POSTGRES_SHARED_BUFFERS=256MB +POSTGRES_EFFECTIVE_CACHE_SIZE=1GB +POSTGRES_MAX_CONNECTIONS=100 + +# ====================== +# Valkey Cache +# ====================== +VALKEY_URL=redis://valkey:6379 +VALKEY_HOST=valkey +VALKEY_PORT=6379 +VALKEY_MAXMEMORY=256mb + +# Knowledge Module Cache Configuration +KNOWLEDGE_CACHE_ENABLED=true +KNOWLEDGE_CACHE_TTL=300 + +# ====================== +# Authentication (Authentik OIDC) +# ====================== +OIDC_ENABLED=true +OIDC_ISSUER=http://auth.mosaicstack.dev/application/o/mosaic-stack/ +OIDC_CLIENT_ID=your-client-id-here +OIDC_CLIENT_SECRET=your-client-secret-here +OIDC_REDIRECT_URI=http://api.mosaicstack.dev/auth/callback/authentik + +# Authentik PostgreSQL Database +AUTHENTIK_POSTGRES_USER=authentik +AUTHENTIK_POSTGRES_PASSWORD=REPLACE_WITH_SECURE_PASSWORD +AUTHENTIK_POSTGRES_DB=authentik + +# Authentik Configuration +AUTHENTIK_SECRET_KEY=REPLACE_WITH_RANDOM_SECRET_MINIMUM_50_CHARS +AUTHENTIK_ERROR_REPORTING=false +AUTHENTIK_BOOTSTRAP_PASSWORD=REPLACE_WITH_SECURE_PASSWORD +AUTHENTIK_BOOTSTRAP_EMAIL=admin@mosaicstack.dev +AUTHENTIK_COOKIE_DOMAIN=.mosaicstack.dev + +# ====================== +# JWT Configuration +# ====================== +JWT_SECRET=REPLACE_WITH_RANDOM_SECRET_MINIMUM_32_CHARS +JWT_EXPIRATION=24h + +# ====================== +# Encryption (Credential Security) +# ====================== +# Generate with: openssl rand -hex 32 +ENCRYPTION_KEY=REPLACE_WITH_64_CHAR_HEX_STRING_GENERATE_WITH_OPENSSL_RAND_HEX_32 + +# ====================== +# OpenBao Secrets Management +# ====================== +OPENBAO_ADDR=http://openbao:8200 +OPENBAO_PORT=8200 +# For development only - remove in production +OPENBAO_DEV_ROOT_TOKEN_ID=root + +# ====================== +# Ollama (Optional AI Service) +# ====================== +OLLAMA_ENDPOINT=http://ollama:11434 +OLLAMA_PORT=11434 +OLLAMA_EMBEDDING_MODEL=mxbai-embed-large + +# Semantic Search Configuration +SEMANTIC_SEARCH_SIMILARITY_THRESHOLD=0.5 + +# ====================== +# OpenAI API (Optional) +# ====================== +# OPENAI_API_KEY=sk-... + +# ====================== +# Application Environment +# ====================== +NODE_ENV=production + +# ====================== +# Gitea Integration (Coordinator) +# ====================== +GITEA_URL=https://git.mosaicstack.dev +GITEA_BOT_USERNAME=mosaic +GITEA_BOT_TOKEN=REPLACE_WITH_COORDINATOR_BOT_API_TOKEN +GITEA_BOT_PASSWORD=REPLACE_WITH_COORDINATOR_BOT_PASSWORD +GITEA_REPO_OWNER=mosaic +GITEA_REPO_NAME=stack +GITEA_WEBHOOK_SECRET=REPLACE_WITH_RANDOM_WEBHOOK_SECRET +COORDINATOR_API_KEY=REPLACE_WITH_RANDOM_API_KEY_MINIMUM_32_CHARS + +# ====================== +# Rate Limiting +# ====================== +RATE_LIMIT_TTL=60 +RATE_LIMIT_GLOBAL_LIMIT=100 +RATE_LIMIT_WEBHOOK_LIMIT=60 +RATE_LIMIT_COORDINATOR_LIMIT=100 +RATE_LIMIT_HEALTH_LIMIT=300 +RATE_LIMIT_STORAGE=redis + +# ====================== +# Orchestrator Configuration +# ====================== +ORCHESTRATOR_API_KEY=REPLACE_WITH_RANDOM_API_KEY_MINIMUM_32_CHARS +CLAUDE_API_KEY=REPLACE_WITH_CLAUDE_API_KEY + +# ====================== +# Logging & Debugging +# ====================== +LOG_LEVEL=info +DEBUG=false diff --git a/DOCKER-SWARM.md b/DOCKER-SWARM.md new file mode 100644 index 0000000..c3423f6 --- /dev/null +++ b/DOCKER-SWARM.md @@ -0,0 +1,299 @@ +# Mosaic Stack - Docker Swarm Deployment + +This guide covers deploying Mosaic Stack to a Docker Swarm cluster with Traefik reverse proxy integration. + +## Prerequisites + +1. **Docker Swarm initialized:** + + ```bash + docker swarm init + ``` + +2. **Traefik running on the swarm** with a network named `traefik-public` + +3. **DNS or /etc/hosts configured** with your domain names: + - `mosaic.mosaicstack.dev` → Web UI + - `api.mosaicstack.dev` → API + - `auth.mosaicstack.dev` → Authentik SSO + +## Quick Start + +### 1. Configure Environment + +Copy the swarm environment template: + +```bash +cp .env.swarm.example .env +``` + +Edit `.env` and set the following **critical** values: + +```bash +# Database passwords +POSTGRES_PASSWORD=your-secure-password-here +AUTHENTIK_POSTGRES_PASSWORD=your-secure-password-here + +# Secrets (generate with openssl rand -hex 32 or openssl rand -base64 50) +AUTHENTIK_SECRET_KEY=$(openssl rand -base64 50) +JWT_SECRET=$(openssl rand -base64 32) +ENCRYPTION_KEY=$(openssl rand -hex 32) +ORCHESTRATOR_API_KEY=$(openssl rand -base64 32) +COORDINATOR_API_KEY=$(openssl rand -base64 32) + +# Claude API Key +CLAUDE_API_KEY=your-claude-api-key + +# Authentik Bootstrap +AUTHENTIK_BOOTSTRAP_PASSWORD=your-admin-password +AUTHENTIK_BOOTSTRAP_EMAIL=admin@yourdomain.com +``` + +### 2. Create Traefik Network (if not exists) + +```bash +docker network create --driver=overlay traefik-public +``` + +### 3. Deploy the Stack + +```bash +./deploy-swarm.sh mosaic +``` + +Or manually: + +```bash +docker stack deploy -c docker-compose.swarm.yml mosaic +``` + +### 4. Verify Deployment + +Check stack status: + +```bash +docker stack services mosaic +docker stack ps mosaic +``` + +Check service logs: + +```bash +docker service logs mosaic_api +docker service logs mosaic_web +docker service logs mosaic_postgres +``` + +## Stack Services + +The following services will be deployed: + +| Service | Internal Port | Traefik Domain | Description | +| ------------------ | ------------- | ------------------------ | ------------------------ | +| `web` | 3000 | `mosaic.mosaicstack.dev` | Next.js Web UI | +| `api` | 3001 | `api.mosaicstack.dev` | NestJS API | +| `authentik-server` | 9000 | `auth.mosaicstack.dev` | Authentik SSO | +| `postgres` | 5432 | - | PostgreSQL 17 + pgvector | +| `valkey` | 6379 | - | Redis-compatible cache | +| `openbao` | 8200 | - | Secrets vault | +| `ollama` | 11434 | - | LLM service (optional) | +| `orchestrator` | 3001 | - | Agent orchestrator | + +## Traefik Integration + +Services are automatically registered with Traefik using labels defined in `deploy.labels`: + +```yaml +deploy: + labels: + - "traefik.enable=true" + - "traefik.http.routers.mosaic-web.rule=Host(`mosaic.mosaicstack.dev`)" + - "traefik.http.routers.mosaic-web.entrypoints=web" + - "traefik.http.services.mosaic-web.loadbalancer.server.port=3000" +``` + +**Important:** Traefik labels MUST be under `deploy.labels` for Docker Swarm (not at service level). + +## Accessing Services + +Once deployed and Traefik is configured: + +- **Web UI:** http://mosaic.mosaicstack.dev +- **API:** http://api.mosaicstack.dev +- **Authentik:** http://auth.mosaicstack.dev + +## Scaling Services + +Scale specific services: + +```bash +# Scale web frontend to 3 replicas +docker service scale mosaic_web=3 + +# Scale API to 2 replicas +docker service scale mosaic_api=2 +``` + +**Note:** Database services (postgres, valkey) should NOT be scaled (remain at 1 replica). + +## Updating Services + +Update a specific service: + +```bash +# Rebuild image +docker compose -f docker-compose.swarm.yml build api + +# Update the service +docker service update --image mosaic-stack-api:latest mosaic_api +``` + +Or redeploy the entire stack: + +```bash +./deploy-swarm.sh mosaic +``` + +## Rolling Updates + +Docker Swarm supports rolling updates. To configure: + +```yaml +deploy: + update_config: + parallelism: 1 + delay: 10s + order: start-first + rollback_config: + parallelism: 1 + delay: 10s +``` + +## Troubleshooting + +### Service Won't Start + +Check service logs: + +```bash +docker service logs mosaic_api --tail 100 --follow +``` + +Check service tasks: + +```bash +docker service ps mosaic_api --no-trunc +``` + +### Traefik Not Routing + +1. Verify service is on `traefik-public` network: + + ```bash + docker service inspect mosaic_web | grep -A 10 Networks + ``` + +2. Check Traefik dashboard for registered routes: + - Usually at http://traefik.yourdomain.com/dashboard/ + +3. Verify domain DNS/hosts resolution: + ```bash + ping mosaic.mosaicstack.dev + ``` + +### Database Connection Issues + +Check postgres is healthy: + +```bash +docker service logs mosaic_postgres --tail 50 +``` + +Verify DATABASE_URL in API service: + +```bash +docker service inspect mosaic_api --format '{{json .Spec.TaskTemplate.ContainerSpec.Env}}' | jq +``` + +### Volume Permissions + +If volume permission errors occur, check service user: + +```bash +# Orchestrator runs as user 1000:1000 +docker service inspect mosaic_orchestrator | grep -A 5 User +``` + +## Backup & Restore + +### Backup Volumes + +```bash +# Backup postgres data +docker run --rm -v mosaic_postgres_data:/data -v $(pwd):/backup alpine \ + tar czf /backup/postgres-backup-$(date +%Y%m%d).tar.gz -C /data . + +# Backup authentik data +docker run --rm -v mosaic_authentik_postgres_data:/data -v $(pwd):/backup alpine \ + tar czf /backup/authentik-backup-$(date +%Y%m%d).tar.gz -C /data . +``` + +### Restore Volumes + +```bash +# Restore postgres data +docker run --rm -v mosaic_postgres_data:/data -v $(pwd):/backup alpine \ + tar xzf /backup/postgres-backup-20260208.tar.gz -C /data + +# Restore authentik data +docker run --rm -v mosaic_authentik_postgres_data:/data -v $(pwd):/backup alpine \ + tar xzf /backup/authentik-backup-20260208.tar.gz -C /data +``` + +## Removing the Stack + +Remove all services and networks (volumes are preserved): + +```bash +docker stack rm mosaic +``` + +Remove volumes (⚠️ **DATA WILL BE LOST**): + +```bash +docker volume rm mosaic_postgres_data +docker volume rm mosaic_valkey_data +docker volume rm mosaic_authentik_postgres_data +# ... etc +``` + +## Security Considerations + +1. **Change default passwords** in `.env` before deploying +2. **Use secrets management** for production: + ```bash + echo "my-db-password" | docker secret create postgres_password - + ``` +3. **Enable TLS** in Traefik (Let's Encrypt) +4. **Restrict network access** using Docker network policies +5. **Run services as non-root** (orchestrator already does this) + +## Differences from Docker Compose + +Key differences when running in Swarm mode: + +| Feature | Docker Compose | Docker Swarm | +| ---------------- | ---------------------------------- | ----------------------- | +| Container names | `container_name: foo` | Auto-generated | +| Restart policy | `restart: unless-stopped` | `deploy.restart_policy` | +| Labels (Traefik) | Service level | `deploy.labels` | +| Networks | `bridge` driver | `overlay` driver | +| Scaling | Manual `docker compose up --scale` | `docker service scale` | +| Updates | Stop/start containers | Rolling updates | + +## Reference + +- **Compose file:** `docker-compose.swarm.yml` +- **Environment:** `.env.swarm.example` +- **Deployment script:** `deploy-swarm.sh` +- **Traefik example:** `../mosaic-telemetry/docker-compose.yml` diff --git a/README.md b/README.md index 0890556..94ccea8 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,46 @@ docker compose down See [Docker Deployment Guide](docs/1-getting-started/4-docker-deployment/) for complete documentation. +### Docker Swarm Deployment (Production) + +**Recommended for production deployments with high availability and auto-scaling.** + +Deploy to a Docker Swarm cluster with integrated Traefik reverse proxy: + +```bash +# 1. Initialize swarm (if not already done) +docker swarm init + +# 2. Create Traefik network +docker network create --driver=overlay traefik-public + +# 3. Configure environment for swarm +cp .env.swarm.example .env +nano .env # Configure domains, passwords, API keys + +# 4. Deploy stack +./deploy-swarm.sh mosaic + +# 5. Check deployment status +docker stack services mosaic +docker stack ps mosaic + +# Access services via Traefik +# Web: http://mosaic.mosaicstack.dev +# API: http://api.mosaicstack.dev +# Auth: http://auth.mosaicstack.dev +``` + +**Key features:** + +- Automatic Traefik integration for routing +- Overlay networking for multi-host deployments +- Built-in health checks and rolling updates +- Horizontal scaling for web and API services +- Zero-downtime deployments + +See [Docker Swarm Deployment Guide](DOCKER-SWARM.md) and [Quick Reference](SWARM-QUICKREF.md) for complete documentation. + ## Project Structure ``` diff --git a/SWARM-QUICKREF.md b/SWARM-QUICKREF.md new file mode 100644 index 0000000..d9a6867 --- /dev/null +++ b/SWARM-QUICKREF.md @@ -0,0 +1,323 @@ +# Docker Swarm Quick Reference + +## Initial Setup + +```bash +# 1. Configure environment +cp .env.swarm.example .env +nano .env # Set passwords, API keys, domains + +# 2. Create Traefik network (if needed) +docker network create --driver=overlay traefik-public + +# 3. Deploy stack +./deploy-swarm.sh mosaic +``` + +## Common Commands + +### Stack Management + +```bash +# Deploy/update stack +docker stack deploy -c docker-compose.swarm.yml mosaic + +# List all stacks +docker stack ls + +# Remove stack +docker stack rm mosaic + +# List services in stack +docker stack services mosaic + +# List tasks in stack +docker stack ps mosaic +``` + +### Service Management + +```bash +# List all services +docker service ls + +# Inspect service +docker service inspect mosaic_api + +# View service logs +docker service logs mosaic_api --tail 100 --follow + +# Scale service +docker service scale mosaic_web=3 + +# Update service (force redeploy) +docker service update --force mosaic_api + +# Update service image +docker service update --image mosaic-stack-api:latest mosaic_api + +# Rollback service +docker service rollback mosaic_api +``` + +### Monitoring + +```bash +# Watch service status +watch -n 2 'docker service ls' + +# Service resource usage +docker stats $(docker ps --filter label=com.docker.swarm.service.name=mosaic_api -q) + +# Check service placement +docker service ps mosaic_api --format "table {{.Name}}\t{{.Node}}\t{{.CurrentState}}" +``` + +### Debugging + +```bash +# Check why service failed +docker service ps mosaic_api --no-trunc + +# View recent logs with timestamps +docker service logs mosaic_api --timestamps --tail 50 + +# Follow logs in real-time +docker service logs mosaic_api --follow + +# Exec into running container +docker exec -it $(docker ps -q -f label=com.docker.swarm.service.name=mosaic_api) sh +``` + +### Network Management + +```bash +# List networks +docker network ls + +# Inspect traefik-public network +docker network inspect traefik-public + +# List containers on traefik-public +docker network inspect traefik-public --format '{{range .Containers}}{{.Name}} {{end}}' +``` + +### Volume Management + +```bash +# List volumes +docker volume ls --filter label=com.docker.stack.namespace=mosaic + +# Inspect volume +docker volume inspect mosaic_postgres_data + +# Backup volume +docker run --rm -v mosaic_postgres_data:/data -v $(pwd):/backup alpine \ + tar czf /backup/postgres-$(date +%Y%m%d-%H%M%S).tar.gz -C /data . + +# Restore volume +docker run --rm -v mosaic_postgres_data:/data -v $(pwd):/backup alpine \ + tar xzf /backup/postgres-20260208-143022.tar.gz -C /data +``` + +## Service-Specific Commands + +### Database (PostgreSQL) + +```bash +# Connect to database +docker exec -it $(docker ps -q -f label=com.docker.swarm.service.name=mosaic_postgres) \ + psql -U mosaic -d mosaic + +# Run migrations (from API container) +docker exec $(docker ps -q -f label=com.docker.swarm.service.name=mosaic_api) \ + pnpm prisma migrate deploy + +# View database logs +docker service logs mosaic_postgres --tail 100 +``` + +### API Service + +```bash +# View API logs +docker service logs mosaic_api --follow + +# Check API health +curl http://api.mosaicstack.dev/health + +# Force API redeploy +docker service update --force mosaic_api +``` + +### Web Service + +```bash +# View web logs +docker service logs mosaic_web --follow + +# Scale web to 3 replicas +docker service scale mosaic_web=3 + +# Check web health +curl http://mosaic.mosaicstack.dev +``` + +### Authentik + +```bash +# View Authentik logs +docker service logs mosaic_authentik-server --follow +docker service logs mosaic_authentik-worker --follow + +# Access Authentik UI +open http://auth.mosaicstack.dev +``` + +## Troubleshooting + +### Service Won't Start + +```bash +# 1. Check service tasks +docker service ps mosaic_api --no-trunc + +# 2. View service logs +docker service logs mosaic_api --tail 100 + +# 3. Check if image exists +docker images | grep mosaic-stack-api + +# 4. Rebuild and update +docker compose -f docker-compose.swarm.yml build api +docker service update --image mosaic-stack-api:latest mosaic_api +``` + +### Traefik Not Routing + +```bash +# 1. Verify service is on traefik-public network +docker service inspect mosaic_web | grep -A 10 Networks + +# 2. Check Traefik labels +docker service inspect mosaic_web --format '{{json .Spec.Labels}}' | jq + +# 3. Verify DNS resolution +ping mosaic.mosaicstack.dev + +# 4. Check Traefik logs (if Traefik is a service) +docker service logs traefik --tail 50 +``` + +### Database Connection Failed + +```bash +# 1. Check postgres is running +docker service ls | grep postgres + +# 2. Check postgres health +docker service ps mosaic_postgres + +# 3. View postgres logs +docker service logs mosaic_postgres --tail 50 + +# 4. Test connection from API container +docker exec $(docker ps -q -f label=com.docker.swarm.service.name=mosaic_api) \ + sh -c 'nc -zv postgres 5432' +``` + +### Out of Memory / Resources + +```bash +# Check node resources +docker node ls +docker node inspect self --format '{{json .Description.Resources}}' | jq + +# Check service resource limits +docker service inspect mosaic_api --format '{{json .Spec.TaskTemplate.Resources}}' | jq + +# Update resource limits +docker service update --limit-memory 1g --reserve-memory 512m mosaic_api +``` + +## Useful Aliases + +Add to `~/.bashrc` or `~/.zshrc`: + +```bash +# Stack shortcuts +alias dss='docker stack services' +alias dsp='docker stack ps' +alias dsl='docker service logs' +alias dsi='docker service inspect' +alias dsu='docker service update' + +# Mosaic-specific +alias mosaic-logs='docker service logs mosaic_api --follow' +alias mosaic-status='docker stack services mosaic' +alias mosaic-ps='docker stack ps mosaic' +alias mosaic-deploy='./deploy-swarm.sh mosaic' +``` + +## Emergency Procedures + +### Complete Stack Restart + +```bash +# 1. Remove stack (keeps volumes) +docker stack rm mosaic + +# 2. Wait for cleanup (30 seconds) +sleep 30 + +# 3. Redeploy +./deploy-swarm.sh mosaic +``` + +### Database Recovery + +```bash +# 1. Stop API to prevent writes +docker service scale mosaic_api=0 + +# 2. Backup current database +docker run --rm -v mosaic_postgres_data:/data -v $(pwd):/backup alpine \ + tar czf /backup/postgres-emergency-$(date +%Y%m%d-%H%M%S).tar.gz -C /data . + +# 3. Stop postgres +docker service scale mosaic_postgres=0 + +# 4. Restore from backup +docker run --rm -v mosaic_postgres_data:/data -v $(pwd):/backup alpine \ + sh -c 'rm -rf /data/* && tar xzf /backup/postgres-20260208.tar.gz -C /data' + +# 5. Restart postgres +docker service scale mosaic_postgres=1 + +# 6. Wait for postgres healthy +sleep 10 + +# 7. Restart API +docker service scale mosaic_api=1 +``` + +## Health Checks + +```bash +# API +curl http://api.mosaicstack.dev/health + +# Web +curl http://mosaic.mosaicstack.dev + +# Authentik +curl http://auth.mosaicstack.dev/-/health/live/ + +# Postgres (from API container) +docker exec $(docker ps -q -f label=com.docker.swarm.service.name=mosaic_api) \ + sh -c 'nc -zv postgres 5432' + +# Valkey (from API container) +docker exec $(docker ps -q -f label=com.docker.swarm.service.name=mosaic_api) \ + sh -c 'nc -zv valkey 6379' +``` diff --git a/deploy-swarm.sh b/deploy-swarm.sh new file mode 100755 index 0000000..8008a48 --- /dev/null +++ b/deploy-swarm.sh @@ -0,0 +1,101 @@ +#!/bin/bash +set -euo pipefail + +# Mosaic Stack - Docker Swarm Deployment Script +# Usage: ./deploy-swarm.sh [stack-name] + +STACK_NAME="${1:-mosaic}" +COMPOSE_FILE="docker-compose.swarm.yml" + +echo "🚀 Deploying Mosaic Stack to Docker Swarm..." +echo "Stack name: $STACK_NAME" +echo "Compose file: $COMPOSE_FILE" +echo "" + +# Check if .env exists +if [ ! -f .env ]; then + echo "❌ Error: .env file not found" + echo "📝 Run the setup wizard to create it:" + echo " ./setup-wizard.sh" + echo "" + echo "Or copy from example:" + echo " cp .env.example .env" + echo " nano .env" + exit 1 +fi + +# Check required environment variables +echo "🔍 Checking required environment variables..." +missing_vars=() + +# Check critical variables +for var in POSTGRES_PASSWORD JWT_SECRET OIDC_CLIENT_ID OIDC_CLIENT_SECRET ENCRYPTION_KEY BETTER_AUTH_SECRET; do + if ! grep -q "^${var}=" .env || grep -q "^${var}=.*REPLACE" .env || [ -z "$(grep "^${var}=" .env | cut -d= -f2)" ]; then + missing_vars+=("$var") + fi +done + +# Check AI provider configuration +AI_PROVIDER=$(grep "^AI_PROVIDER=" .env 2>/dev/null | cut -d= -f2 || echo "ollama") + +if [ "$AI_PROVIDER" = "claude" ]; then + if ! grep -q "^CLAUDE_API_KEY=" .env || grep -q "^CLAUDE_API_KEY=.*REPLACE" .env; then + missing_vars+=("CLAUDE_API_KEY (required when AI_PROVIDER=claude)") + fi +elif [ "$AI_PROVIDER" = "openai" ]; then + if ! grep -q "^OPENAI_API_KEY=" .env || grep -q "^OPENAI_API_KEY=.*REPLACE" .env; then + missing_vars+=("OPENAI_API_KEY (required when AI_PROVIDER=openai)") + fi +fi + +if [ ${#missing_vars[@]} -gt 0 ]; then + echo "❌ Missing or placeholder values for:" + printf " - %s\n" "${missing_vars[@]}" + echo "" + echo "Run the setup wizard to configure:" + echo " ./setup-wizard.sh" + echo "" + echo "Or manually edit .env" + exit 1 +fi + +echo "✅ All required variables configured" +echo " AI Provider: $AI_PROVIDER" +echo "" + +# Check if traefik-public network exists +if ! docker network ls --filter name=traefik-public --format '{{.Name}}' | grep -q '^traefik-public$'; then + echo "⚠️ traefik-public network not found. Creating it..." + docker network create --driver=overlay traefik-public + echo "✅ traefik-public network created" +else + echo "✅ traefik-public network already exists" +fi + +# Build images (optional - uncomment if you want to build before deploying) +# echo "" +# echo "🔨 Building images..." +# docker compose -f $COMPOSE_FILE build + +# Deploy the stack +echo "" +echo "📦 Deploying stack..." +docker stack deploy -c $COMPOSE_FILE --with-registry-auth $STACK_NAME + +echo "" +echo "✅ Stack deployed successfully!" +echo "" +echo "📊 Stack status:" +docker stack ps $STACK_NAME --format "table {{.Name}}\t{{.CurrentState}}\t{{.Error}}" + +echo "" +echo "🔍 To check stack services:" +echo " docker stack services $STACK_NAME" +echo "" +echo "🔍 To check stack logs:" +echo " docker service logs ${STACK_NAME}_api" +echo " docker service logs ${STACK_NAME}_web" +echo " docker service logs ${STACK_NAME}_postgres" +echo "" +echo "🗑️ To remove the stack:" +echo " docker stack rm $STACK_NAME" diff --git a/docker-compose.swarm.yml b/docker-compose.swarm.yml new file mode 100644 index 0000000..dcabdb9 --- /dev/null +++ b/docker-compose.swarm.yml @@ -0,0 +1,380 @@ +services: + # ====================== + # PostgreSQL Database + # ====================== + postgres: + build: + context: ./docker/postgres + dockerfile: Dockerfile + 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: + build: + context: ./docker/openbao + dockerfile: Dockerfile + 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 + build: + context: . + dockerfile: ./apps/api/Dockerfile + args: + - NODE_ENV=production + 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 + build: + context: . + dockerfile: ./apps/orchestrator/Dockerfile + 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 + security_opt: + - no-new-privileges:true + 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 + build: + context: . + dockerfile: ./apps/web/Dockerfile + args: + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:3001} + 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 diff --git a/setup-wizard.sh b/setup-wizard.sh new file mode 100755 index 0000000..5d29c25 --- /dev/null +++ b/setup-wizard.sh @@ -0,0 +1,221 @@ +#!/bin/bash +set -euo pipefail + +echo "═══════════════════════════════════════════════════════" +echo " Mosaic Stack - Interactive Setup Wizard" +echo "═══════════════════════════════════════════════════════" +echo "" + +# Backup existing .env if it exists +if [ -f .env ]; then + BACKUP=".env.bak.$(date +%Y%m%d_%H%M%S)" + cp .env "$BACKUP" + echo "✓ Backed up existing .env to $BACKUP" + echo "" +fi + +# Start with .env.example as base +if [ ! -f .env ]; then + cp .env.example .env +fi + +# Helper function to update or add env var +update_env() { + local key=$1 + local value=$2 + if grep -q "^$key=" .env; then + sed -i "s|^$key=.*|$key=$value|" .env + else + echo "$key=$value" >> .env + fi +} + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Domain Configuration" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +read -p "Web domain [app.mosaicstack.dev]: " WEB_DOMAIN +WEB_DOMAIN=${WEB_DOMAIN:-app.mosaicstack.dev} +update_env "MOSAIC_WEB_DOMAIN" "$WEB_DOMAIN" + +read -p "API domain [api.mosaicstack.dev]: " API_DOMAIN +API_DOMAIN=${API_DOMAIN:-api.mosaicstack.dev} +update_env "MOSAIC_API_DOMAIN" "$API_DOMAIN" + +# Update NEXT_PUBLIC_API_URL to use the configured domain +update_env "NEXT_PUBLIC_API_URL" "https://$API_DOMAIN" +update_env "MOSAIC_BASE_URL" "https://$WEB_DOMAIN" + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Authentication Configuration" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +read -p "Authentik OIDC Issuer URL: " OIDC_ISSUER +if [ -n "$OIDC_ISSUER" ]; then + update_env "OIDC_ISSUER" "$OIDC_ISSUER" +fi + +read -p "OIDC Client ID: " OIDC_CLIENT_ID +if [ -n "$OIDC_CLIENT_ID" ]; then + update_env "OIDC_CLIENT_ID" "$OIDC_CLIENT_ID" +fi + +read -sp "OIDC Client Secret: " OIDC_CLIENT_SECRET +echo "" +if [ -n "$OIDC_CLIENT_SECRET" ]; then + update_env "OIDC_CLIENT_SECRET" "$OIDC_CLIENT_SECRET" +fi + +# Auto-set redirect URI +REDIRECT_URI="https://${API_DOMAIN}/auth/callback/authentik" +update_env "OIDC_REDIRECT_URI" "$REDIRECT_URI" +echo "✓ Set redirect URI to: $REDIRECT_URI" + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " AI Provider Configuration" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" +echo "Choose AI provider for orchestrator agents:" +echo " 1) Ollama (local or remote) - Recommended for self-hosted" +echo " 2) Claude API (requires API key)" +echo " 3) OpenAI API (requires API key)" +echo " 4) Skip (configure manually later)" +echo "" +read -p "Select [1]: " AI_CHOICE +AI_CHOICE=${AI_CHOICE:-1} + +case $AI_CHOICE in + 1) + echo "" + read -p "Ollama endpoint [http://localhost:11434]: " OLLAMA_ENDPOINT + OLLAMA_ENDPOINT=${OLLAMA_ENDPOINT:-http://localhost:11434} + update_env "OLLAMA_ENDPOINT" "$OLLAMA_ENDPOINT" + update_env "OLLAMA_MODE" "remote" + + read -p "Ollama model for orchestrator [llama3.1:latest]: " OLLAMA_MODEL + OLLAMA_MODEL=${OLLAMA_MODEL:-llama3.1:latest} + update_env "OLLAMA_MODEL" "$OLLAMA_MODEL" + + update_env "AI_PROVIDER" "ollama" + echo "✓ Configured Ollama at $OLLAMA_ENDPOINT" + echo " Note: Claude API key is NOT required for Ollama mode" + ;; + 2) + echo "" + read -sp "Claude API Key: " CLAUDE_API_KEY + echo "" + if [ -n "$CLAUDE_API_KEY" ]; then + update_env "CLAUDE_API_KEY" "$CLAUDE_API_KEY" + update_env "AI_PROVIDER" "claude" + echo "✓ Configured Claude API" + fi + ;; + 3) + echo "" + read -sp "OpenAI API Key: " OPENAI_API_KEY + echo "" + if [ -n "$OPENAI_API_KEY" ]; then + update_env "OPENAI_API_KEY" "$OPENAI_API_KEY" + update_env "AI_PROVIDER" "openai" + echo "✓ Configured OpenAI API" + fi + ;; + 4) + echo "✓ Skipping AI provider configuration" + update_env "AI_PROVIDER" "ollama" + echo " Defaulting to Ollama (configure OLLAMA_ENDPOINT in .env)" + ;; +esac + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Security Configuration" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +# Generate secrets if not present or contain REPLACE +echo "Generating security secrets..." + +if ! grep -q "^JWT_SECRET=" .env || grep -q "REPLACE" .env; then + JWT_SECRET=$(openssl rand -base64 32) + update_env "JWT_SECRET" "$JWT_SECRET" + echo "✓ Generated JWT_SECRET" +fi + +if ! grep -q "^ENCRYPTION_KEY=" .env || grep -q "REPLACE" .env; then + ENCRYPTION_KEY=$(openssl rand -hex 32) + update_env "ENCRYPTION_KEY" "$ENCRYPTION_KEY" + echo "✓ Generated ENCRYPTION_KEY" +fi + +if ! grep -q "^BETTER_AUTH_SECRET=" .env || grep -q "REPLACE" .env; then + BETTER_AUTH_SECRET=$(openssl rand -base64 32) + update_env "BETTER_AUTH_SECRET" "$BETTER_AUTH_SECRET" + echo "✓ Generated BETTER_AUTH_SECRET" +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Database Configuration" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +if ! grep -q "^POSTGRES_PASSWORD=" .env || grep -q "REPLACE" .env; then + echo "Generate new PostgreSQL password? (recommended for new installations)" + read -p "[y/N]: " GEN_DB_PASS + if [[ $GEN_DB_PASS =~ ^[Yy]$ ]]; then + POSTGRES_PASSWORD=$(openssl rand -base64 24) + update_env "POSTGRES_PASSWORD" "$POSTGRES_PASSWORD" + # Update DATABASE_URL + update_env "DATABASE_URL" "postgresql://mosaic:${POSTGRES_PASSWORD}@postgres:5432/mosaic" + echo "✓ Generated PostgreSQL password" + else + echo "✓ Keeping existing PostgreSQL password" + fi +else + echo "✓ Using existing PostgreSQL password" +fi + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Traefik Configuration" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" + +echo "Use external Traefik instance? [Y/n]: " +read -p "" USE_EXTERNAL_TRAEFIK +USE_EXTERNAL_TRAEFIK=${USE_EXTERNAL_TRAEFIK:-Y} + +if [[ $USE_EXTERNAL_TRAEFIK =~ ^[Yy]$ ]]; then + update_env "TRAEFIK_MODE" "upstream" + update_env "TRAEFIK_NETWORK" "traefik-public" + echo "✓ Configured for upstream Traefik" +else + update_env "TRAEFIK_MODE" "bundled" + echo "✓ Configured for bundled Traefik" +fi + +echo "" +echo "═══════════════════════════════════════════════════════" +echo " Configuration Complete!" +echo "═══════════════════════════════════════════════════════" +echo "" +echo "Configuration saved to .env" +echo "" +echo "Next steps:" +echo " 1. Review .env file: nano .env" +echo " 2. Deploy stack:" +if [ -f deploy-swarm.sh ]; then + echo " ./deploy-swarm.sh mosaic" +else + echo " docker compose up -d" +fi +echo "" +echo "Services will be available at:" +echo " Web: https://$WEB_DOMAIN" +echo " API: https://$API_DOMAIN" +echo ""