# ═══════════════════════════════════════════════════════════ # MicProtocol - Full Stack Docker Compose # Production-ready orchestration # ═══════════════════════════════════════════════════════════ version: "3.8" services: # ─── PostgreSQL Database (pgvector — required by Chatwoot + Formbricks) postgres: image: pgvector/pgvector:pg16 container_name: micprotocol-postgres restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER:-micprotocol} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-micprotocol_secret} POSTGRES_DB: ${POSTGRES_DB:-micprotocol} volumes: - postgres_data:/var/lib/postgresql/data - ./scripts/init-databases.sql:/docker-entrypoint-initdb.d/init-databases.sql ports: - "5432:5432" healthcheck: test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-micprotocol}" ] interval: 10s timeout: 5s retries: 5 networks: - micprotocol-network # ─── Redis (Caching & Job Queue) ───────────────────────── redis: image: redis:7-alpine container_name: micprotocol-redis restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD:-redis_secret} volumes: - redis_data:/data ports: - "6379:6379" healthcheck: test: [ "CMD", "redis-cli", "ping" ] interval: 10s timeout: 3s retries: 5 networks: - micprotocol-network # ─── Meilisearch (Search Engine) ───────────────────────── meilisearch: image: getmeili/meilisearch:v1.6 container_name: micprotocol-meilisearch restart: unless-stopped environment: MEILI_MASTER_KEY: ${MEILISEARCH_API_KEY:-masterKey} MEILI_NO_ANALYTICS: "true" volumes: - meilisearch_data:/meili_data ports: - "7700:7700" networks: - micprotocol-network # ─── API Server ────────────────────────────────────────── api: build: context: . dockerfile: apps/api/Dockerfile container_name: micprotocol-api restart: unless-stopped environment: NODE_ENV: production API_PORT: 4000 DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/${POSTGRES_DB:-micprotocol}?schema=public REDIS_HOST: redis REDIS_PORT: 6379 REDIS_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_secret} MEILISEARCH_HOST: http://meilisearch:7700 CLERK_SECRET_KEY: ${CLERK_SECRET_KEY} CLERK_PUBLISHABLE_KEY: ${CLERK_PUBLISHABLE_KEY} WEB_URL: ${WEB_URL:-http://localhost:3000} API_URL: ${API_URL:-http://localhost:4000} MIC_REALTIME_URL: http://mic-realtime:4001 MIC_REALTIME_INTERNAL_TOKEN: ${MIC_REALTIME_INTERNAL_TOKEN} INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-${MIC_REALTIME_INTERNAL_TOKEN}} MIC_REALTIME_SHARED_SECRET: ${MIC_REALTIME_SHARED_SECRET} COOKIE_SECRET: ${COOKIE_SECRET:-cookie_secret_change_in_production} # Email RESEND_API_KEY: ${RESEND_API_KEY} # Storage R2_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID} R2_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY} R2_ENDPOINT: ${R2_ENDPOINT} R2_BUCKET: ${R2_BUCKET} # AI NVIDIA_NIM_API_KEY: ${NVIDIA_NIM_API_KEY} DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} MOONSHOT_API_KEY: ${MOONSHOT_API_KEY} # Self-hosted integration URLs (Docker network) CALCOM_URL: http://calcom:3000 CHATWOOT_URL: http://chatwoot-web:3000 DOCUMENSO_URL: http://documenso:3000 FORMBRICKS_URL: http://formbricks:3000 LISTMONK_URL: http://listmonk:9000 UMAMI_URL: http://umami:3000 LIVEKIT_HOST: http://livekit:7880 LIVEKIT_URL: http://livekit:7880 PRETIX_URL: http://pretix:8000 STRIPE_SECRET_KEY: ${STRIPE_SECRET_KEY} ports: - "4000:4000" depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: [ "CMD", "wget", "-q", "--spider", "http://localhost:4000/health" ] interval: 30s timeout: 10s retries: 3 start_period: 40s networks: - micprotocol-network deploy: resources: limits: cpus: "2" memory: 2G reservations: cpus: "0.5" memory: 512M # ─── Background Workers ────────────────────────────────── # Same image/entrypoint as `api`, published without ports: a plain # second instance of the app whose role is queue processing. main.js # registers the BullMQ WorkerHosts, including the clip-scoring # processor which spawns the Python pipeline via execFile — see # apps/api/src/queue/clip-scoring.processor.ts. The Python ML stack # baked into this image is what actually serves CLIP scoring. The # previous `npx bullmq` command never worked (the package ships no # CLI). worker: build: context: . dockerfile: apps/api/Dockerfile container_name: micprotocol-worker restart: unless-stopped command: node /app/apps/api/dist/main.js ipc: host environment: NODE_ENV: production DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/${POSTGRES_DB:-micprotocol}?schema=public REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_secret} MEILISEARCH_HOST: http://meilisearch:7700 MEILISEARCH_URL: http://meilisearch:7700 NVIDIA_NIM_API_KEY: ${NVIDIA_NIM_API_KEY} DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} MOONSHOT_API_KEY: ${MOONSHOT_API_KEY} RESEND_API_KEY: ${RESEND_API_KEY} depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - micprotocol-network deploy: resources: limits: # Python CLIP/BART inference runs inside this container; the # 1G limit starved torch and got OOM-killed mid-scoring. cpus: "2" memory: 2G # ─── Erlang/OTP Real-time Collaboration Engine ───────── # Owns chat fanout, presence, live domain events. The NestJS API # publishes into it via /internal/dispatch; the web client opens # a WebSocket at /ws?token=... to subscribe to conversations. mic-realtime: build: context: ./services/mic-realtime dockerfile: Dockerfile container_name: micprotocol-mic-realtime restart: unless-stopped environment: MIC_REALTIME_PORT: 4001 MIC_REALTIME_HOST: 0.0.0.0 MICPROTOCOL_API_URL: ${API_URL:-http://api:4000} # No insecure compose fallbacks here on purpose: mic_config.erl # fails closed on the known-insecure default strings, so a # deployment without strong .env values crashes at boot with an # explicit error instead of silently running with default creds. MIC_REALTIME_INTERNAL_TOKEN: ${MIC_REALTIME_INTERNAL_TOKEN} MIC_REALTIME_SHARED_SECRET: ${MIC_REALTIME_SHARED_SECRET} MIC_REALTIME_PUBLIC_URL: ${MIC_REALTIME_PUBLIC_URL:-ws://localhost:4001} MIC_REALTIME_MAX_MSG: 65536 MIC_REALTIME_RATE_LIMIT: 120 MIC_REALTIME_PRESENCE_TTL: 90 REDIS_HOST: redis REDIS_PORT: 6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_secret} ports: - "4001:4001" depends_on: api: condition: service_started redis: condition: service_healthy networks: - micprotocol-network healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:4001/healthz"] interval: 15s timeout: 5s retries: 5 deploy: resources: limits: memory: 1G cpus: "1.5" # ─── Next.js Web App ───────────────────────────────────── web: build: context: . dockerfile: apps/web/Dockerfile container_name: micprotocol-web restart: unless-stopped environment: NODE_ENV: production NEXT_PUBLIC_API_URL: ${API_URL:-http://localhost:4000} NEXT_PUBLIC_MIC_REALTIME_URL: ${MIC_REALTIME_PUBLIC_URL:-ws://localhost:4001} NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${CLERK_PUBLISHABLE_KEY:-pk_test_cGVuZGluZy1tYWNhdy05OC5jbGVyay5hY2NvdW50cy5kZXYk} CLERK_SECRET_KEY: ${CLERK_SECRET_KEY:-sk_test_cGVuZGluZy1tYWNhdy05OC5jbGVyay5hY2NvdW50cy5kZXYk} DEV_AUTH_BYPASS_ENABLED: ${DEV_AUTH_BYPASS_ENABLED:-true} ports: - "3000:3000" depends_on: - api - mic-realtime networks: - micprotocol-network # ─── Admin Web App ─────────────────────────────────────── admin: build: context: . dockerfile: apps/admin/Dockerfile container_name: micprotocol-admin restart: unless-stopped environment: NODE_ENV: production API_URL: ${API_URL:-http://api:4000} NEXT_PUBLIC_API_URL: ${API_URL:-http://api:4000} NEXT_PUBLIC_APP_URL: ${WEB_URL:-http://localhost:3000} ports: - "3006:3006" depends_on: - api networks: - micprotocol-network # ═══════════════════════════════════════════════════════════ # Self-Hosted Integrations (Open-Source SaaS Replacements) # ═══════════════════════════════════════════════════════════ # Each service shares the central Postgres + Redis infra above # (separate DB/schema per service) to keep ops simple. # Port map: 3001=formbricks 3002=calcom 3003=umami 3004=documenso # 3005=chatwoot 3006=admin 9000=listmonk # 9001=minio-s3 9090=minio-console # ─── Cal.com (Scheduling) ──────────────────────────────── calcom: image: calcom/cal.com:latest container_name: micprotocol-calcom restart: unless-stopped ports: - "3002:3000" environment: # Bind to all interfaces so other containers can reach Cal.com HOSTNAME: "0.0.0.0" # Cal.com requires its own database (Prisma migrations need a clean schema). DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/calcom?schema=public DATABASE_DIRECT_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/calcom?schema=public NEXTAUTH_SECRET: ${CALCOM_NEXTAUTH_SECRET} CALENDSO_ENCRYPTION_KEY: ${CALCOM_ENCRYPTION_KEY} NEXTAUTH_URL: ${CALCOM_URL:-http://localhost:3002} NEXT_PUBLIC_WEBAPP_URL: ${CALCOM_URL:-http://localhost:3002} NEXT_PUBLIC_API_V2_URL: ${CALCOM_API_URL:-http://localhost:3002/api/v2} # Telemetry off for self-hosted CALCOM_TELEMETRY_DISABLED: "1" # Email EMAIL_FROM: ${EMAIL_FROM:-noreply@micprotocol.local} EMAIL_SERVER_HOST: ${SMTP_HOST:-mailhog} EMAIL_SERVER_PORT: ${SMTP_PORT:-1025} EMAIL_SERVER_USER: ${SMTP_USER:-mailhog_user} EMAIL_SERVER_PASSWORD: ${SMTP_PASSWORD:-mailhog_password} # S3 storage (MinIO) for Cal.com uploads S3_ACCESS_KEY: ${MINIO_ROOT_USER:-micprotocol_admin} S3_SECRET_KEY: ${MINIO_ROOT_PASSWORD:-micprotocol_secret} S3_REGION: us-east-1 S3_BUCKET: calcom-uploads S3_ENDPOINT: http://minio:9000 S3_FORCE_PATH_STYLE: "true" # Redis for Cal.com v2 API/background jobs REDIS_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 # Disable paid org features NEXT_PUBLIC_SINGLE_ORG_SLUG: ${CALCOM_SINGLE_ORG_SLUG:-micprotocol} ORGANIZATIONS_ENABLED: "0" depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] interval: 30s timeout: 10s retries: 5 start_period: 40s networks: - micprotocol-network # ─── Listmonk (Newsletter / Email Marketing) ───────────── listmonk: image: listmonk/listmonk:latest container_name: micprotocol-listmonk restart: unless-stopped ports: - "9000:9000" # Run install + upgrade + serve on first boot. Idempotent. command: > sh -c "./listmonk --install --idempotent --yes --config '' && ./listmonk --upgrade --yes --config '' && ./listmonk --config ''" environment: LISTMONK_app__address: "0.0.0.0:9000" LISTMONK_db__host: postgres LISTMONK_db__port: 5432 LISTMONK_db__user: ${POSTGRES_USER:-micprotocol} LISTMONK_db__password: ${POSTGRES_PASSWORD:-micprotocol_secret} LISTMONK_db__database: listmonk LISTMONK_db__ssl_mode: disable LISTMONK_db__max_open: 25 LISTMONK_db__max_idle: 25 LISTMONK_db__max_lifetime: 300s # Optional: bootstrap super-admin on first run LISTMONK_ADMIN_USER: ${LISTMONK_ADMIN_USER:-admin} LISTMONK_ADMIN_PASSWORD: ${LISTMONK_ADMIN_PASSWORD:-admin_secret} TZ: Etc/UTC volumes: - listmonk_uploads:/listmonk/uploads:rw depends_on: postgres: condition: service_healthy healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://localhost:9000/health"] interval: 30s timeout: 10s retries: 5 start_period: 30s networks: - micprotocol-network # ─── Formbricks (Surveys / Customer Feedback) ──────────── # v4 architecture: web app + Hub API + Cube (analytics) + migrate job formbricks-migrate: image: ghcr.io/formbricks/formbricks:latest container_name: micprotocol-formbricks-migrate restart: "no" entrypoint: ["sh", "-c"] command: [ 'node packages/database/dist/scripts/apply-migrations.js', ] environment: DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/formbricks?schema=public NEXTAUTH_SECRET: ${FORMBRICKS_NEXTAUTH_SECRET} ENCRYPTION_KEY: ${FORMBRICKS_ENCRYPTION_KEY} depends_on: postgres: condition: service_healthy networks: - micprotocol-network formbricks: image: ghcr.io/formbricks/formbricks:latest container_name: micprotocol-formbricks restart: unless-stopped ports: - "3001:3000" environment: # Bind to all interfaces so other containers can reach Formbricks HOSTNAME: "0.0.0.0" WEBAPP_URL: ${FORMBRICKS_URL:-http://localhost:3001} NEXTAUTH_URL: ${FORMBRICKS_URL:-http://localhost:3001} DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/formbricks?schema=public NEXTAUTH_SECRET: ${FORMBRICKS_NEXTAUTH_SECRET} ENCRYPTION_KEY: ${FORMBRICKS_ENCRYPTION_KEY} CRON_SECRET: ${FORMBRICKS_CRON_SECRET} REDIS_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 # Skip in-container migrate (handled by formbricks-migrate above) SKIP_STARTUP_MIGRATION: "true" # Hub/Cube wiring HUB_API_URL: http://formbricks-hub:8080 HUB_API_KEY: ${FORMBRICKS_HUB_API_KEY} CUBEJS_API_URL: http://formbricks-cube:4000 CUBEJS_API_SECRET: ${FORMBRICKS_CUBE_API_SECRET} CUBEJS_JWT_ISSUER: formbricks-web CUBEJS_JWT_AUDIENCE: formbricks-cube # Auth defaults (signup disabled without invite) EMAIL_VERIFICATION_DISABLED: 1 PASSWORD_RESET_DISABLED: 1 # Email MAIL_FROM: ${EMAIL_FROM:-noreply@micprotocol.local} SMTP_HOST: ${SMTP_HOST:-mailhog} SMTP_PORT: ${SMTP_PORT:-1025} SMTP_USER: ${SMTP_USER:-mailhog_user} SMTP_PASSWORD: ${SMTP_PASSWORD:-mailhog_password} # Telemetry TELEMETRY_DISABLED: 1 RATE_LIMITING_DISABLED: 0 depends_on: postgres: condition: service_healthy redis: condition: service_healthy formbricks-migrate: condition: service_completed_successfully formbricks-cube: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"] interval: 30s timeout: 10s retries: 5 start_period: 40s networks: - micprotocol-network formbricks-hub-migrate: image: ghcr.io/formbricks/hub:latest container_name: micprotocol-formbricks-hub-migrate restart: "no" entrypoint: ["sh", "-c"] command: [ '/usr/local/bin/goose -dir /app/migrations postgres "$$DATABASE_URL" up && /usr/local/bin/river migrate-up --database-url "$$DATABASE_URL"', ] environment: DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/formbricks?schema=public depends_on: formbricks-migrate: condition: service_completed_successfully networks: - micprotocol-network formbricks-hub: image: ghcr.io/formbricks/hub:latest container_name: micprotocol-formbricks-hub restart: unless-stopped environment: API_KEY: ${FORMBRICKS_HUB_API_KEY} DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/formbricks?schema=public depends_on: formbricks-hub-migrate: condition: service_completed_successfully postgres: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/health"] interval: 30s timeout: 10s retries: 5 start_period: 30s networks: - micprotocol-network formbricks-cube: image: cubejs/cube:v1.6.6 container_name: micprotocol-formbricks-cube restart: unless-stopped command: ["sh", "-c", "exec cubejs server"] environment: CUBEJS_DB_TYPE: postgres CUBEJS_DB_HOST: postgres CUBEJS_DB_NAME: formbricks CUBEJS_DB_USER: ${POSTGRES_USER:-micprotocol} CUBEJS_DB_PASS: ${POSTGRES_PASSWORD:-micprotocol_secret} CUBEJS_DB_PORT: 5432 CUBEJS_API_SECRET: ${FORMBRICKS_CUBE_API_SECRET} CUBEJS_JWT_ISSUER: formbricks-web CUBEJS_JWT_AUDIENCE: formbricks-cube CUBEJS_DEFAULT_API_SCOPES: meta,data CUBEJS_CACHE_AND_QUEUE_DRIVER: memory depends_on: postgres: condition: service_healthy healthcheck: test: ["CMD", "node", "-e", "require('http').get('http://127.0.0.1:4000/readyz',(r)=>{r.resume();process.exit(r.statusCode===200?0:1)}).on('error',()=>process.exit(1))"] interval: 10s timeout: 5s retries: 12 start_period: 30s networks: - micprotocol-network # ─── Documenso (Document e-signing) ────────────────────── # Requires a signing cert. Generate via instructions in .env.example documenso: image: documenso/documenso:latest container_name: micprotocol-documenso restart: unless-stopped ports: - "3004:3000" environment: # Bind to all interfaces so other containers can reach Documenso HOSTNAME: "0.0.0.0" PORT: 3000 NEXTAUTH_SECRET: ${DOCUMENSO_NEXTAUTH_SECRET} NEXT_PRIVATE_ENCRYPTION_KEY: ${DOCUMENSO_ENCRYPTION_KEY} NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: ${DOCUMENSO_ENCRYPTION_SECONDARY_KEY} NEXT_PUBLIC_WEBAPP_URL: ${DOCUMENSO_URL:-http://localhost:3004} NEXT_PRIVATE_INTERNAL_WEBAPP_URL: http://documenso:3000 NEXT_PRIVATE_DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/documenso?schema=public NEXT_PRIVATE_DIRECT_DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/documenso?schema=public # Uploads via MinIO (S3-compatible) NEXT_PUBLIC_UPLOAD_TRANSPORT: s3 NEXT_PRIVATE_UPLOAD_ENDPOINT: http://minio:9000 NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE: "true" NEXT_PRIVATE_UPLOAD_REGION: us-east-1 NEXT_PRIVATE_UPLOAD_BUCKET: documenso-uploads NEXT_PRIVATE_UPLOAD_ACCESS_KEY_ID: ${MINIO_ROOT_USER:-micprotocol_admin} NEXT_PRIVATE_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD:-micprotocol_secret} # SMTP NEXT_PRIVATE_SMTP_TRANSPORT: smtp NEXT_PRIVATE_SMTP_HOST: ${SMTP_HOST:-mailhog} NEXT_PRIVATE_SMTP_PORT: ${SMTP_PORT:-1025} NEXT_PRIVATE_SMTP_SECURE: "false" NEXT_PRIVATE_SMTP_FROM_NAME: MicProtocol NEXT_PRIVATE_SMTP_FROM_ADDRESS: ${EMAIL_FROM:-noreply@micprotocol.local} # Signing cert (mounted read-only below, generated by host per .env.example) NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH: /opt/documenso/cert.p12 NEXT_PRIVATE_SIGNING_PASSPHRASE: ${DOCUMENSO_SIGNING_PASSPHRASE} # Disable paid signup gates for self-hosted NEXT_PUBLIC_DISABLE_SIGNIN: "false" NEXT_PUBLIC_DISABLE_SIGNUP: "false" volumes: - ./scripts/documenso-cert:/opt/documenso/cert:ro depends_on: postgres: condition: service_healthy networks: - micprotocol-network # ─── Chatwoot (Customer Support / Live Chat) ──────────── # Rails + Sidekiq workers, palette requested separately to keep # things simple by default. chatwoot-web: image: chatwoot/chatwoot:latest container_name: micprotocol-chatwoot-web restart: unless-stopped ports: - "3005:3000" entrypoint: docker/entrypoints/rails.sh command: ["bundle", "exec", "rails", "s", "-p", "3000", "-b", "0.0.0.0"] environment: NODE_ENV: production RAILS_ENV: production INSTALLATION_ENV: docker SECRET_KEY_BASE: ${CHATWOOT_SECRET_KEY_BASE} RAILS_LOG_TO_STDOUT: "true" # Database — chatwoot needs pgvector; see init-databases.sql POSTGRES_HOST: postgres POSTGRES_PORT: 5432 POSTGRES_DATABASE: chatwoot POSTGRES_USERNAME: ${POSTGRES_USER:-micprotocol} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-micprotocol_secret} DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/chatwoot?sslmode=disable # Redis (shared instance, namespaced by Chatwoot internally) REDIS_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_secret} REDIS_SIDEKIQ_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 # Frontend URL FRONTEND_URL: ${CHATWOOT_URL:-http://localhost:3005} # SMTP for outbound mail SMTP_ADDRESS: ${SMTP_HOST:-mailhog} SMTP_PORT: ${SMTP_PORT:-1025} SMTP_USERNAME: ${SMTP_USER:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_DOMAIN: ${SMTP_DOMAIN:-micprotocol.local} MAILER_SENDER_EMAIL: ${EMAIL_FROM:-Chatwoot } # Storage ACTIVE_STORAGE_SERVICE: local RAILS_STORAGE_LOCATION: /app/storage # Disable dev-mode features ENABLE_ACCOUNT_GAME: "false" USE_INBOUND_EMAIL_SERVICE: "false" volumes: - chatwoot_storage:/app/storage depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 5 start_period: 60s networks: - micprotocol-network chatwoot-worker: image: chatwoot/chatwoot:latest container_name: micprotocol-chatwoot-worker restart: unless-stopped command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"] environment: NODE_ENV: production RAILS_ENV: production INSTALLATION_ENV: docker SECRET_KEY_BASE: ${CHATWOOT_SECRET_KEY_BASE} RAILS_LOG_TO_STDOUT: "true" POSTGRES_HOST: postgres POSTGRES_PORT: 5432 POSTGRES_DATABASE: chatwoot POSTGRES_USERNAME: ${POSTGRES_USER:-micprotocol} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-micprotocol_secret} DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/chatwoot?sslmode=disable REDIS_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 REDIS_PASSWORD: ${REDIS_PASSWORD:-redis_secret} REDIS_SIDEKIQ_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 FRONTEND_URL: ${CHATWOOT_URL:-http://localhost:3005} SMTP_ADDRESS: ${SMTP_HOST:-mailhog} SMTP_PORT: ${SMTP_PORT:-1025} SMTP_USERNAME: ${SMTP_USER:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_DOMAIN: ${SMTP_DOMAIN:-micprotocol.local} MAILER_SENDER_EMAIL: ${EMAIL_FROM:-Chatwoot } depends_on: postgres: condition: service_healthy redis: condition: service_healthy healthcheck: test: ["CMD", "bundle", "exec", "sidekiq", "health"] interval: 30s timeout: 10s retries: 5 start_period: 60s networks: - micprotocol-network # ─── Pretix (Self-Hosted Ticketing & Event Registration) ───── # Open-source ticketing platform. Port 8000 inside Docker network. # Pretix uses its own Postgres database + Redis + a separate pretixworker. pretix: image: pretix/pretix:latest container_name: micprotocol-pretix restart: unless-stopped ports: - "8000:8000" environment: PRETIX_DB_BACKEND: postgres PRETIX_DB_NAME: pretix PRETIX_DB_USER: ${POSTGRES_USER:-micprotocol} PRETIX_DB_PASSWORD: ${POSTGRES_PASSWORD:-micprotocol_secret} PRETIX_DB_HOST: postgres PRETIX_DB_PORT: 5432 PRETIX_URL: ${PRETIX_URL:-http://localhost:8000} PRETIX_ADMIN_EMAIL: ${PRETIX_ADMIN_EMAIL:-admin@micprotocol.local} PRETIX_ADMIN_PASSWORD: ${PRETIX_ADMIN_PASSWORD:-admin_secret} PRETIX_ADMIN_NAME: MicProtocol Admin PRETIX_INSTANCE_NAME: MicProtocol Ticketing PRETIX_INSTANCE_SLUG: micprotocol PRETIX_SECRET: ${PRETIX_SECRET} PRETIX_EMAIL_BACKEND: django.core.mail.backends.console.EmailBackend # Pretix needs Redis for caching PRETIX_REDIS_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - micprotocol-network pretixworker: image: pretix/pretix:latest container_name: micprotocol-pretixworker restart: unless-stopped command: pretixworker environment: PRETIX_DB_BACKEND: postgres PRETIX_DB_NAME: pretix PRETIX_DB_USER: ${POSTGRES_USER:-micprotocol} PRETIX_DB_PASSWORD: ${POSTGRES_PASSWORD:-micprotocol_secret} PRETIX_DB_HOST: postgres PRETIX_DB_PORT: 5432 PRETIX_URL: ${PRETIX_URL:-http://localhost:8000} PRETIX_SECRET: ${PRETIX_SECRET} PRETIX_REDIS_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 depends_on: postgres: condition: service_healthy redis: condition: service_healthy pretix: condition: service_healthy networks: - micprotocol-network # ─── MinIO (S3-compatible storage for all integrations) ── minio: image: minio/minio:latest container_name: micprotocol-minio restart: unless-stopped ports: - "9001:9000" - "9090:9090" environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER:-micprotocol_admin} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-micprotocol_secret} command: server /data --console-address ":9090" volumes: - minio_data:/data networks: - micprotocol-network # One-shot bucket creator for all S3 consumers minio-createbuckets: image: minio/mc:latest container_name: micprotocol-minio-mc depends_on: - minio restart: "no" entrypoint: > /bin/sh -c " /usr/bin/mc alias set myminio http://minio:9000 ${MINIO_ROOT_USER:-micprotocol_admin} ${MINIO_ROOT_PASSWORD:-micprotocol_secret}; /usr/bin/mc mb myminio/micprotocol-storage --ignore-existing; /usr/bin/mc mb myminio/calcom-uploads --ignore-existing; /usr/bin/mc mb myminio/documenso-uploads --ignore-existing; /usr/bin/mc mb myminio/formbricks-uploads --ignore-existing; /usr/bin/mc anonymous set public myminio/micprotocol-storage; exit 0; " networks: - micprotocol-network # ─── Umami (Self-hosted web analytics) ─────────────────── umami: image: ghcr.io/umami-software/umami:postgresql-latest container_name: micprotocol-umami restart: unless-stopped ports: - "3003:3000" environment: # Bind to all interfaces so other containers can reach Umami HOSTNAME: "0.0.0.0" DATABASE_URL: postgresql://${POSTGRES_USER:-micprotocol}:${POSTGRES_PASSWORD:-micprotocol_secret}@postgres:5432/umami?schema=public DATABASE_TYPE: postgresql APP_SECRET: ${UMAMI_APP_SECRET} depends_on: postgres: condition: service_healthy networks: - micprotocol-network # ─── LiveKit (WebRTC SFU for recording studio) ────────── # Provides real-time multi-participant audio rooms. # Ports: 7880 (HTTP), 7881 (TCP), 7882 (UDP), 7883 (TURN/TLS) livekit: image: livekit/livekit-server:latest container_name: micprotocol-livekit restart: unless-stopped ports: - "7880:7880" - "7881:7881" - "7882:7882/udp" command: - --config - /etc/livekit.yaml volumes: - ./services/livekit/livekit.yaml:/etc/livekit.yaml:ro networks: - micprotocol-network # ─── coturn (TURN server for LiveKit NAT traversal) ─────── # Provides TURN/STUN relay for WebRTC participants behind symmetric NAT. # LiveKit autodiscords TURN via the coturn REST API / livekit config. coturn: image: coturn/coturn:4.6.2 container_name: micprotocol-coturn restart: unless-stopped command: > sh -c " turnserver -v --listening-port=3478 --tls-listening-port=5349 --min-port=49152 --max-port=65535 --use-auth-secret --static-auth-secret=${COTURN_SECRET:-coturn_secret} --realm=${ROOT_DOMAIN:-localhost} --no-cli --no-stdout-log --syslog " environment: COTURN_SECRET: ${COTURN_SECRET:-coturn_secret} ROOT_DOMAIN: ${ROOT_DOMAIN:-localhost} ports: - "3478:3478/tcp" - "3478:3478/udp" - "5349:5349/tcp" - "5349:5349/udp" - "49152-65535:49152-65535/udp" networks: - micprotocol-network # ─── Mailhog (Local SMTP catch-all for all services) ───── # Replaces paid SMTP services in dev. Swagger UI at :8025. # In production, point SMTP_HOST/PORT to your real relay. mailhog: image: mailhog/mailhog:latest container_name: micprotocol-mailhog restart: unless-stopped ports: - "1025:1025" - "8025:8025" networks: - micprotocol-network # ─── Volumes ─────────────────────────────────────────────── volumes: postgres_data: driver: local redis_data: driver: local meilisearch_data: driver: local minio_data: driver: local listmonk_uploads: driver: local chatwoot_storage: driver: local # ─── Networks ────────────────────────────────────────────── networks: micprotocol-network: driver: bridge