MicProtocol / docs /self-hosted-stack.md
Benaah's picture
Upload folder using huggingface_hub
b17baae verified
|
Raw History Blame Contribute Delete
17.9 kB

Self-Hosted Stack

MicProtocol ships a complete self-hostable integration fleet in docker-compose.yml. Every service replaces a paid SaaS equivalent, giving you maximum data ownership, no per-seat licensing, and zero vendor lock-in.

This doc is the unified operations reference. For per-integration API behavior and webhook side effects, see Third-Party & Self-Hosted Integrations and Webhooks.

Architecture

graph TD
    API[MicProtocol NestJS API :4000] --> POSTGRES[(PostgreSQL pgvector :5432)]
    API --> REDIS[(Redis :6379)]
    API --> MEILI[(Meilisearch :7700)]
    API --> MINIO[(MinIO :9001)]
    API --> MICRT[mic-realtime :4001]

    API -. webhook .-> CALCOM[Cal.com :3002]
    API -. webhook .-> FORMBRICKS[Formbricks stack :3001]
    API -. webhook .-> CHATWOOT[Chatwoot :3005]
    API -. webhook .-> DOCUMENSO[Documenso :3004]
    API -. webhook .-> LISTMONK[Listmonk :9000]
    API -. webhook .-> LIVEKIT[LiveKit :7880]

    CALCOM --> POSTGRES
    FORMBRICKS --> POSTGRES
    CHATWOOT --> POSTGRES
    DOCUMENSO --> POSTGRES
    LISTMONK --> POSTGRES
    UMAMI --> POSTGRES

    CALCOM --> REDIS
    CHATWOOT --> REDIS
    FORMBRICKS --> REDIS

    CALCOM --> MINIO
    DOCUMENSO --> MINIO
    FORMBRICKS --> MINIO

    MAILHOG[Mailhog :8025] -. SMTP .-> ALL[All services]

    style API fill:#e1f5e1
    style MICRT fill:#fff3e0

Service Catalog

Core Infrastructure

Service Container Image Port(s) Database
PostgreSQL micprotocol-postgres pgvector/pgvector:pg16 5432 All databases (via init-databases.sql)
Redis micprotocol-redis redis:7-alpine 6379 n/a (in-memory)
Meilisearch micprotocol-meilisearch getmeili/meilisearch:v1.6 7700 n/a (own volume)
MinIO (S3 storage) micprotocol-minio minio/minio:latest 9001 (API), 9090 (Console) n/a (own volume)
MinIO bucket creator micprotocol-minio-mc minio/mc:latest - (one-shot) n/a
Mailhog (dev SMTP) micprotocol-mailhog mailhog/mailhog:latest 1025 (SMTP), 8025 (UI) n/a

MicProtocol Apps

Service Container Port Notes
API micprotocol-api 4000 tRPC + REST + Swagger at /api/docs
Background worker micprotocol-worker - Same app image as the API (no published ports); runs BullMQ WorkerHosts incl. the Python clip-scoring subprocess
mic-realtime (Erlang/OTP) micprotocol-mic-realtime 4001 Chat fanout + presence
Web micprotocol-web 3000 Next.js 14
Admin micprotocol-admin 3006 Next.js platform admin

Self-Hosted Integrations

Service Container(s) Port Replacement DB schema
Cal.com micprotocol-calcom 3002 Calendly calcom
Formbricks v4 micprotocol-formbricks + -hub + -cube + 2 migrate jobs 3001 (web), 8080 (hub), 4000 (cube) Typeform / SurveyMonkey formbricks (pgvector)
Documenso micprotocol-documenso 3004 DocuSign / PandaDoc documenso
Chatwoot micprotocol-chatwoot-web + -worker 3005 Intercom / Zendesk chatwoot (pgvector)
Listmonk micprotocol-listmonk 9000 Mailchimp / ConvertKit listmonk
Umami micprotocol-umami 3003 Google Analytics / Plausible cloud umami
LiveKit micprotocol-livekit 7880 (HTTP), 7881 (TCP), 7882 (UDP) Twilio Video / Zoom SDK n/a

Formbricks v4 substack

graph LR
    MIGRATE1[migrate job] --> DB[(Postgres formbricks DB)]
    MIGRATE2[hub migrate job] --> DB
    HUB[formbricks-hub :8080] --> DB
    HUB --> CUBE[cube.js :4000]
    CUBE --> DB
    WEB[formbricks :3001] --> HUB
    WEB --> CUBE
    WEB --> DB
    WEB --> REDIS[(Redis)]

Containers in the Formbricks substack:

  1. micprotocol-formbricks-migrate (one-shot, runs Prisma migrations)
  2. micprotocol-formbricks (web app, :3001)
  3. micprotocol-formbricks-hub-migrate (one-shot, runs goose + river migrations)
  4. micprotocol-formbricks-hub (Hub API, :8080)
  5. micprotocol-formbricks-cube (Cube.js analytics, :4000)

Formbricks-specific runbook: formbricks-runbook.md.

Port Map

Fixed ports in docker-compose.yml — change in docker-compose.yml and matching env vars together:

Port Service
3000 Web (also guest portal)
3001 Formbricks
3002 Cal.com
3003 Umami
3004 Documenso
3005 Chatwoot
3006 Admin
4000 API
4001 mic-realtime
5432 Postgres
6379 Redis
7700 Meilisearch
7880 LiveKit HTTP
7881 LiveKit TCP
7882 LiveKit UDP
8025 Mailhog UI
9000 Listmonk
9001 MinIO API
9090 MinIO Console
1025 Mailhog SMTP

Database Layout

init-databases.sql runs on first Postgres boot and creates one database per integration:

Database Owner pgvector pgcrypto
micprotocol (default) MicProtocol API - yes
chatwoot Chatwoot yes yes
formbricks Formbricks yes yes
calcom Cal.com - yes
documenso Documenso - yes
listmonk Listmonk - yes
umami Umami - yes

If a volume already exists, init-databases.sql will not run. To bootstrap missing databases on an existing volume:

docker exec -i micprotocol-postgres psql -U micprotocol -d micprotocol < scripts/init-databases.sql

Shared Object Storage (MinIO)

MinIO is the default storage backend for every self-hosted integration that uploads files. minio-createbuckets is a one-shot service that creates:

Bucket Owner
micprotocol-storage MicProtocol API (media, headshots, clips)
calcom-uploads Cal.com
documenso-uploads Documenso
formbricks-uploads Formbricks

micprotocol-storage is set to public-read (used for guest-facing media). The others are private.

In production, point MicProtocol at Cloudflare R2 instead of MinIO by changing R2_ENDPOINT, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY, R2_BUCKET, and STORAGE_PUBLIC_URL — no code changes required, both speak the S3 API.

Shared SMTP (Mailhog in dev)

Every self-hosted integration sends outbound mail through the same SMTP settings:

SMTP_HOST=mailhog          # in compose network
SMTP_PORT=1025
SMTP_USER=                 # empty for Mailhog
SMTP_PASSWORD=             # empty for Mailhog
EMAIL_FROM=noreply@<yourdomain>

In production, swap Mailhog for your transactional relay (Resend, SES, Postmark, or self-hosted Postal/Haraka) by setting SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, and EMAIL_FROM to the provider's values. The integrations read these env vars directly — no code changes.

Shared Redis (namespaced)

Chatwoot, Formbricks, and Cal.com all share the micprotocol-redis instance. Chatwoot and Cal.com namespace keys internally; Formbricks uses Redis for rate-limiting and Cube.js queue. There is currently no isolation beyond provider namespacing — for high-throughput production, split into one Redis per integration.

Co-located Secrets

Every self-hosted service reads its secrets from environment variables that the API also needs. The complete list lives in .env.example. Critical guidance:

Category Examples Rotation policy
Postgres / Redis passwords POSTGRES_PASSWORD, REDIS_PASSWORD Rotatable; requires service restart
Encryption keys (immutable) FORMBRICKS_ENCRYPTION_KEY, DOCUMENSO_ENCRYPTION_KEY, CALCOM_ENCRYPTION_KEY, NEXTAUTH_SECRET variants Never rotate after first start; old encrypted data becomes unrecoverable
Webhook signatures *_WEBHOOK_SECRET Rotatable; update on both sides simultaneously
Admin / API tokens CALCOM_API_KEY, LISTMONK_API_TOKEN, CHATWOOT_API_KEY Rotatable via the admin UI of each integration
App secrets UMAMI_APP_SECRET, FORMBRICKS_CRON_SECRET Rotatable

Use bash scripts/generate-secrets.sh to produce strong random values for every variable in one shot. See Getting Started → Environment Configuration for details.

Service-Specific Configuration Notes

