# Self-hosting Docker Compose — starts PostgreSQL, backend, and frontend. # # Services bind to 127.0.0.1 only. For cross-machine or public access, front # them with a reverse proxy (Caddy / nginx / Cloudflare Tunnel) that terminates # TLS and forwards to 127.0.0.1:8080 (backend) and 127.0.0.1:3000 (frontend). # Do NOT change these bindings to 0.0.0.0 — Docker bypasses host firewalls # (UFW/iptables) by default, so the raw ports would be exposed to the internet # with the default JWT_SECRET and Postgres credentials. See: # apps/docs/content/docs/self-host-quickstart.mdx # # Usage: # cp .env.example .env # # Edit .env — change JWT_SECRET at minimum # docker compose -f docker-compose.selfhost.yml up -d # # Frontend: http://localhost:${FRONTEND_PORT:-3000} # Backend: http://localhost:${BACKEND_PORT:-${API_PORT:-${SERVER_PORT:-${PORT:-8080}}}} name: multica services: postgres: image: pgvector/pgvector:pg17 environment: POSTGRES_DB: ${POSTGRES_DB:-multica} POSTGRES_USER: ${POSTGRES_USER:-multica} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-multica} volumes: - pgdata:/var/lib/postgresql/data restart: unless-stopped healthcheck: test: [ "CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-multica} -d ${POSTGRES_DB:-multica}", ] interval: 5s timeout: 5s retries: 5 backend: image: ${MULTICA_BACKEND_IMAGE:-ghcr.io/multica-ai/multica-backend}:${MULTICA_IMAGE_TAG:-latest} depends_on: postgres: condition: service_healthy ports: - "127.0.0.1:${BACKEND_PORT:-${API_PORT:-${SERVER_PORT:-${PORT:-8080}}}}:8080" volumes: - backend_uploads:/app/data/uploads environment: DATABASE_URL: postgres://${POSTGRES_USER:-multica}:${POSTGRES_PASSWORD:-multica}@postgres:5432/${POSTGRES_DB:-multica}?sslmode=disable PORT: "8080" METRICS_ADDR: ${METRICS_ADDR:-} JWT_SECRET: ${JWT_SECRET:-change-me-in-production} FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:-http://localhost:${FRONTEND_PORT:-3000}} CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-} RESEND_API_KEY: ${RESEND_API_KEY:-} RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:-noreply@multica.ai} SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-25} SMTP_USERNAME: ${SMTP_USERNAME:-} SMTP_PASSWORD: ${SMTP_PASSWORD:-} SMTP_TLS: ${SMTP_TLS:-} SMTP_TLS_INSECURE: ${SMTP_TLS_INSECURE:-false} SMTP_EHLO_NAME: ${SMTP_EHLO_NAME:-} GOOGLE_CLIENT_ID: ${GOOGLE_CLIENT_ID:-} GOOGLE_CLIENT_SECRET: ${GOOGLE_CLIENT_SECRET:-} GOOGLE_REDIRECT_URI: ${GOOGLE_REDIRECT_URI:-http://localhost:${FRONTEND_PORT:-3000}/auth/callback} S3_BUCKET: ${S3_BUCKET:-} S3_REGION: ${S3_REGION:-us-west-2} AWS_ENDPOINT_URL: ${AWS_ENDPOINT_URL:-} ATTACHMENT_DOWNLOAD_MODE: ${ATTACHMENT_DOWNLOAD_MODE:-auto} ATTACHMENT_DOWNLOAD_URL_TTL: ${ATTACHMENT_DOWNLOAD_URL_TTL:-30m} CLOUDFRONT_DOMAIN: ${CLOUDFRONT_DOMAIN:-} CLOUDFRONT_KEY_PAIR_ID: ${CLOUDFRONT_KEY_PAIR_ID:-} CLOUDFRONT_PRIVATE_KEY: ${CLOUDFRONT_PRIVATE_KEY:-} COOKIE_DOMAIN: ${COOKIE_DOMAIN:-} APP_ENV: ${APP_ENV:-production} MULTICA_DEV_VERIFICATION_CODE: ${MULTICA_DEV_VERIFICATION_CODE:-} MULTICA_APP_URL: ${MULTICA_APP_URL:-http://localhost:${FRONTEND_PORT:-3000}} ALLOW_SIGNUP: ${ALLOW_SIGNUP:-true} ALLOWED_EMAILS: ${ALLOWED_EMAILS:-} ALLOWED_EMAIL_DOMAINS: ${ALLOWED_EMAIL_DOMAINS:-} DISABLE_WORKSPACE_CREATION: ${DISABLE_WORKSPACE_CREATION:-} GITHUB_APP_SLUG: ${GITHUB_APP_SLUG:-} GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:-} # Public URL the API is reachable at from the open internet, no # trailing slash. Used to mint absolute webhook URLs for autopilot # webhook triggers. Leave unset behind a same-origin reverse proxy # (e.g. plain localhost dev); the frontend will compose the URL # from window.origin + webhook_path in that case. Headers are # intentionally NOT used to derive this value, to avoid Host / # X-Forwarded-Host spoofing on misconfigured proxies. MULTICA_PUBLIC_URL: ${MULTICA_PUBLIC_URL:-} # Comma-separated CIDRs whose source IP is allowed to set # X-Forwarded-For / X-Real-IP for the webhook per-IP rate limiter. # Empty default = headers ignored, RemoteAddr used. Set e.g. # "127.0.0.1/32" when running behind a same-host reverse proxy. MULTICA_TRUSTED_PROXIES: ${MULTICA_TRUSTED_PROXIES:-} # Lark / Feishu bot integration. MULTICA_LARK_SECRET_KEY is the # opt-in: unset = integration disabled. Mainland 飞书 and international # Lark are auto-detected per installation and served side by side, so # the two base-URL knobs should normally stay EMPTY. They are optional # deployment-wide overrides that force every installation onto one host # (proxy / mock / single-cloud staging). Upgrading from a setup that # used https://open.larksuite.com here? The server relabels existing # installs to region=lark on first boot, then you can clear them. # See docs/lark-bot-integration. MULTICA_LARK_SECRET_KEY: ${MULTICA_LARK_SECRET_KEY:-} MULTICA_LARK_HTTP_BASE_URL: ${MULTICA_LARK_HTTP_BASE_URL:-} MULTICA_LARK_CALLBACK_BASE_URL: ${MULTICA_LARK_CALLBACK_BASE_URL:-} restart: unless-stopped frontend: image: ${MULTICA_WEB_IMAGE:-ghcr.io/multica-ai/multica-web}:${MULTICA_IMAGE_TAG:-latest} depends_on: - backend ports: - "127.0.0.1:${FRONTEND_PORT:-3000}:3000" environment: HOSTNAME: "0.0.0.0" restart: unless-stopped volumes: pgdata: backend_uploads: