# ═══════════════════════════════════════════════════════════ # MicProtocol – Coolify Production Stack # Deploy via Coolify Docker Compose resource. # Coolify auto-builds API/Web/Admin from source on git push. # Coolify proxy handles SSL + domain binding for all services. # ═══════════════════════════════════════════════════════════ services: # ─── Database ───────────────────────────────────────────── postgres: image: pgvector/pgvector:pg16 restart: unless-stopped volumes: - postgres_data:/var/lib/postgresql/data - ./scripts/init-databases.sql:/docker-entrypoint-initdb.d/init-databases.sql environment: POSTGRES_USER: ${POSTGRES_USER:-micprotocol} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-micprotocol_secret} POSTGRES_DB: ${POSTGRES_DB:-micprotocol} healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-micprotocol}"] interval: 10s timeout: 5s retries: 5 networks: - micprotocol # ─── Cache & Queues ─────────────────────────────────────── redis: image: redis:7-alpine restart: unless-stopped command: redis-server --requirepass ${REDIS_PASSWORD:-redis_secret} volumes: - redis_data:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 3s retries: 5 networks: - micprotocol # ─── Search Engine (embedded in API container) ─────────── # Meilisearch runs inside the API container (see apps/api/Dockerfile). # No separate service needed. # ─── API (NestJS + tRPC) ───────────────────────────────── api: build: context: . dockerfile: apps/api/Dockerfile 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_PASSWORD: ${REDIS_PASSWORD:-redis_secret} MEILISEARCH_HOST: http://localhost:7700 WEB_URL: ${WEB_URL:-https://micprotocol-web.local} CLERK_SECRET_KEY: ${CLERK_SECRET_KEY} RESEND_API_KEY: ${RESEND_API_KEY} R2_ACCESS_KEY_ID: ${R2_ACCESS_KEY_ID} R2_SECRET_ACCESS_KEY: ${R2_SECRET_ACCESS_KEY} R2_ENDPOINT: ${R2_ENDPOINT} R2_BUCKET: ${R2_BUCKET} LIVEKIT_API_KEY: ${LIVEKIT_API_KEY:-devkey} LIVEKIT_API_SECRET: ${LIVEKIT_API_SECRET:-devsecret} LIVEKIT_HOST: ${LIVEKIT_HOST:-http://livekit:7880} LISTMONK_URL: ${LISTMONK_URL:-http://listmonk:9000} LISTMONK_API_TOKEN: ${LISTMONK_API_TOKEN} NVIDIA_NIM_API_KEY: ${NVIDIA_NIM_API_KEY} DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} MOONSHOT_API_KEY: ${MOONSHOT_API_KEY} COOKIE_SECRET: ${COOKIE_SECRET:-cookie_secret_change_in_production} # Real-time collaboration engine (Erlang) — see mic-realtime service below. # MIC_REALTIME_URL is the in-network base URL the API uses to publish # events + issue WS tokens. The two secrets MUST be strong random values # in production (the API fails to boot otherwise — RealtimeClient). MIC_REALTIME_URL: ${MIC_REALTIME_URL:-http://mic-realtime:4001} MIC_REALTIME_INTERNAL_TOKEN: ${MIC_REALTIME_INTERNAL_TOKEN} MIC_REALTIME_SHARED_SECRET: ${MIC_REALTIME_SHARED_SECRET} SENTRY_DSN: ${SENTRY_DSN} depends_on: postgres: condition: service_healthy redis: condition: service_healthy mic-realtime: condition: service_started networks: - micprotocol # ─── Background Workers (BullMQ) ────────────────────────── worker: build: context: . dockerfile: apps/api/Dockerfile restart: unless-stopped command: npx bullmq-pro 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} RESEND_API_KEY: ${RESEND_API_KEY} NVIDIA_NIM_API_KEY: ${NVIDIA_NIM_API_KEY} DEEPSEEK_API_KEY: ${DEEPSEEK_API_KEY} MOONSHOT_API_KEY: ${MOONSHOT_API_KEY} depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - micprotocol # ─── 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. # Exposed to the internet by Caddy on realtime. (wss://). mic-realtime: build: context: ./services/mic-realtime dockerfile: Dockerfile restart: unless-stopped environment: MIC_REALTIME_PORT: 4001 MIC_REALTIME_HOST: 0.0.0.0 # Internal in-network API URL (NOT the public NEXT_PUBLIC_API_URL) # so realtime→API callbacks stay on the Docker network. MICPROTOCOL_API_URL: ${API_URL:-http://api:4000} # Both secrets MUST be strong random values in production # (the service fails closed at startup otherwise). MIC_REALTIME_INTERNAL_TOKEN: ${MIC_REALTIME_INTERNAL_TOKEN} MIC_REALTIME_SHARED_SECRET: ${MIC_REALTIME_SHARED_SECRET} MIC_REALTIME_PUBLIC_URL: ${MIC_REALTIME_PUBLIC_URL} MIC_REALTIME_MAX_MSG: 65536 MIC_REALTIME_RATE_LIMIT: 120 MIC_REALTIME_PRESENCE_TTL: 90 depends_on: - api networks: - micprotocol healthcheck: test: ["CMD", "wget", "-qO-", "http://localhost:4001/healthz"] interval: 15s timeout: 5s retries: 5 # ─── Web App (Next.js) ─────────────────────────────────── web: build: context: . dockerfile: apps/web/Dockerfile restart: unless-stopped depends_on: - api environment: NODE_ENV: production NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY} CLERK_SECRET_KEY: ${CLERK_SECRET_KEY} NEXT_PUBLIC_UMAMI_WEBSITE_ID: ${UMAMI_WEBSITE_ID} NEXT_PUBLIC_UMAMI_URL: ${UMAMI_URL} # Must be a public wss:// URL in production (e.g. wss://realtime.) NEXT_PUBLIC_MIC_REALTIME_URL: ${MIC_REALTIME_PUBLIC_URL} networks: - micprotocol # ─── Admin Panel (Next.js) ─────────────────────────────── admin: build: context: . dockerfile: apps/admin/Dockerfile restart: unless-stopped depends_on: - api environment: NODE_ENV: production NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL} API_URL: http://api:4000 # Must be a public wss:// URL in production (e.g. wss://realtime.) NEXT_PUBLIC_MIC_REALTIME_URL: ${MIC_REALTIME_PUBLIC_URL} networks: - micprotocol # ─── Cal.com (Scheduling) ───────────────────────────────── calcom: image: calcom/cal.com:latest restart: unless-stopped environment: 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:-https://calcom.local} NEXT_PUBLIC_WEBAPP_URL: ${CALCOM_URL:-https://calcom.local} CALCOM_TELEMETRY_DISABLED: "1" EMAIL_FROM: ${EMAIL_FROM:-noreply@micprotocol.local} EMAIL_SERVER_HOST: ${SMTP_HOST} EMAIL_SERVER_PORT: ${SMTP_PORT:-587} EMAIL_SERVER_USER: ${SMTP_USER} EMAIL_SERVER_PASSWORD: ${SMTP_PASSWORD} REDIS_URL: redis://:${REDIS_PASSWORD:-redis_secret}@redis:6379 NEXT_PUBLIC_SINGLE_ORG_SLUG: ${CALCOM_SINGLE_ORG_SLUG:-micprotocol} ORGANIZATIONS_ENABLED: "0" depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - micprotocol # ─── Listmonk (Newsletter / Email Marketing) ───────────── listmonk: image: listmonk/listmonk:latest restart: unless-stopped command: > sh -c "./listmonk --install --idempotent --yes && ./listmonk --upgrade --yes && ./listmonk" 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_ADMIN_USER: ${LISTMONK_ADMIN_USER:-admin} LISTMONK_ADMIN_PASSWORD: ${LISTMONK_ADMIN_PASSWORD:-admin_secret} volumes: - listmonk_uploads:/listmonk/uploads:rw depends_on: postgres: condition: service_healthy networks: - micprotocol # ─── Formbricks (Surveys) ───────────────────────────────── formbricks: image: ghcr.io/formbricks/formbricks:latest restart: unless-stopped environment: WEBAPP_URL: ${FORMBRICKS_URL:-https://formbricks.local} NEXTAUTH_URL: ${FORMBRICKS_URL:-https://formbricks.local} 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 EMAIL_VERIFICATION_DISABLED: 1 PASSWORD_RESET_DISABLED: 1 MAIL_FROM: ${EMAIL_FROM:-noreply@micprotocol.local} SMTP_HOST: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT:-587} TELEMETRY_DISABLED: 1 depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - micprotocol # ─── Documenso (Document Signing) ───────────────────────── documenso: image: documenso/documenso:latest restart: unless-stopped environment: 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:-https://documenso.local} 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 NEXT_PUBLIC_UPLOAD_TRANSPORT: r2 NEXT_PRIVATE_SMTP_TRANSPORT: smtp NEXT_PRIVATE_SMTP_HOST: ${SMTP_HOST} NEXT_PRIVATE_SMTP_PORT: ${SMTP_PORT:-587} NEXT_PRIVATE_SMTP_FROM_NAME: MicProtocol NEXT_PRIVATE_SMTP_FROM_ADDRESS: ${EMAIL_FROM:-noreply@micprotocol.local} NEXT_PUBLIC_DISABLE_SIGNUP: "false" NEXT_PUBLIC_DISABLE_SIGNIN: "false" volumes: - documenso_cert:/opt/documenso/cert:ro depends_on: postgres: condition: service_healthy networks: - micprotocol # ─── Chatwoot (Live Chat) ───────────────────────────────── chatwoot: image: chatwoot/chatwoot:latest restart: unless-stopped 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} POSTGRES_HOST: postgres POSTGRES_PORT: 5432 POSTGRES_DATABASE: chatwoot POSTGRES_USERNAME: ${POSTGRES_USER:-micprotocol} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-micprotocol_secret} 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:-https://chatwoot.local} SMTP_ADDRESS: ${SMTP_HOST} SMTP_PORT: ${SMTP_PORT:-587} SMTP_USERNAME: ${SMTP_USER} SMTP_PASSWORD: ${SMTP_PASSWORD} MAILER_SENDER_EMAIL: ${EMAIL_FROM:-Chatwoot } volumes: - chatwoot_storage:/app/storage depends_on: postgres: condition: service_healthy redis: condition: service_healthy networks: - micprotocol # ─── Umami (Analytics) ──────────────────────────────────── umami: image: ghcr.io/umami-software/umami:postgresql-latest restart: unless-stopped environment: 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 # ─── LiveKit (WebRTC SFU) ───────────────────────────────── livekit: image: livekit/livekit-server:latest restart: unless-stopped ports: - "7880:7880" - "7881:7881" - "7882:7882/udp" command: > --config-body " port: 7880 rtc: port_range_start: 7882 port_range_end: 7882 use_external_ip: false keys: ${LIVEKIT_API_KEY:-devkey}: ${LIVEKIT_API_SECRET:-devsecret} webhook: urls: [http://api:4000/integrations/webhooks/livekit] api_key: ${LIVEKIT_API_KEY:-devkey} " networks: - micprotocol # ─── SMTP Relay ───────────────────────────────────────────── # Set SMTP_HOST/SMTP_PORT for integrations that send email directly. # Resend (app-level email) is configured via RESEND_API_KEY. networks: micprotocol: driver: bridge volumes: postgres_data: redis_data: listmonk_uploads: chatwoot_storage: documenso_cert: