Files
multica/SELF_HOSTING.md
LinYushen a6247ad714 helm: gate uploads PVC behind backend.uploads.persistence.enabled (#3655)
Adds a value (default true for backward compatibility) that gates the
uploads PersistentVolumeClaim, the backend container's volumeMount, and
the pod-spec volume. Operators who serve uploads from S3 (S3_BUCKET set)
can now set backend.uploads.persistence.enabled=false to drop the PVC
entirely, removing the ReadWriteOnce Multi-Attach barrier on the storage
side for replicas > 1.

Also makes the PVC accessModes configurable (default [ReadWriteOnce]) so
operators with a ReadWriteMany-capable StorageClass can share the
uploads volume across replicas without object storage.

Documentation: values.yaml comments and the SELF_HOSTING.md resource
list are updated to describe the new toggle.

Refs: https://github.com/multica-ai/multica/issues/3646

Co-authored-by: multica-agent <github@multica.ai>
2026-06-02 17:21:26 +08:00

21 KiB

Self-Hosting Guide

Deploy Multica on your own infrastructure in minutes.

Architecture

Component Description Technology
Backend REST API + WebSocket server Go (single binary)
Frontend Web application Next.js 16
Database Primary data store PostgreSQL 17 with pgvector

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:

# 1. Install CLI + provision the self-host server
curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --with-server

# 2. Configure CLI, authenticate, and start the daemon
multica setup self-host

This 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 multica-ai/tap/multica

Step-by-Step Setup (Alternative)

If you prefer to run each step manually:

Step 1 — Start the Server

Prerequisites: Docker and Docker Compose.

git clone https://github.com/multica-ai/multica.git
cd multica
make selfhost

make 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:

Note: If you prefer to run the Docker Compose steps manually, see Manual Docker Compose Setup below.

Step 2 — Log In

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_KEY in .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=development and MULTICA_DEV_VERIFICATION_CODE=888888 in .env, then restart the backend. This fixed code is ignored when APP_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_CODE on a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.

Step 3 — Install CLI & Start Daemon

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:

a) Install the CLI and an AI agent

brew install multica-ai/tap/multica

You also need at least one AI agent CLI installed:

b) One-command setup

multica setup self-host

This automatically:

  1. Configures the CLI to connect to localhost (ports 8080/3000)
  2. Opens your browser for authentication
  3. Discovers your workspaces
  4. 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.com

To verify the daemon is running:

multica daemon status

Alternative: If you prefer manual steps, see Manual CLI Configuration below.

Step 4 — Verify & Start Using

  1. Open your workspace in the web app at http://localhost:3000
  2. Navigate to Settings → Runtimes — you should see your machine listed
  3. Go to Settings → Agents and create a new agent
  4. Create an issue and assign it to your agent — it will pick up the task automatically

Kubernetes Deployment (Alternative)

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/multica-ai/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-postgrespgvector/pgvector:pg17 backed by a 10Gi PVC
  • multica-backend — Go API/WS server. Backed by a 5Gi ReadWriteOnce uploads PVC by default; set backend.uploads.persistence.enabled=false when 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 Ingress resources: one for the web host, one for the backend host
  • multica-config ConfigMap (rendered from values.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.

One release per namespace: the prebuilt multica-web image bakes REMOTE_API_URL=http://backend:8080 at build time, so the chart ships an ExternalName Service literally named backend. Because that name is unprefixed, you can run only one Multica release per namespace, and helm install will fail if a Service/backend already exists there (pass --take-ownership, or use a dedicated namespace). If you build a web image with a patched REMOTE_API_URL, set frontend.compatibility.backendAlias: false to drop the alias.

Prerequisites: kubectl and helm (v3.13+ for --take-ownership, or v4+) configured for the target cluster, an Ingress controller (Traefik / NGINX), and a default StorageClass.

Step 1 — Point hostnames at the cluster

The chart defaults to multica.dev.lan (web) and api.multica.dev.lan (backend). Pick one of:

  • /etc/hosts on every machine that needs access (developer laptops + the machine running the daemon):

    192.168.1.206  multica.dev.lan api.multica.dev.lan
    

    Replace 192.168.1.206 with 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.

Step 2 — Create the namespace

kubectl create namespace multica

Step 3 — Create the multica-secrets Secret

The 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).

Step 4 — Install the chart

helm install multica oci://ghcr.io/multica-ai/charts/multica \
  --version <chart-version> \
  -n multica

Released 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/multica-ai/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/multica-ai/charts/multica \
  --version <chart-version> \
  -n multica \
  -f my-values.yaml

When developing from a checkout, use the local chart path instead:

helm install multica deploy/helm/multica -n multica

Watch the pods come up:

kubectl -n multica get pods -w

On 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.

Step 5 — Log In

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-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 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: development in your values file and MULTICA_DEV_VERIFICATION_CODE=888888 in the Secret, then helm upgrade and restart. This fixed code is ignored when APP_ENV=production.

    helm upgrade multica oci://ghcr.io/multica-ai/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_CODE on a publicly reachable instance — anyone who knows an email address can then log in with that fixed code.

Step 6 — Install CLI & Start Daemon

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.lan

Make sure the machine running the daemon has the same /etc/hosts (or DNS) entries from Step 1.

Updating

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-frontend