Cal.com

  • Uses its own calcom database (Prisma migrations need a clean schema).
  • NEXT_PUBLIC_SINGLE_ORG_SLUG=micprotocol and ORGANIZATIONS_ENABLED=0 disable paid multi-org features for self-hosted single-tenant use.
  • Generate an API key in Cal.com Settings → API Keys; set CALCOM_API_KEY in .env.
  • Encrypts DB columns with CALCOM_ENCRYPTION_KEY (24-char hex, never rotate after init).

Formbricks v4

  • Five containers, must start in order: formbricks-migrate → formbricks-hub-migrate → formbricks-cube → formbricks-hub → formbricks. Compose depends_on chains handle this automatically.
  • SKIP_STARTUP_MIGRATION=true prevents the web container from re-migrating (the dedicated migrate job already did it).
  • SKIP_STARTUP_MIGRATION must remain true; running migrations inside the web container will race with the migrate job.
  • Analytics require Cube.js (CUBEJS_API_SECRET must match between web and cube containers).
  • Sign-up is disabled without an invite (EMAIL_VERIFICATION_DISABLED=1).

Documenso

  • Requires a signing certificate. Generate with the commands in scripts/documenso-cert/README.md.
  • Mount the cert directory read-only at /opt/documenso/cert; the app looks for cert.p12 inside.
  • DOCUMENSO_ENCRYPTION_KEY and DOCUMENSO_ENCRYPTION_SECONDARY_KEY are both required and never rotated.

Chatwoot

  • Rails + Sidekiq worker pair (chatwoot-web + chatwoot-worker).
  • Uses the chatwoot database with pgvector (required by Chatwoot itself).
  • Storage is local (RAILS_STORAGE_LOCATION=/app/storage, mounted as chatwoot_storage volume). For larger deployments, switch ACTIVE_STORAGE_SERVICE to S3/MinIO.
  • FRONTEND_URL must match the public URL or the live-chat widget will fail to load.
  • Generate SECRET_KEY_BASE with openssl rand -hex 64.

Listmonk

  • Container runs ./listmonk --install --idempotent --yes && ./listmonk --upgrade --yes && ./listmonk --config '' on first boot — idempotent, safe on restarts.
  • LISTMONK_db__database=listmonk (must match the name in init-databases.sql).
  • API token is generated in Listmonk Admin → Settings → API; set LISTMONK_API_TOKEN in .env.
  • LISTMONK_ADMIN_USER/LISTMONK_ADMIN_PASSWORD bootstrap the super-admin on first run.

Umami

  • Single container, Postgres-backed.
  • UMAMI_APP_SECRET is required; generate with openssl rand -base64 32.
  • Add a website in the Umami UI and copy the website ID to NEXT_PUBLIC_UMAMI_WEBSITE_ID.

LiveKit

  • SFU for the in-app recording studio. MicProtocol owns the browser client (apps/web); LiveKit owns the WebRTC media routing.
  • Dev defaults (devkey / devsecret) work in Compose but must be changed for production.
  • Webhooks from LiveKit back to the API call POST /integrations/webhooks/livekit and trigger RecordingSession state transitions.
  • For internet-facing deployments, expose UDP 7882 and configure rtc.use_external_ip: true (or run behind a TURN server).

mic-realtime

Erlang/OTP service that owns:

  • Chat fanout (per-workspace conversation messages)
  • Presence (who is online in a workspace)
  • Live domain events (published by the NestJS API via /internal/dispatch)

Key env vars:

Var Default Notes
MIC_REALTIME_PORT 4001 HTTP listener
MIC_REALTIME_HOST 0.0.0.0 Bind address
MICPROTOCOL_API_URL http://api:4000 Used by mic-realtime to call back into the API
MIC_REALTIME_INTERNAL_TOKEN (change me) Bearer used by API to publish events
MIC_REALTIME_SHARED_SECRET (change me) JWT signing secret for WS auth tokens
MIC_REALTIME_PUBLIC_URL ws://localhost:4001 Public WS URL handed to web clients
MIC_REALTIME_MAX_MSG 65536 Max message size in bytes
MIC_REALTIME_RATE_LIMIT 120 Messages per minute per connection
MIC_REALTIME_PRESENCE_TTL 90 Presence TTL (seconds)

Both MIC_REALTIME_INTERNAL_TOKEN and MIC_REALTIME_SHARED_SECRET must be strong random strings in production.

Production deployment

mic-realtime ships in every deployment topology:

Topology Where Public route
Local dev docker-compose.yml (service mic-realtime) ws://localhost:4001
Coolify docker-compose.coolify.yml (service mic-realtime, internal :4001) wss://realtime.<root> via Caddy
Render render.yaml (micprotocol-realtime, rootDir: services/mic-realtime) wss://micprotocol-realtime.onrender.com (or custom domain)

The Caddyfile routes realtime.<root> to mic-realtime:4001 (the specific host block wins over the *. wildcard). Caddy handles the WebSocket upgrade transparently on the same route, so one URL serves both the GET /healthz probe and GET /ws?token=....

Secrets are fail-closed: RealtimeClient (apps/api/src/chat/realtime.client.ts) and mic_config.erl both refuse to start in production when MIC_REALTIME_INTERNAL_TOKEN / MIC_REALTIME_SHARED_SECRET are missing or equal to the known defaults. Generate strong values (openssl rand -hex 32) and set MIC_REALTIME_PUBLIC_URL to the public wss:// URL; the web/admin frontends receive it as NEXT_PUBLIC_MIC_REALTIME_URL. The Coolify stack defaults the API→realtime base URL to the internal http://mic-realtime:4001 (MIC_REALTIME_URL), so API→realtime callbacks never leave the Docker network.

⚠️ Render free instances sleep after ~15 min of inactivity and cap at 750 hours/month — a sleeping WebSocket engine drops every live chat/presence socket. Use plan: starter or higher for realtime in production.

Start / Stop / Update

Bring up the full stack

docker compose up -d

Bring up only infra (for local app dev)

docker compose up -d postgres redis meilisearch minio minio-createbuckets

Rolling update a single integration

docker compose pull formbricks
docker compose up -d --no-deps formbricks

Formbricks v4 migrations need the migrate jobs to re-run after a version bump:

docker compose up formbricks-migrate
docker compose up formbricks-hub-migrate
docker compose up -d --no-deps formbricks formbricks-hub formbricks-cube

Tear down

docker compose down            # keep data
docker compose down -v         # wipe all volumes (destructive)

Health Endpoints

Service Endpoint
API GET http://localhost:4000/health
Cal.com GET http://localhost:3002/api/health
Formbricks web GET http://localhost:3001/api/health
Formbricks Hub GET http://localhost:8080/health
Formbricks Cube GET http://localhost:4000/formbricks/readyz (via cube container)
Chatwoot GET http://localhost:3005/health
Listmonk GET http://localhost:9000/health
Documenso GET http://localhost:3004/api/health
Umami GET http://localhost:3003/api/health
MinIO GET http://localhost:9001/minio/health/live
Meilisearch GET http://localhost:7700/health
mic-realtime GET http://localhost:4001/healthz
LiveKit GET http://localhost:7880/health (HTTP only; UDP port 7882 for WebRTC)
Mailhog UI at :8025 always returns 200

The API exposes a consolidated integration health check at:

curl -s http://localhost:4000/integrations/health | jq

This pings each self-hosted service's health endpoint in parallel and reports reachability.

Integration Management Dashboard

The Admin panel (apps/admin) provides a full management interface for all self-hosted integrations:

Feature Description
Health Monitoring Real-time health status, latency, and version for Chatwoot, Cal.com, Formbricks, Listmonk, Documenso, Umami, and LiveKit
Webhook Secret Management View and rotate shared webhook secrets directly from the admin panel
Admin Panel Links Direct "Open Admin" buttons that open each integration's native admin UI in a new tab
Credential Management Connect, update, or disconnect API keys for self-hosted integrations
Webhook Delivery Log Recent webhook deliveries with status, latency, and error details
Provider Metrics Per-provider request counts, average latency, and error rates

Access these features from the Monitoring and Integrations tabs in the admin navigation sidebar.

Resource Limits

Default deploy.resources.limits in docker-compose.yml:

Service CPUs Memory
API 2 2G
Worker 2 2G
mic-realtime 1.5 1G

Other services have no explicit limits — tune for your hardware. CLIP scoring (Python ML subprocess) runs inside the worker and is by far the most CPU- and memory-hungry: see CLIP Scoring Engine for scale guidance.

Alternatives if You Skip Self-Hosting

If you don't want to run a particular self-hosted service, simply remove its block from docker-compose.yml and omit its env vars from .env. The MicProtocol API degrades gracefully — each integration's API client is called only when its corresponding URL env var is set. Missing integrations return false from the health check and the API logs a warning rather than 500-ing.

For the externally-hosted (non-self-hosted) integrations (Clerk, Resend, Cloudflare R2, NVIDIA NIM, DeepSeek, Moonshot, Stripe, Paystack, M-Pesa, Africa's Talking, Twilio, Google/Outlook calendars), see Third-Party & Self-Hosted Integrations.