Multica / Get started
Get started quickly with Self-Host
Use Docker to run Multica on your own server or local machine (you can also use Helm on Kubernetes). About 10 minutes.

Overview
This page takes you through using Docker to run Multica's server (backend + frontend + PostgreSQL) on your own machine or server. After completing this article, your data is completely in your own hands - including workspace, issue, comments, and agent configuration.
The execution of the agent still relies on your locally run daemon process + locally installed AI programming tools - this is exactly the same as Cloud. Self-host replaces the server layer, not the execution layer.
Prerequisites
- Docker is installed and can run
docker compose - Git optional (recommended - you can pull source code)
- A machine that can be turned on for a long time (either local/intranet/cloud host)
- At least one AI programming tool installed on the machine running the daemon (not necessarily the machine running the server - it can be your development laptop)
1. Pull the project + start the backend with one click
make selfhost will:
**Already have a Kubernetes cluster? ** Instead of using Docker, use Helm chart directly - jump to [Kubernetes deployment (alternative)] (#kubernetes-deployment alternative) below, and then return to [Step 4] (#4-First login--Create workspace) to complete the login.
git clone https://github.com/multica-ai/multica.git
cd multica
make selfhost
- If there is no
.envfile, automatically generate one from.env.exampleand generate random JWT_SECRET and Postgres password - Pull the official Docker image (PostgreSQL, Multica backend, Multica frontend)
- Use
docker-compose.selfhost.ymlto start all services - Wait for the backend
/healthendpoint to be ready
If it is a production probe after startup and you want database or migration exceptions to be reflected as failures, please use /readyz instead.
When the backend container starts, it will automatically run database migration (docker/entrypoint.sh executes ./migrate up before starting the server) - you will see the migration output in the backend log. Upgrading the version is also handled automatically.
**Is the image not released yet? ** If
make selfhostreports that it cannot pull the image, it may be that you are on an unreleased version tag. Switch to a stable version or build directly from source:make selfhost-build.
After startup is complete:
- Front end: http://localhost:3000
- Backend: http://localhost:8080
**All ports only listen to
127.0.0.1. **docker-compose.selfhost.ymlbinds each published port to loopback -ss -tlnpwill not see0.0.0.0:8080, and the external network/other machines will not be connected at all by default. This is to avoid service keys and Postgres credentials being exposed directly to the public network. To do cross-machine access, please use a reverse proxy to terminate TLS in front, as detailed below [Step 5b - Cross-machine access: Use a reverse proxy to block the service in front] (#5b-Cross-machine access, use a reverse proxy to block the service in front).
2. Important: Maintain production security configurations
docker-compose.selfhost.ymlsetsAPP_ENVtoproductionby default and leavesMULTICA_DEV_VERIFICATION_CODEempty, so the public network instance does not have a fixed verification code by default. Only setMULTICA_DEV_VERIFICATION_CODEin local or private test automation. If fixed verification codes are enabled whenAPP_ENVis not in production, anyone who can request a verification code can log in with this fixed value. For details, see [Login and Registration Configuration → Fixed Local Test Verification Code] (/auth-setup#Fixed Local Test Verification Code). Be sure to checkAPP_ENV=productionin.envbefore deploying on the public network, andMULTICA_DEV_VERIFICATION_CODEis empty.
3. Configure email service (optional but recommended)
If no email is provided, the user cannot receive the verification code through email; the server will print the generated verification code to stdout.
Two sending channels are supported, choose one according to the deployment environment:
Option A — Resend (public network/cloud deployment):
-
Register at Resend and get an API key
-
Verify a sending domain name you control
-
Set in
.env:RESEND_API_KEY=re_xxxxxxxxxxxx RESEND_FROM_EMAIL=noreply@yourdomain.com
Option B — SMTP relay (intranet/self-deployment):
Suitable for scenarios where the intranet cannot access api.resend.com, or there is already an internal mail relay (Microsoft Exchange, Postfix, self-deployed SendGrid, etc.). When set at the same time, SMTP_HOST has a higher priority than Resend, and the verification code and invitation email will not go to the external provider. The server will automatically upgrade when advertising STARTTLS; port 465 (SMTPS / implicit TLS) will automatically enable the connection or handshake TLS mode, and non-standard SMTPS ports can be forcibly opened with SMTP_TLS=implicit (alias: smtps, ssl).
Anonymous Exchange internal relay (port 25) - Host is trusted by IP, no credentials required:
SMTP_HOST=exchange.internal.example.com
SMTP_PORT=25
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_INSECURE=false
RESEND_FROM_EMAIL=noreply@yourdomain.com # Also serves as SMTP From: header
Authentication submission (port 587, STARTTLS) - relay requires service account; the server automatically upgrades when advertising STARTTLS:
SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587
SMTP_USERNAME=multica
SMTP_PASSWORD=...
SMTP_TLS_INSECURE=false # Only changed to true if private CA / self-signed certificate
RESEND_FROM_EMAIL=noreply@yourdomain.com
Implicit TLS/SMTPS (port 465) - Applicable to service providers such as Alibaba Cloud/Tencent Enterprise Mailbox that do not advertise STARTTLS. Port 465 automatically enables implicit TLS, so SMTP_TLS can be omitted here:
SMTP_HOST=smtp.qiye.aliyun.com
SMTP_PORT=465
SMTP_USERNAME=multica@yourdomain.com
SMTP_PASSWORD=...
SMTP_TLS=implicit #465 can be omitted; required on non-standard SMTPS ports
RESEND_FROM_EMAIL=noreply@yourdomain.com
For strictly public relays that reject the default localhost greeting from public IPs (e.g. Google Workspace smtp-relay.gmail.com), set SMTP_EHLO_NAME to the FQDN expected by the relay - otherwise the connection will be disconnected directly and manifest as an unintelligible EOF on a subsequent command. It defaults to the container hostname, which is usually not a legal FQDN:
SMTP_HOST=smtp-relay.gmail.com
SMTP_PORT=587
SMTP_EHLO_NAME=mail.yourdomain.com # FQDN accepted by relay; the default is the (non-FQDN) container host name
RESEND_FROM_EMAIL=noreply@yourdomain.com
Then restart: docker compose -f docker-compose.selfhost.yml restart backend. When restarting, backend will print the currently selected provider and the negotiated TLS mode (EmailService: SMTP relay <host>:<port> (starttls|implicit-tls) from=… / Resend API / DEV mode). The password will not be recorded, so it is safe to screenshot this line to colleagues.
For more auth configuration (OAuth, registration whitelist) and complete SMTP variable description, see Login and Registration Configuration and Environment Variables.
4. First time login + create workspace
Open http://localhost:3000:
- Enter your email
- Get the verification code from the email received by your configured email backend (Resend or SMTP relay); if neither is configured, copy the line
[DEV] Verification codefrom the stdout of the server container - Do not use
888888directly; it will only take effect ifMULTICA_DEV_VERIFICATION_CODE=888888is explicitly set on a non-production private instance - Create your first workspace after logging in
5. Connect the command line tool to your own server
The command line installation method is the same as Cloud Quick Start → 2. Install command line tools - Homebrew / Script / PowerShell optional.
5a. The same machine
When the CLI and server are on the same machine, the default parameters are sufficient:
multica setup self-host
It will automatically connect to http://localhost:8080 (backend) + http://localhost:3000 (frontend), guide you to log in in the browser, save PAT locally, and automatically start the daemon process.
5b. Cross-machine access: Use reverse proxy to block services in front
Because compose only listens to 127.0.0.1 by default, daemons running from other machines cannot connect to http://<server-ip>:8080 - this is also intentional, otherwise the service key will be directly exposed to the public network. The correct approach is to run a reverse proxy (Caddy / nginx / Cloudflare Tunnel) on the server, let it terminate TLS, and then reverse proxy to 127.0.0.1:8080 (backend) and 127.0.0.1:3000 (frontend). Then point the CLI to the public HTTPS domain name:
multica setup self-host \
--server-url https://<your domain name> \
--app-url https://<your domain name>
Are you more comfortable using environment variables? When the corresponding flag is omitted,
setup self-hostwill readMULTICA_SERVER_URLandMULTICA_APP_URL(when set at the same time, the flag takes precedence).MULTICA_SERVER_URLalso accepts thews://.../wsdaemon writing method in Environment Variables, and automatically normalizes it to an HTTP address.
The smallest available Caddyfile, a single domain name with both front and back ends (with WebSocket forwarding, both daemon and web page depend on it):
multica.example.com {
# WebSocket routing - must precede catch-all
@ws path /ws /ws/*
handle @ws {
reverse_proxy 127.0.0.1:8080 {
flush_interval -1
}
}
#BackendAPI
handle /api/* {
reverse_proxy 127.0.0.1:8080
}
# Other requests → Frontend
reverse_proxy 127.0.0.1:3000
}
After the proxy is up, remember to set FRONTEND_ORIGIN in the server's .env to https://multica.example.com and restart the backend, otherwise the origin verification of WebSocket will reject the browser (see [Troubleshooting → WebSocket cannot connect](/vibe-coding-tools/multica/troubleshooting#websocket-cannot connect)).
Cloudflare Tunnel is also a good choice - it directly gives a public domain name + TLS, and there is no need to expose any ports on the host to the outside world. Nginx can also do it (divided into two domain names app. / api. + proxy_set_header Upgrade to WebSocket). The key is to terminate TLS and forward the Upgrade header on /ws.
6. Create the agent + assign the first task
The process is the same as Cloud - see Getting Started Quickly with Cloud → Steps 5-6.
<span id="7-usage-rollup-no-operator-action-required" />
7. Usage summary (no operation and maintenance required)
The in-process scheduler ticks every 30 seconds and claims the 5-minute UTC plan through the syscronexecutions table. It is safe to run multiple backend copies at the same time - the unique key (jobname, scopekind, scopeid, plantime) ensures that there is only one winner for each plan. New deployments do not require any additional configuration.
Usage / Runtime Kanban reads the derived table
task_usage_hourly, which is filled periodically byrollup_task_usage_hourly(). As of MUL-2957, the backend runs this rollup in-process via the DB backend's scheduler -pg_cronis no longer required and an external cron / systemd timer is no longer recommended. The defaultpgvector/pgvector:pg17image works without modification.
**Compatibility - Registered pg_cron task. ** If you have previously registered rollup with pg_cron (SELECT cron.schedule('rollup_task_usage_hourly', '*/5 * * * *', …)), it is okay not to delete it - the SQL function internally holds advisory lock 4246, and the application scheduler and pg_cron will not concurrently double write. To clear redundant items, you can execute:
SELECT cron.unschedule('rollup_task_usage_hourly')
FROM cron.job WHERE jobname = 'rollup_task_usage_hourly';
**Upgraded from v0.3.4 → v0.3.5+. ** The previous version required operations to manually run cmd/backfill_task_usage_hourly before applying migration 103, otherwise the fail-closed guard would abort migrate up. As of MUL-2957 this step is automatic: the migrate command runs an idempotent monthly slice backfill before applying migration 103 (protected by advisory lock 4246) before continuing. On a busy database you can still run a standalone backfill with --sleep-between-slices=2s to limit read pressure, but it is no longer necessary.
For complete reference (including operation and maintenance considerations and Kubernetes deployment forms), see SELF_HOSTING_ADVANCED.md → Usage Dashboard Rollup in the repository.
Kubernetes deployment (alternative)
If you are already running a Kubernetes cluster, a Helm chart is also included in the repository, with the path deploy/helm/multica/. It is the k8s version of make selfhost - the same backend image, frontend image, pgvector/pgvector:pg17 Postgres, encapsulated into Deployment / Service / Ingress, plus a ConfigMap rendered by values.yaml. This set of charts is based on k3s + Traefik + local-path
This chart does not template any sensitive values. It refers to a Secret called multica-secrets by name, so the real JWT / DB / Resend / Google keys never need to be entered in git, nor in values.yaml. First use kubectl to create the namespace and Secret at once:
kubectl create namespace multica
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=""
Reinstall chart:
git clone https://github.com/multica-ai/multica.git
cd multica
helm install multica deploy/helm/multica -n multica
The default hostnames are multica.dev.lan (web) and api.multica.dev.lan (backend). Just add them to /etc/hosts (or local DNS) and point them to any node IP that is reachable by Ingress. To change the host name, make a copy of deploy/helm/multica/values.yaml, change ingress.frontend.host / ingress.backend.host, then change backend.config.appUrl / frontendOrigin / localUploadBaseUrl / googleRedirectUri to the corresponding address, and then helm install ... -f my-values.yaml.
On a cold cluster, the backend may be Running but Not Ready for a few minutes until Postgres gets up and runs the migration - startupProbe will capture this period and the pod will not be restarted by liveness. Wait for it to be Ready:
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 the browser and return to the above [Step 4 - First Login] (#4 - First Login - Create Workspace) to continue. Connect to your Ingress host from the command line:
multica setup self-host \
--server-url http://api.multica.dev.lan \
--app-url http://multica.dev.lan
I only want to pull the latest image without moving the chart: kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontend. To lock to a certain Multica version, set images.backend.tag / images.frontend.tag in the values file and then helm upgrade. helm -n multica uninstall multica only deletes the workload, keeping both PVC and Secret; kubectl delete namespace multica will clear everything.
The complete reference - the three login methods, the backend ExternalName alias added to bypass the REMOTE_API_URL hard-coded in the web image build-time, resource restrictions, TLS - are all in the repository SELF_HOSTING.md.
FAQ
- The backend cannot start: Look at the container log
docker compose -f docker-compose.selfhost.yml logs backend; the common problem isDATABASE_URLorJWT_SECRETin.env - Verification code not received: No email backend is configured (Neither Resend nor SMTP is set) → Find
[DEV] Verification codefromdocker compose logs backend - WebSocket cannot connect: Public network deployment must set
FRONTEND_ORIGINto your real front-end domain name; see [Troubleshooting → WebSocket cannot connect](/vibe-coding-tools/multica/troubleshooting#websocket-cannot connect) - Usage / Runtime Kanban is always 0: No one schedules
rollup_task_usage_hourly()- see Step 7 above and [Troubleshooting → Usage Kanban is always 0](/vibe-coding-tools/multica/troubleshooting#usage-kanban is always -0) migrate upreportsrefusing to drop legacy daily rollups: fail-closed guard forv0.3.4 → v0.3.5+upgrade path. As of MUL-2957 the migrate command will automatically run backfill before applying migration 103 - see Step 7
Next step
- Environment Variables - complete env list
- Login and registration configuration —— Resend / OAuth / registration whitelist detailed configuration
- GitHub Integration - Connect a GitHub App to allow PR to be automatically associated with issues and automatically transferred to Done when merged
- Troubleshooting - If you encounter problems, come here first
- Desktop Application - You can connect to Desktop through
~/.multica/desktop.json; Web front-end + CLI is still the fastest self-deployment path