Deploy Multica on your own infrastructure in minutes.
| Component | Description | Technology |
|---|---|---|
| Backend | REST API + WebSocket server | Go (single binary) |
| Frontend | Web application | Next.js 16 |
| Database | Primary data store | PostgreSQL 17 (pgcrypto + pg_trgm) |
Each user who runs AI agents locally also installs the multica CLI and runs the agent daemon on their own machine.
Two commands to set up everything — server, CLI, and configuration.
macOS / Linux
# 1. Install CLI + provision the self-host server
curl -fsSL https://raw.githubusercontent.com/hduhelp/multica/main/scripts/install.sh | bash -s -- --with-server
# 2. Configure CLI, authenticate, and start the daemon
multica setup self-hostWindows (PowerShell)
# 1. Install CLI + provision the self-host server
$env:MULTICA_MODE="with-server"; irm https://raw.githubusercontent.com/hduhelp/multica/main/scripts/install.ps1 | iex
# 2. Configure CLI, authenticate, and start the daemon
multica setup self-hostThis installs the multica CLI, checks out the latest self-host assets, pulls the official Multica images from GHCR, and configures everything for localhost.
Open http://localhost:3000. To log in, configure RESEND_API_KEY in .env for email-based codes (recommended), or leave Resend unset and copy the generated code from the backend logs. See Step 2 — Log In for details.
Prerequisites: Docker and Docker Compose must be installed. The script checks for this and provides install links if missing.
CLI only? If the self-host server is already running and you only need the CLI on a macOS/Linux machine, install it with Homebrew:
brew install hduhelp/tap/multica
If you prefer to run each step manually:
Prerequisites: Docker and Docker Compose.
git clone https://github.com/hduhelp/multica.git
cd multica
make selfhostmake selfhost automatically creates .env from the example, generates a random JWT_SECRET, and starts all services via Docker Compose.
By default it pulls the latest stable release images from GHCR. To build the backend/web from your current checkout instead, run make selfhost-build.
If the selected GHCR tag has not been published yet, make selfhost now tells you to fall back to make selfhost-build.
make selfhost-build uses local multica-backend:dev / multica-web:dev tags, so it does not overwrite the pulled :latest images.
Once ready:
- Frontend: http://localhost:3000
- Backend API: http://localhost:8080
Note: If you prefer to run the Docker Compose steps manually, see Manual Docker Compose Setup below.
Open http://localhost:3000 in your browser. The Docker self-host stack defaults to APP_ENV=production (set in docker-compose.selfhost.yml), and there is no fixed verification code by default. Pick one of the following to log in:
- Recommended (production): configure
RESEND_API_KEYin.env, then restart the backend. Real verification codes will be sent to the email address you enter. See Advanced Configuration → Email. - Without email configured: the verification code is generated server-side and printed to the backend container logs (look for
[DEV] Verification code for ...:). Useful for one-off testing on a single machine. - Deterministic local/private testing: set
APP_ENV=developmentandMULTICA_DEV_VERIFICATION_CODE=888888in.env, then restart the backend. This fixed code is ignored whenAPP_ENV=production.
Changes to ALLOW_SIGNUP, DISABLE_WORKSPACE_CREATION, and GOOGLE_CLIENT_ID also take effect after restarting the backend / compose stack. The web UI reads all three from /api/config at runtime, so no web rebuild is needed. See Advanced Configuration → Signup Controls for the recommended sequence to lock down workspace creation.
Warning: do not set
MULTICA_DEV_VERIFICATION_CODEon a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.
The daemon runs on your local machine (not inside Docker). It detects installed AI agent CLIs, registers them with the server, and executes tasks when agents are assigned work.
Each team member who wants to run AI agents locally needs to:
brew install hduhelp/tap/multicaYou also need at least one AI agent CLI installed:
- Claude Code (
claudeon PATH) - Antigravity CLI (
agyon PATH) - CodeBuddy Code (
codebuddyon PATH) - DevEco Code (
devecoon PATH) - Codex (
codexon PATH) - GitHub Copilot CLI (
copiloton PATH) - OpenClaw (
openclawon PATH) - OpenCode (
opencodeon PATH) - Huawei Cloud CodeArts (
codeartson PATH) - Hermes (
hermeson PATH) - Pi (
pion PATH) - Cursor Agent (
cursor-agenton PATH) - Kimi (
kimion PATH) - Reasonix (
reasonixon PATH; runreasonix setupfirst) - Dim (
dimon PATH) - Kiro CLI (
kiro-clion PATH) - Qoder CLI (
qoderclion PATH) - Qoder CN CLI (
qoderclicnon PATH) - Trae CLI (
traeclion PATH) - Grok Build CLI (
grokon PATH) - Qwen Code (
qwenon PATH) - QwenPaw (
qwenpawon PATH; pick its model in QwenPaw's own configuration) - MiniMax Code CLI (
mcode0.1.2+ on PATH). Install a supported Node.js release (>=22.19 <23or>=24 <27), runnpm install --global @minimax-ai/code@latest, thenmcode login. - DeepSeek Harness (
dshon PATH with the Multica runtime profile installed; setDEEPSEEK_API_KEY)
multica setup self-hostThis automatically:
- Configures the CLI to connect to
localhost(ports 8080/3000) - Opens your browser for authentication
- Discovers your workspaces
- Starts the daemon in the background
For on-premise deployments with custom domains:
multica setup self-host --server-url https://api.example.com --app-url https://app.example.comTo verify the daemon is running:
multica daemon statusAlternative: If you prefer manual steps, see Manual CLI Configuration below.
- Open your workspace in the web app at http://localhost:3000
- Navigate to Settings → Runtimes — you should see your machine listed
- Go to Settings → Agents and create a new agent
- Create an issue and assign it to your agent — it will pick up the task automatically
If you already run a Kubernetes cluster, you can deploy Multica there instead of Docker Compose using the released OCI Helm chart at oci://ghcr.io/hduhelp/charts/multica or the source chart at deploy/helm/multica/. It targets a typical k3s / k8s setup with an Ingress controller and a default ReadWriteOnce StorageClass — authored against k3s + Traefik + local-path, and should work on any cluster with minor tweaks.
The chart creates the following resources in the target namespace:
multica-postgres—pgvector/pgvector:pg17backed by a 10Gi PVCmultica-backend— Go API/WS server. Backed by a 5GiReadWriteOnceuploads PVC by default; setbackend.uploads.persistence.enabled=falsewhen you have configured S3 (backend.config.s3Bucket) and don't want the chart to declare the PVC at all.multica-frontend— Next.js standalone server- Two
Ingressresources: one for the web host, one for the backend host multica-configConfigMap (rendered fromvalues.yaml)
The multica-secrets Secret is not managed by the chart — you create it once with kubectl so real values never need to land in git.
Runtime frontend upstreams: current
multica-webimages readREMOTE_API_URLandDOCS_URLwhen the Next.js server runs, so API/docs upstream changes do not require a web rebuild. The chart defaultsREMOTE_API_URLto this release's backend Service.frontend.compatibility.backendAliasexists only for legacy images that still bakedREMOTE_API_URL=http://backend:8080at build time.
Prerequisites:
kubectlandhelm(v3.13+ for--take-ownership, or v4+) configured for the target cluster, an Ingress controller (Traefik / NGINX), and a default StorageClass.
The chart defaults to multica.dev.lan (web) and api.multica.dev.lan (backend). Pick one of:
-
/etc/hostson every machine that needs access (developer laptops + the machine running the daemon):192.168.1.206 multica.dev.lan api.multica.dev.lanReplace
192.168.1.206with any node IP where your Ingress controller's Service is reachable. -
Local DNS (Pi-hole, Unbound, etc.): add A records for both hostnames pointing at the cluster Ingress IP.
To use different hostnames, override the matching values at install time (see Step 4) — ingress.frontend.host, ingress.backend.host, plus backend.config.appUrl, backend.config.frontendOrigin, backend.config.localUploadBaseUrl, and backend.config.googleRedirectUri.
kubectl create namespace multicaThe chart references this Secret by name. Create it once with random values:
kubectl -n multica create secret generic multica-secrets \
--from-literal=JWT_SECRET="$(openssl rand -hex 32)" \
--from-literal=POSTGRES_PASSWORD="$(openssl rand -hex 16)" \
--from-literal=RESEND_API_KEY="" \
--from-literal=GOOGLE_CLIENT_SECRET="" \
--from-literal=CLOUDFRONT_PRIVATE_KEY="" \
--from-literal=MULTICA_DEV_VERIFICATION_CODE=""Leave optional values empty for now — you can fill them in later (see Step 5 — Log In).
helm install multica oci://ghcr.io/hduhelp/charts/multica \
--version <chart-version> \
-n multicaReleased chart versions strip the leading v from the Git tag. For example, release tag v0.3.5 publishes chart version 0.3.5; the chart defaults the backend and frontend image tags to v0.3.5.
To override defaults, export the chart values, edit them, and pass them with -f:
helm show values oci://ghcr.io/hduhelp/charts/multica \
--version <chart-version> > my-values.yaml
# edit my-values.yaml — e.g. change ingress hosts, image tags, resource limits
helm install multica oci://ghcr.io/hduhelp/charts/multica \
--version <chart-version> \
-n multica \
-f my-values.yamlWhen developing from a checkout, use the local chart path instead:
helm install multica deploy/helm/multica -n multicaWatch the pods come up:
kubectl -n multica get pods -wOn a cold cluster the backend can sit Running but not Ready for a few minutes while it waits on PostgreSQL and runs migrations — a startupProbe absorbs this, so the pod should not restart. Once the backend reports Ready, migrations have completed and /healthz returns OK:
curl -H "Host: api.multica.dev.lan" http://<ingress-ip>/healthz
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}Then open http://multica.dev.lan in your browser.
The chart defaults to APP_ENV=production (set in values.yaml under backend.config.appEnv), and there is no fixed verification code by default. Pick one of the following to log in — the same three options as the Docker setup:
-
Recommended (production): patch the Secret with a real Resend key, then restart the backend:
kubectl -n multica patch secret multica-secrets --type=merge \ -p '{"stringData":{"RESEND_API_KEY":"re_xxx"}}' kubectl -n multica rollout restart deploy/multica-backendReal verification codes will be sent to the email address you enter. See Advanced Configuration → Email.
-
Without email configured: the verification code is generated server-side and printed to the backend pod logs (look for
[DEV] Verification code for ...:). Useful for one-off testing.kubectl -n multica logs -f deploy/multica-backend | grep "Verification code"
-
Deterministic local/private testing: set
backend.config.appEnv: developmentin your values file andMULTICA_DEV_VERIFICATION_CODE=888888in the Secret, thenhelm upgradeand restart. This fixed code is ignored whenAPP_ENV=production.helm upgrade multica oci://ghcr.io/hduhelp/charts/multica \ --version <chart-version> \ -n multica \ -f my-values.yaml --set backend.config.appEnv=development kubectl -n multica patch secret multica-secrets --type=merge \ -p '{"stringData":{"MULTICA_DEV_VERIFICATION_CODE":"888888"}}' kubectl -n multica rollout restart deploy/multica-backend
ALLOW_SIGNUP, DISABLE_WORKSPACE_CREATION, and GOOGLE_CLIENT_ID likewise live under backend.config.* in values.yaml (as allowSignup, disableWorkspaceCreation, and googleClientId). After helm upgrade, the backend pod will roll automatically because the ConfigMap hash changes; the web UI reads all three from /api/config at runtime, so no web rebuild is needed.
Warning: do not set
MULTICA_DEV_VERIFICATION_CODEon a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.
The daemon runs on your local machine, not in the cluster. Install the CLI and an AI agent as in Step 3 above, then point the CLI at your Ingress hostnames:
multica setup self-host \
--server-url http://api.multica.dev.lan \
--app-url http://multica.dev.lanMake sure the machine running the daemon has the same /etc/hosts (or DNS) entries from Step 1.
To pull the latest images without changing the chart version when your values still use the mutable latest image tag:
kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontendTo upgrade to a specific Multica release, upgrade to the matching chart version. The released chart defaults its app images to the matching Git tag:
helm upgrade multica oci://ghcr.io/hduhelp/charts/multica \
--version <chart-version> \
-n multica \
-f my-values.yamlIf you need to override the app images independently from the chart version, set the image tags in your values file:
images:
backend:
tag: v0.2.4
frontend:
tag: v0.2.4Then run the same upgrade command with -f my-values.yaml:
helm upgrade multica oci://ghcr.io/hduhelp/charts/multica \
--version <chart-version> \
-n multica \
-f my-values.yamlTo roll back if an upgrade goes sideways:
helm -n multica rollback multicaUpgrading from
v0.3.4tov0.3.5+fails withrefusing to drop legacy daily rollups: ...? As of MUL-2957 themigrate upcommand runs an idempotent monthly-slice backfill automatically before applying migration103, so a clean upgrade is a singlehelm upgrade+ backend rollout. If you are still on a pre-MUL-2957 binary or the auto-hook fails, run the standalone backfill against the same database the chart is using (kubectl -n multica exec deploy/multica-backend -- ./backfill_task_usage_hourly --sleep-between-slices=2s), then restart the backend deployment to re-apply migrations. See Advanced Configuration → Usage Dashboard Rollup for the full recovery flow.
# Remove the workloads but keep the PVCs and the Secret
helm -n multica uninstall multica
# Wipe everything, including PostgreSQL data and uploads
kubectl delete namespace multicaThe Usage / Runtime dashboards read from a derived task_usage_hourly table populated by rollup_task_usage_hourly(). As of MUL-2957 the backend runs this rollup in-process on every replica via a DB-backed scheduler (sys_cron_executions); a fresh self-host install needs no operator action and the bundled pgvector/pgvector:pg17 image works without changes — you do not need to swap it for an image that ships pg_cron, register an external cron job, set up a systemd timer, or run a Kubernetes CronJob.
Multiple backend replicas are safe: each replica ticks every 30 seconds and tries to claim the current 5-minute UTC plan, but the unique key (job_name, scope_kind, scope_id, plan_time) means only one wins each plan. Inspect steady-state operation:
WeCom (企业微信) smart bot and replica count. Unlike Slack and Lark, whose outbound is stateless HTTP that any replica can perform, the WeCom smart bot's only outbound path is an in-process WebSocket long connection, held by the replica that owns that bot's lease. What happens to a reply produced on a different replica depends on the realtime relay:
- Sharded or dual relay mode (
REDIS_URLset, the default with Redis): the reply or inbox push is forwarded to the lease holder over the relay and delivered. Multi-replica WeCom is supported in this mode.- Legacy relay mode, or no Redis: the reply is dropped and the WeCom user sees nothing. Run the WeCom-enabled backend as a single replica in this configuration.
In every mode there is one residual window: a reply produced while no replica holds a live connection to that bot — all of them mid-reconnect — is not delivered. It is counted: the replica that routed it checks afterwards whether any replica ever claimed the delivery, and increments
multica_wecom_outbound_dropped_total{reason="no_live_connection"}when none did, so the window can be measured on a deployment rather than guessed at. If a rare lost reply during reconnects is unacceptable, a single replica remains the most conservative deployment. Everything else (including the rollup scheduler above) is multi-replica safe.
SELECT plan_time, status, attempt, runner_id,
error_code, error_msg, started_at, finished_at
FROM sys_cron_executions
WHERE job_name = 'rollup_task_usage_hourly'
ORDER BY plan_time DESC
LIMIT 20;Full reference (audit table semantics, advisory lock 4246, the standalone backfill command, flag descriptions, the v0.3.4 → v0.3.5+ migration auto-hook) lives in Advanced Configuration → Usage Dashboard Rollup.
Upgrading from
v0.3.4tov0.3.5+? As of MUL-2957 themigrate upcommand runs an idempotent monthly-slice backfill automatically right before applying migration103, so the upgrade completes in a single invocation — no operator step required. If you are still on a pre-MUL-2957 binary or the auto-hook fails for an environmental reason, runbackfill_task_usage_hourlyagainst the same database and re-run the upgrade. See Advanced Configuration → Usage Dashboard Rollup for the recovery flow.
External schedulers — pg_cron registered on the database, an external cron job, a systemd timer, or a Kubernetes CronJob — that call SELECT rollup_task_usage_hourly() directly were the only option before MUL-2957 and remain a supported compatibility path. They are no longer the recommended setup; new deployments should rely on the in-process scheduler instead. The SQL function holds advisory lock 4246 internally, so the in-process scheduler and any pre-existing external schedule can coexist without ever double-writing the rollup.
If you already have a pg_cron job in production, the safe sequence to retire it is:
-
Confirm the in-process scheduler is healthy on at least one backend replica — recent SUCCESS rows should be landing in
sys_cron_executionsforrollup_task_usage_hourly:SELECT plan_time, status, runner_id, finished_at FROM sys_cron_executions WHERE job_name = 'rollup_task_usage_hourly' AND status = 'SUCCESS' ORDER BY plan_time DESC LIMIT 5;
-
Once SUCCESS rows are arriving on schedule, unschedule the redundant
pg_cronentry:SELECT cron.unschedule('rollup_task_usage_hourly') FROM cron.job WHERE jobname = 'rollup_task_usage_hourly';
-
Leave the
pg_cronextension itself installed unless you are sure no other workload depends on it. The bundledpgvector/pgvector:pg17image does not shippg_cron, so nothing in Multica's default install needs it; uninstallingpg_cronfrom a custom image that other workloads still use is a separate decision.
External cron / systemd timer / Kubernetes CronJob setups that call SELECT rollup_task_usage_hourly() directly can be retired the same way — once sys_cron_executions shows steady SUCCESS rows from the in-process scheduler, the external job is redundant and can be removed.
If you installed via the install script:
curl -fsSL https://raw.githubusercontent.com/hduhelp/multica/main/scripts/install.sh | bash -s -- --stopIf you cloned the repo manually:
# Stop the Docker Compose services (backend, frontend, database)
make selfhost-stop
# Stop the local daemon
multica daemon stopIf you've been self-hosting and want to switch your CLI to Multica Cloud:
multica setupThis reconfigures the CLI for multica.hduhelp.com, re-authenticates, and restarts the daemon. You will be prompted before overwriting the existing configuration.
Your local Docker services are unaffected. Stop them separately if you no longer need them.
docker compose -f docker-compose.selfhost.yml pull
docker compose -f docker-compose.selfhost.yml up -dPin MULTICA_IMAGE_TAG in .env to an exact version like v0.2.4 if you want to stay on a specific release. Migrations run automatically on backend startup.
If the selected GHCR tag has not been published yet, fall back to make selfhost-build or docker compose -f docker-compose.selfhost.yml -f docker-compose.selfhost.build.yml up -d --build.
Upgrading from
v0.3.4tov0.3.5+fails withrefusing to drop legacy daily rollups: ...? That's migration103's fail-closed guard: it requirestask_usage_hourlyto be seeded before the legacy daily rollups are dropped. As of MUL-2957migrate upruns that backfill automatically right before applying103, so the upgrade completes in a single invocation. If you are still on a pre-MUL-2957 binary or the auto-hook fails, runbackfill_task_usage_hourlymanually first, then re-run the upgrade. Full instructions in Advanced Configuration → Usage Dashboard Rollup.
If you prefer running Docker Compose steps manually instead of make selfhost:
git clone https://github.com/hduhelp/multica.git
cd multica
cp .env.example .envEdit .env — set JWT_SECRET (required): docker compose refuses to start without
it, and a production backend refuses to boot on the dev default or any known
placeholder.
JWT_SECRET=$(openssl rand -hex 32)Then start everything:
docker compose -f docker-compose.selfhost.yml pull
docker compose -f docker-compose.selfhost.yml up -dIf you prefer configuring the CLI step by step instead of multica setup:
# Point CLI to your local server
multica config set server_url http://localhost:8080
multica config set app_url http://localhost:3000
# Login (opens browser)
multica login
# Start the daemon
multica daemon startFor production deployments with TLS:
multica config set app_url https://app.example.com
multica config set server_url https://api.example.com
multica login
multica daemon startFor environment variables, manual setup (without Docker), reverse proxy configuration, database setup, and more, see the Advanced Configuration Guide.