Files
multica/docker-compose.selfhost.yml
klinsmayaandClaude Fable 5.1 bf16d830f8 chore(zai-beta): add integration changelog, GHCR image workflow and compose env passthrough
zai-beta carries PR #7 (/admin console), #8 (open-connector provider) and
#9 (open-connector admin page) on top of main. Document the merged changes
in CHANGELOG-zai.md, build backend/web images to GHCR on every push to the
branch, and pass PLATFORM_ADMIN_EMAILS and OPEN_CONNECTOR_* through the
self-host compose file so the admin console and connector integration can
be exercised from the published images.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01LXKgPcVW6kdD471tCZ9GFf
2026-10-21 07:10:37 +00:00

245 lines
13 KiB
YAML

# 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
# while any deployment credentials remain weak. See:
# apps/docs/content/docs/self-host-quickstart.mdx
#
# Usage:
# cp .env.example .env
# # Edit .env — generate JWT_SECRET with `openssl rand -hex 32` (required)
# 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}}}}
#
# The published values above are HOST ports; the containers always listen on
# 8080 / 3000 internally, so changing them never needs a rebuild. PORT is the
# variable to edit; BACKEND_PORT, API_PORT and SERVER_PORT are optional aliases
# that override it in that order. Keep this alias order identical to Makefile
# and scripts/local-env.sh. The web dev fallback intentionally omits PORT
# because Next uses that variable for its own frontend listener.
#
# Note that *which source* wins differs per entry point — Compose lets the
# calling environment outrank this file, while make lets the included env file
# outrank the environment. Nothing re-derives the published port from the inputs
# any more: `make selfhost` (via scripts/selfhost-wait.sh) and both installers
# read it back with `docker compose port`, so the health check and the printed
# URL always match what was actually published.
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
DATABASE_SEARCH_WORK_MEM_MB: ${DATABASE_SEARCH_WORK_MEM_MB:-64}
DATABASE_REPLICA_URL: ${DATABASE_REPLICA_URL:-}
DATABASE_REPLICA_MAX_CONNS: ${DATABASE_REPLICA_MAX_CONNS:-}
DATABASE_REPLICA_MIN_CONNS: ${DATABASE_REPLICA_MIN_CONNS:-}
REDIS_URL: ${REDIS_URL:-}
REDIS_CLUSTER_MODE: ${REDIS_CLUSTER_MODE:-false}
PORT: "8080"
MAINTENANCE_PORT: ${MAINTENANCE_PORT:-}
METRICS_ADDR: ${METRICS_ADDR:-}
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET must be set to a strong random value — generate one with 'openssl rand -hex 32'}
FRONTEND_ORIGIN: ${FRONTEND_ORIGIN:-http://localhost:3000}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-}
RESEND_API_KEY: ${RESEND_API_KEY:-}
RESEND_FROM_EMAIL: ${RESEND_FROM_EMAIL:[email protected]}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-25}
SMTP_USERNAME: ${SMTP_USERNAME:-}
SMTP_PASSWORD: ${SMTP_PASSWORD:-}
SMTP_FROM_EMAIL: ${SMTP_FROM_EMAIL:-}
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:3000/auth/callback}
S3_BUCKET: ${S3_BUCKET:-}
S3_REGION: ${S3_REGION:-us-west-2}
AWS_ENDPOINT_URL: ${AWS_ENDPOINT_URL:-}
S3_USE_PATH_STYLE: ${S3_USE_PATH_STYLE:-}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID:-}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:-}
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:3000}
MULTICA_DATABASE_STARTUP_TIMEOUT: ${MULTICA_DATABASE_STARTUP_TIMEOUT:-3m}
MULTICA_DATABASE_CONNECT_TIMEOUT: ${MULTICA_DATABASE_CONNECT_TIMEOUT:-5s}
MULTICA_SHUTDOWN_HOLD_DURATION: ${MULTICA_SHUTDOWN_HOLD_DURATION:-}
MULTICA_RUNTIME_RECONNECT_GRACE: ${MULTICA_RUNTIME_RECONNECT_GRACE:-}
ALLOW_SIGNUP: ${ALLOW_SIGNUP:-true}
PLATFORM_ADMIN_EMAILS: ${PLATFORM_ADMIN_EMAILS:-}
OPEN_CONNECTOR_BASE_URL: ${OPEN_CONNECTOR_BASE_URL:-}
OPEN_CONNECTOR_ADMIN_TOKEN: ${OPEN_CONNECTOR_ADMIN_TOKEN:-}
OPEN_CONNECTOR_MCP_URL: ${OPEN_CONNECTOR_MCP_URL:-}
OPEN_CONNECTOR_STATE_SECRET: ${OPEN_CONNECTOR_STATE_SECRET:-}
OPEN_CONNECTOR_CALLBACK_BASE_URL: ${OPEN_CONNECTOR_CALLBACK_BASE_URL:-}
OPEN_CONNECTOR_OAUTH_SCOPES: ${OPEN_CONNECTOR_OAUTH_SCOPES:-}
ALLOWED_EMAILS: ${ALLOWED_EMAILS:-}
ALLOWED_EMAIL_DOMAINS: ${ALLOWED_EMAIL_DOMAINS:-}
DISABLE_WORKSPACE_CREATION: ${DISABLE_WORKSPACE_CREATION:-}
DO_NOT_TRACK: ${DO_NOT_TRACK:-}
# Managed Cloud stays off for self-hosting. All managed Cloud clients use
# this single URL when it is configured.
MULTICA_CLOUD_URL: ${MULTICA_CLOUD_URL:-}
GITHUB_APP_SLUG: ${GITHUB_APP_SLUG:-}
GITHUB_WEBHOOK_SECRET: ${GITHUB_WEBHOOK_SECRET:-}
# App API credentials. The pull_request webhook is what mirrors a PR
# onto a card; these are what let the server authenticate as the App
# and pull the CI / mergeability snapshot that enriches it, plus
# Settings -> Repositories -> Choose from GitHub. Left unset,
# ghsnapshot.NewClientFromEnv returns a nil client and the enrichment
# degrades off silently - no error and no log line to notice.
# GITHUB_APP_PRIVATE_KEY is a PEM block: double-quote it in .env so
# its newlines survive into the container.
GITHUB_APP_ID: ${GITHUB_APP_ID:-}
GITHUB_APP_PRIVATE_KEY: ${GITHUB_APP_PRIVATE_KEY:-}
# 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:-}
# Surface HTML must use a second, cookie-free browser origin routed to
# this backend. Both settings are optional only in the sense that leaving
# either empty disables plugin surfaces rather than weakening isolation.
MULTICA_PLUGIN_SECRET_KEY: ${MULTICA_PLUGIN_SECRET_KEY:-}
MULTICA_PLUGIN_SURFACE_ORIGIN: ${MULTICA_PLUGIN_SURFACE_ORIGIN:-}
MULTICA_PLUGIN_API_URL: ${MULTICA_PLUGIN_API_URL:-}
MULTICA_PLUGIN_DIR: ${MULTICA_PLUGIN_DIR:-}
# Optional API URL shown in self-host daemon setup commands. Set this
# when the daemon reaches the API through a different URL than the one
# used for public webhook URLs; otherwise MULTICA_PUBLIC_URL is used.
MULTICA_DAEMON_SERVER_URL: ${MULTICA_DAEMON_SERVER_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:-}
# Server-side LLM layer, backing internal helpers such as chat title
# generation. Both the API key and the base URL empty = layer disabled,
# and callers fall back silently rather than failing. MULTICA_LLM_MAX_RETRIES
# is the retry budget: unset = 2, 0 = disabled, 1-5 = an exact ceiling.
# Anything else fails the boot instead of being silently corrected.
# MULTICA_LLM_DISABLE_THINKING asks gateways that honor the field to
# turn model reasoning off; see .env.example for the contract.
MULTICA_LLM_API_KEY: ${MULTICA_LLM_API_KEY:-}
MULTICA_LLM_BASE_URL: ${MULTICA_LLM_BASE_URL:-}
MULTICA_LLM_DEFAULT_MODEL: ${MULTICA_LLM_DEFAULT_MODEL:-}
MULTICA_LLM_MAX_RETRIES: ${MULTICA_LLM_MAX_RETRIES:-}
MULTICA_LLM_DISABLE_THINKING: ${MULTICA_LLM_DISABLE_THINKING:-}
# 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:-}
# Slack bot integration. MULTICA_SLACK_SECRET_KEY is the opt-in: unset =
# integration disabled. It decrypts the per-installation bot/app tokens,
# which are brought by each workspace via OAuth/BYO and stored encrypted
# in the database, so this single deployment-wide key is all the operator
# needs to set here.
MULTICA_SLACK_SECRET_KEY: ${MULTICA_SLACK_SECRET_KEY:-}
# DingTalk bot integration. MULTICA_DINGTALK_SECRET_KEY is the opt-in:
# unset = integration disabled. It encrypts each installation's AppSecret
# at rest, so this single deployment-wide key is all the operator needs
# to set here.
MULTICA_DINGTALK_SECRET_KEY: ${MULTICA_DINGTALK_SECRET_KEY:-}
# Self-hosted Git provider integration (Forgejo / Gitea / GitLab). This
# is a self-host-only feature, so the compose file turns it on by default;
# the managed cloud leaves it unset (off). It still needs a valid
# MULTICA_VCS_SECRET_KEY below to actually work.
MULTICA_VCS_INTEGRATION_ENABLED: ${MULTICA_VCS_INTEGRATION_ENABLED:-true}
# VCS integration at-rest encryption key for token-based providers
MULTICA_VCS_SECRET_KEY: ${MULTICA_VCS_SECRET_KEY:-}
# WeCom smart-bot integration. MULTICA_WECOM_SECRET_KEY is the
# opt-in: unset = integration disabled. It encrypts each installation's
# smart-bot secret at rest. The server needs it to unseal a bot's
# subscribe credentials at connection time, and to seal the secret when
# an installation is created from the WeCom settings tab.
MULTICA_WECOM_SECRET_KEY: ${MULTICA_WECOM_SECRET_KEY:-}
# Comma-separated ranges the inbound media fetcher may dial even though
# they look reserved. Empty (the default) keeps the SSRF guard as strict
# as it ships; set it only behind a fake-IP proxy whose pool would
# otherwise get every attachment refused. See .env.example.
MULTICA_WECOM_MEDIA_ALLOW_CIDRS: ${MULTICA_WECOM_MEDIA_ALLOW_CIDRS:-}
# 1 = log every inbound and outbound WeCom frame, including the first
# 120 runes of each message body, so a real-device session can be
# checked against the server afterwards. Off unless set; turn it on only
# for a debugging session and unset it when that session ends, because
# what it records is user message content.
MULTICA_WECOM_TRACE: ${MULTICA_WECOM_TRACE:-}
# Telegram bot integration. MULTICA_TELEGRAM_SECRET_KEY is the
# opt-in: unset = integration disabled. It decrypts the
# per-installation bot token stored in channel_installation.config
# (bot_token_encrypted), so this single deployment-wide key is all
# the operator needs to set here.
MULTICA_TELEGRAM_SECRET_KEY: ${MULTICA_TELEGRAM_SECRET_KEY:-}
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"
REMOTE_API_URL: ${REMOTE_API_URL:-http://backend:8080}
DOCS_URL: ${DOCS_URL:-}
NEXT_PUBLIC_API_URL: ${NEXT_PUBLIC_API_URL:-}
NEXT_PUBLIC_WS_URL: ${NEXT_PUBLIC_WS_URL:-}
restart: unless-stopped
volumes:
pgdata:
backend_uploads: