---
title: Self-Host 快速上手
description: 在自己的服务器或本机用 Docker 把 Multica 跑起来（也可以在 Kubernetes 上用 Helm）。约 10 分钟。
---

import { Callout } from "fumadocs-ui/components/callout";

这一页带你用 Docker 把 Multica 的**服务器**（后端 + 前端 + PostgreSQL）跑在自己的机器或服务器上。走完这一篇你的数据就完全在自己手里——包括 [工作区](/workspaces)、[issue](/issues)、[评论](/comments)、[智能体](/agents) 配置。

智能体**执行**还是靠你本地跑的 [守护进程](/daemon-runtimes) + 本地装好的 [AI 编程工具](/providers)——这点和 Cloud 完全一样。Self-host 换掉的是服务器那一层，不是执行那一层。

## 前置要求

- **Docker** 安装好并且能跑 `docker compose`
- **Git** 可选（推荐——可以拉源码）
- 一台能长期开机的机器（本地 / 内网 / 云主机都行）
- 至少一款 AI 编程工具装在**运行守护进程的机器上**（不一定是跑服务器的机器——可以是你开发用的笔记本）

## 1. 拉取项目 + 一键启动后端

<Callout type="info">
**已经有 Kubernetes 集群？** 不用走 Docker，直接用 Helm chart——跳到下面的 [Kubernetes 部署（替代方案）](#kubernetes-部署替代方案)，装完再回到 [第 4 步](#4-首次登录--创建工作区) 完成登录。
</Callout>

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

`make selfhost` 会：

1. 如果没有 `.env` 文件，从 `.env.example` 自动生成一份并**生成随机 JWT_SECRET**
2. 拉取官方 Docker 镜像（PostgreSQL、Multica backend、Multica frontend）
3. 用 `docker-compose.selfhost.yml` 启动全部服务
4. 等后端 `/health` 端点准备就绪

如果是启动完成后的生产探针，想让数据库或 migration 异常也体现为失败，请改用 `/readyz`。

后端容器启动时会**自动跑数据库 migration**（`docker/entrypoint.sh` 在启动 server 前执行 `./migrate up`）——你会在 backend 日志里看到 migration 输出。升级版本时同样自动处理。

<Callout type="info">
**镜像还没发布？** 如果 `make selfhost` 报拉不到镜像，可能是你在某个未发布的版本标签上。切到稳定版本或直接从源码构建：`make selfhost-build`。
</Callout>

启动完成后：

- **前端**：[http://localhost:3000](http://localhost:3000)
- **后端**：[http://localhost:8080](http://localhost:8080)

<Callout type="info">
**所有端口只监听 `127.0.0.1`。** `docker-compose.selfhost.yml` 把每个 publish 出来的端口都绑到 loopback —— `ss -tlnp` 不会看到 `0.0.0.0:8080`，外网/其它机器默认根本连不上。这是为了避免默认 `JWT_SECRET` 和 Postgres 凭据被直接暴露到公网。要做跨机访问，请用反向代理在前面终结 TLS，详见下方 [Step 5b —— 跨机访问：用反向代理把服务挡在前面](#5b-跨机访问用反向代理把服务挡在前面)。
</Callout>

## 2. 重要：保持生产安全配置

<Callout type="warning">
**`docker-compose.selfhost.yml` 默认把 `APP_ENV` 设成 `production`**，并让 `MULTICA_DEV_VERIFICATION_CODE` 为空，所以公网实例默认没有固定验证码。

只在本地或私有测试自动化里设置 `MULTICA_DEV_VERIFICATION_CODE`。如果在 `APP_ENV` 非 production 时启用了固定验证码，任何能请求验证码的人都能用这个固定值登录。详见 [登录与注册配置 → 固定本地测试验证码](/auth-setup#固定本地测试验证码)。

公网部署前一定检查 `.env` 里 `APP_ENV=production`，且 `MULTICA_DEV_VERIFICATION_CODE` 为空。
</Callout>

## 3. 配置邮件服务（可选但推荐）

如果不配邮件，用户无法通过邮件收到验证码；server 会把生成的验证码打印到 stdout。

支持两种发送通道，按部署环境二选一：

**Option A — Resend（公网/云端部署）：**

1. 在 [Resend](https://resend.com/) 注册并拿一个 API key
2. 验证一个你控制的发件域名
3. 在 `.env` 里设：

    ```bash
    RESEND_API_KEY=re_xxxxxxxxxxxx
    RESEND_FROM_EMAIL=noreply@yourdomain.com
    ```

**Option B — SMTP relay（内网/自部署）：**

适合内网无法访问 `api.resend.com`，或已经有内部邮件中继（Microsoft Exchange、Postfix、自部署 SendGrid 等）的场景。同时设置时 `SMTP_HOST` 优先级高于 Resend，验证码和邀请邮件不会走外部 provider。服务端 advertise STARTTLS 时会自动升级；端口 `465`（SMTPS / 隐式 TLS）会自动启用连接即握手的 TLS 模式，非标准 SMTPS 端口用 `SMTP_TLS=implicit`（别名：`smtps`、`ssl`）强制开启。

**匿名 Exchange 内部 relay（端口 25）** —— 主机按 IP 被信任，不需要凭据：

```bash
SMTP_HOST=exchange.internal.example.com
SMTP_PORT=25
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_TLS_INSECURE=false
RESEND_FROM_EMAIL=noreply@yourdomain.com  # 同时作为 SMTP From: 头
```

**认证提交（端口 587，STARTTLS）** —— relay 需要 service account；服务端 advertise STARTTLS 时自动升级：

```bash
SMTP_HOST=smtp.internal.example.com
SMTP_PORT=587
SMTP_USERNAME=multica
SMTP_PASSWORD=...
SMTP_TLS_INSECURE=false        # 仅在私有 CA / 自签证书时改成 true
RESEND_FROM_EMAIL=noreply@yourdomain.com
```

**隐式 TLS / SMTPS（端口 465）** —— 适用于阿里云 / 腾讯企业邮箱等不广告 STARTTLS 的服务商。端口 `465` 自动启用隐式 TLS，因此这里 `SMTP_TLS` 可省略：

```bash
SMTP_HOST=smtp.qiye.aliyun.com
SMTP_PORT=465
SMTP_USERNAME=multica@yourdomain.com
SMTP_PASSWORD=...
SMTP_TLS=implicit              # 465 上可省略；非标准 SMTPS 端口上必填
RESEND_FROM_EMAIL=noreply@yourdomain.com
```

对于**拒绝来自公网 IP 的默认 `localhost` 问候的严格公网 relay（例如 Google Workspace `smtp-relay.gmail.com`）**，把 `SMTP_EHLO_NAME` 设成 relay 期望的 FQDN——否则连接会被直接断开，并在后续某条命令上表现为一个不知所云的 `EOF`。它默认取容器主机名，而后者通常不是合法的 FQDN：

```bash
SMTP_HOST=smtp-relay.gmail.com
SMTP_PORT=587
SMTP_EHLO_NAME=mail.yourdomain.com   # relay 接受的 FQDN；默认取（非 FQDN 的）容器主机名
RESEND_FROM_EMAIL=noreply@yourdomain.com
```

之后重启：`docker compose -f docker-compose.selfhost.yml restart backend`。重启时 backend 会打印当前选择的 provider 和协商出的 TLS 模式（`EmailService: SMTP relay <host>:<port> (starttls|implicit-tls) from=…` / `Resend API` / `DEV mode`），密码不会被记录，所以这行截图给同事是安全的。

更多 auth 配置（OAuth、注册白名单）以及完整的 SMTP 变量说明见 [登录与注册配置](/auth-setup) 和 [环境变量](/environment-variables)。

## 4. 首次登录 + 创建工作区

打开 [http://localhost:3000](http://localhost:3000)：

- 输入你的邮箱
- 从你配置的邮件后端（Resend 或 SMTP relay）收到的邮件里拿验证码；两者都没配的话，从 server 容器的 stdout 里抄 `[DEV] Verification code` 这行
- 不要直接使用 `888888`；只有在非 production 私有实例上显式设置 `MULTICA_DEV_VERIFICATION_CODE=888888` 后它才会生效
- 登录后创建第一个工作区

## 5. 连接命令行工具到你自己的 server

命令行装法和 [Cloud 快速上手 → 2. 装命令行工具](/cloud-quickstart#2-装-multica-命令行工具) 一样——Homebrew / 脚本 / PowerShell 任选。

### 5a. 同一台机器

CLI 和 server 在同一台机器上时，默认参数就够用：

```bash
multica setup self-host
```

会自动连 `http://localhost:8080`（backend）+ `http://localhost:3000`（frontend），引导你在浏览器里登录、把 PAT 存到本地、**自动启动守护进程**。

### 5b. 跨机访问：用反向代理把服务挡在前面

因为 compose 默认只监听 `127.0.0.1`，从别的机器跑的 daemon 是连不上 `http://<server-ip>:8080` 的——这也是有意为之，否则默认 `JWT_SECRET` 等于直接暴露在公网。正确做法是在 server 上跑一个反向代理（Caddy / nginx / Cloudflare Tunnel），由它终结 TLS，再反代到 `127.0.0.1:8080`（backend）和 `127.0.0.1:3000`（frontend）。然后把 CLI 指到公开的 HTTPS 域名：

```bash
multica setup self-host \
  --server-url https://<你的域名> \
  --app-url https://<你的域名>
```

最小可用的 Caddyfile，单域名同时挂前后端（带 WebSocket 转发，daemon 和网页端都依赖）：

```nginx
multica.example.com {
    # WebSocket 路由——必须在 catch-all 之前
    @ws path /ws /ws/*
    handle @ws {
        reverse_proxy 127.0.0.1:8080 {
            flush_interval -1
        }
    }

    # Backend API
    handle /api/* {
        reverse_proxy 127.0.0.1:8080
    }

    # 其它请求 → 前端
    reverse_proxy 127.0.0.1:3000
}
```

代理起好之后，记得在 server 的 `.env` 里把 `FRONTEND_ORIGIN` 设成 `https://multica.example.com` 并重启后端，否则 WebSocket 的 origin 校验会把浏览器拒掉（见 [故障排查 → WebSocket 连不上](/troubleshooting#websocket-连不上)）。

[Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) 也是不错的选择——它直接给一个公开域名 + TLS，host 上不用对外暴露任何端口。Nginx 也能做（分 `app.` / `api.` 两个域名 + `proxy_set_header Upgrade` 转 WebSocket），关键就是终结 TLS、并在 `/ws` 上转发 `Upgrade` 头。

## 6. 创建智能体 + 分配第一个任务

流程和 Cloud 一样——见 [Cloud 快速上手 → 5-6 步](/cloud-quickstart#5-创建智能体)。

<span id="7-usage-rollup-no-operator-action-required" />

## 7. 用量汇总（无需运维操作）

<Callout type="info">
Usage / Runtime 看板读的是派生表 `task_usage_hourly`，由 `rollup_task_usage_hourly()` 周期性填充。从 MUL-2957 起，后端通过 DB 后端的调度器在进程内运行该 rollup —— 不再需要 `pg_cron`，外部 cron / systemd timer 也不再是推荐方案。默认的 `pgvector/pgvector:pg17` 镜像无需改动即可工作。
</Callout>

进程内调度器每 30 秒 tick 一次，通过 `sys_cron_executions` 表认领 5 分钟一档的 UTC plan。多 backend 副本同时跑也安全 —— 唯一键 `(job_name, scope_kind, scope_id, plan_time)` 保证每个 plan 只有一个赢家。新部署不需要任何额外配置。

**兼容性 —— 已注册的 `pg_cron` 任务。** 如果你之前用 `pg_cron` 注册过 rollup（`SELECT cron.schedule('rollup_task_usage_hourly', '*/5 * * * *', …)`），不删也行 —— SQL 函数内部持有 advisory lock 4246，应用调度器和 `pg_cron` 不会并发双写。要清掉冗余项可以执行：

```sql
SELECT cron.unschedule('rollup_task_usage_hourly')
  FROM cron.job WHERE jobname = 'rollup_task_usage_hourly';
```

**从 `v0.3.4 → v0.3.5+` 升级。** 上一版要求运维在应用 migration 103 之前手动跑 `cmd/backfill_task_usage_hourly`，否则 fail-closed 守卫会中止 `migrate up`。从 MUL-2957 起这一步是自动的：migrate 命令会在应用 migration 103 之前（advisory lock 4246 保护下）运行幂等的按月切片 backfill，然后继续。在繁忙的数据库上你仍可以用 `--sleep-between-slices=2s` 跑独立 backfill 来限制读压力，但已不是必需。

完整参考（含运维注意事项和 Kubernetes 部署形态）见仓库的 [`SELF_HOSTING_ADVANCED.md → Usage Dashboard Rollup`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING_ADVANCED.md#usage-dashboard-rollup)。

## Kubernetes 部署（替代方案）

如果你已经在跑 Kubernetes 集群，仓库里也带了一个 Helm chart，路径 `deploy/helm/multica/`。它就是 k8s 版的 `make selfhost`——一样的 backend 镜像、frontend 镜像、`pgvector/pgvector:pg17` Postgres，封装成 Deployment / Service / Ingress，再加上一个由 `values.yaml` 渲染出来的 `ConfigMap`。这套 chart 是按照 k3s + Traefik + `local-path` 写的，集群里只要有 Ingress controller 和默认的 `ReadWriteOnce` StorageClass 就能跑，其他类型的集群稍微改一改也能用。

这个 chart **不会模板化任何敏感值**。它通过 name 引用一个叫 `multica-secrets` 的 Secret，所以真实的 JWT / DB / Resend / Google 密钥永远不用进 git，也不用进 `values.yaml`。先用 kubectl 一次性把命名空间和 Secret 建好：

```bash
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=""
```

再装 chart：

```bash
git clone https://github.com/multica-ai/multica.git
cd multica
helm install multica deploy/helm/multica -n multica
```

默认主机名是 `multica.dev.lan`（web）和 `api.multica.dev.lan`（backend）。把它们加进 `/etc/hosts`（或者本地 DNS），指向任意一个 Ingress 可达的节点 IP 就行。要换主机名，就把 `deploy/helm/multica/values.yaml` 复制一份，改掉 `ingress.frontend.host` / `ingress.backend.host`，再把 `backend.config.appUrl` / `frontendOrigin` / `localUploadBaseUrl` / `googleRedirectUri` 改成相应的地址，然后 `helm install ... -f my-values.yaml`。

冷集群上 backend 可能会 `Running` 但 `Not Ready` 持续几分钟，等 Postgres 起来并跑完 migration——startupProbe 会兜住这一段，pod 不会被 liveness 重启。等它 `Ready` 之后：

```bash
curl -H "Host: api.multica.dev.lan" http://<ingress-ip>/healthz
# {"status":"ok","checks":{"db":"ok","migrations":"ok"}}
```

然后浏览器打开 `http://multica.dev.lan`，回到上面的 [第 4 步——首次登录](#4-首次登录--创建工作区) 继续。命令行连到你的 Ingress 主机：

```bash
multica setup self-host \
  --server-url http://api.multica.dev.lan \
  --app-url http://multica.dev.lan
```

只想拉最新镜像、不动 chart：`kubectl -n multica rollout restart deploy/multica-backend deploy/multica-frontend`。要锁到某个 Multica 版本，就在 values 文件里设 `images.backend.tag` / `images.frontend.tag`，再 `helm upgrade`。`helm -n multica uninstall multica` 只删工作负载，PVC 和 Secret 都保留；`kubectl delete namespace multica` 才会全清。

完整参考——三种登录方式、为了绕过 web 镜像 build-time 写死的 `REMOTE_API_URL` 而加的 `backend` ExternalName 别名、资源限制、TLS——都在仓库的 [`SELF_HOSTING.md`](https://github.com/multica-ai/multica/blob/main/SELF_HOSTING.md#kubernetes-deployment-alternative)。

## 常见问题

- **后端起不来**：看容器日志 `docker compose -f docker-compose.selfhost.yml logs backend`；常见是 `.env` 里 `DATABASE_URL` 或 `JWT_SECRET` 有问题
- **验证码收不到**：没配任何邮件后端（Resend 和 SMTP 都没设） → 从 `docker compose logs backend` 里找 `[DEV] Verification code`
- **WebSocket 连不上**：公网部署必须设 `FRONTEND_ORIGIN` 成你真实的前端域名；见 [故障排查 → WebSocket 连不上](/troubleshooting#websocket-连不上)
- **Usage / Runtime 看板一直是 0**：没人调度 `rollup_task_usage_hourly()` —— 见上面的 [第 7 步](#7-usage-rollup-no-operator-action-required) 和 [故障排查 → Usage 看板一直是 0](/troubleshooting#usage-看板一直是-0)
- **`migrate up` 报 `refusing to drop legacy daily rollups`**：`v0.3.4 → v0.3.5+` 升级路径的 fail-closed guard。从 MUL-2957 起 migrate 命令在应用 migration 103 之前会自动跑 backfill —— 见 [第 7 步](#7-usage-rollup-no-operator-action-required)

## 下一步

- [环境变量](/environment-variables) —— 完整 env 清单
- [登录与注册配置](/auth-setup) —— Resend / OAuth / 注册白名单详细配置
- [GitHub 集成](/github-integration) —— 连一个 GitHub App，让 PR 自动关联 issue、merge 时自动转 Done
- [故障排查](/troubleshooting) —— 遇到问题先来这里
- [桌面应用](/desktop-app) —— 可以通过 `~/.multica/desktop.json` 连接 Desktop；Web 前端 + CLI 仍然是最快的自部署路径