To 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/multica-ai/charts/multica \
  --version <chart-version> \
  -n multica \
  -f my-values.yaml

If 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.4

Then run the same upgrade command with -f my-values.yaml:

helm upgrade multica oci://ghcr.io/multica-ai/charts/multica \
  --version <chart-version> \
  -n multica \
  -f my-values.yaml

To roll back if an upgrade goes sideways:

helm -n multica rollback multica

Upgrading from v0.3.4 to v0.3.5+ fails with refusing to drop legacy daily rollups: ...? Same migration guard as the Docker path — see Usage Dashboard Rollup → Option C. Run the 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.

Tearing down

# 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 multica

Usage Dashboard Rollup (Required)

Starting with v0.3.5, the Usage / Runtime dashboards read from a derived task_usage_hourly table rather than directly from task_usage. Raw task_usage rows are written by the backend on every task, but the dashboard only sees data after rollup_task_usage_hourly() runs and aggregates them into task_usage_hourly.

The bundled pgvector/pgvector:pg17 image does NOT include pg_cron. If nothing schedules the rollup, the dashboard will stay at zero forever even though task_usage is populated. You have three supported options — pick one before relying on the dashboard.

Upgrading from v0.3.4 to v0.3.5+ with existing task_usage history: migration 103 is fail-closed and will abort migrate up with refusing to drop legacy daily rollups: …. Run backfill_task_usage_hourly first (Option C below), then re-run the upgrade. Fresh installs are exempted by that guard and migrate cleanly — but the dashboard will still stay at zero until you pick Option A or Option B.

Option A — External cron / systemd-timer (simplest)

Schedule a 5-minute job that calls rollup_task_usage_hourly(). It is idempotent and watermark-driven, so a missed tick catches up on the next run.

# /etc/cron.d/multica-rollup — every 5 minutes
*/5 * * * * root docker compose -f /path/to/multica/docker-compose.selfhost.yml \
  exec -T postgres psql -U multica -d multica \
  -c "SELECT rollup_task_usage_hourly();" >/dev/null

Or as a systemd timer + service if you prefer that surface. The function returns the number of (upserted + deleted-empty) rows; it's safe to call concurrently with itself (an advisory lock makes overlapping runs no-op) and safe to call alongside backfill_task_usage_hourly.

Option B — Swap Postgres for an image that ships pg_cron

If you'd rather have Postgres schedule itself, replace pgvector/pgvector:pg17 in docker-compose.selfhost.yml with an image that bundles both pgvector and pg_cron (e.g. supabase/postgres, or your own build of pgvector/pgvector with pg_cron added and shared_preload_libraries=pg_cron set on the server). Then, once:

CREATE EXTENSION IF NOT EXISTS pg_cron;
SELECT cron.schedule(
  'rollup_task_usage_hourly',
  '*/5 * * * *',
  $$SELECT rollup_task_usage_hourly()$$
);

shared_preload_libraries requires a Postgres restart to take effect — set it in postgresql.conf (or via the image's documented mechanism) before bringing the container up.

Option C — Backfill history first, then schedule

If you're upgrading from v0.3.4 → v0.3.5+ and already have task_usage rows (or you just want the dashboard to show historical data on a fresh install that you've been running for a while), run the bundled backfill command once before scheduling the rollup:

# Backfills task_usage_hourly from all historical task_usage rows and stamps
# the rollup watermark. Idempotent — safe to re-run.
docker compose -f docker-compose.selfhost.yml exec backend \
  ./backfill_task_usage_hourly --sleep-between-slices=2s

On a database with years of data this can scan tens of millions of rows; --sleep-between-slices=2s throttles the read pressure. Use --months-back N (plus --force-partial) if you only want the last N months. Once it finishes, set up Option A or Option B so new buckets keep flowing.

After upgrading, re-run migrate up (or restart the backend container — migrations run automatically on startup) to apply migration 103 cleanly.

Stopping Services

If you installed via the install script:

curl -fsSL https://raw.githubusercontent.com/multica-ai/multica/main/scripts/install.sh | bash -s -- --stop

If you cloned the repo manually:

# Stop the Docker Compose services (backend, frontend, database)
make selfhost-stop

# Stop the local daemon
multica daemon stop

Switching to Multica Cloud

If you've been self-hosting and want to switch your CLI to Multica Cloud:

multica setup

This reconfigures the CLI for multica.ai, 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.

Upgrading

docker compose -f docker-compose.selfhost.yml pull
docker compose -f docker-compose.selfhost.yml up -d

Pin 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.4 to v0.3.5+ fails with refusing to drop legacy daily rollups: ...? That's migration 103's fail-closed guard: it requires task_usage_hourly to be seeded before the legacy daily rollups are dropped. Run backfill_task_usage_hourly first, then re-run the upgrade. Full instructions in Usage Dashboard Rollup → Option C.


Manual Docker Compose Setup

If you prefer running Docker Compose steps manually instead of make selfhost:

git clone https://github.com/multica-ai/multica.git
cd multica
cp .env.example .env

Edit .env — at minimum, change JWT_SECRET:

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 -d

Manual CLI Configuration

If 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 start

For 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 start

Advanced Configuration

For environment variables, manual setup (without Docker), reverse proxy configuration, database setup, and more, see the Advanced Configuration Guide.