mirror of
https://gh-proxy.org/https://github.com/klinsmaya/multica.git
synced 2026-11-08 01:16:45 +08:00
router.go reads MULTICA_WECOM_DEFAULT_LOCALE to pick the language the bot writes its own copy in for readers it cannot look a language up for (a group chat, anyone not linked to a Multica account). Neither .env.example nor docker-compose.selfhost.yml named it, so a self-hosted operator had no way to find the knob, and setting it on the host did nothing: compose forwards only what its environment: block lists, and the deployment silently stayed on zh-Hans. Add it to both, and a test that parses router.go for every MULTICA_WECOM_* literal it reads and fails when either file is missing one.
246 lines
13 KiB
YAML
246 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}
|
|
ALLOWED_EMAILS: ${ALLOWED_EMAILS:-}
|
|
ALLOWED_EMAIL_DOMAINS: ${ALLOWED_EMAIL_DOMAINS:-}
|
|
DISABLE_WORKSPACE_CREATION: ${DISABLE_WORKSPACE_CREATION:-}
|
|
WORKSPACE_CREATION_ALLOWED_EMAILS: ${WORKSPACE_CREATION_ALLOWED_EMAILS:-}
|
|
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:-}
|
|
# Which language the bot's own copy is written in for readers whose
|
|
# language cannot be looked up — a group chat, or anyone not linked to a
|
|
# Multica account yet. A linked person reads their own profile language
|
|
# instead. zh-Hans (default) or en; an unrecognised value leaves the
|
|
# default in place and the resolved one is logged at boot.
|
|
MULTICA_WECOM_DEFAULT_LOCALE: ${MULTICA_WECOM_DEFAULT_LOCALE:-}
|
|
# 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:-}
|
|
GOOGLE_ANALYTICS_ID: ${GOOGLE_ANALYTICS_ID:-}
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
pgdata:
|
|
backend_uploads:
|