# UniDesk Deployment Reference

主 server 使用根目录 `docker-compose.yml` 统一编排 database、backend-core、frontend、provider-gateway 以及必须留在主 server 的用户服务。当前环境本身就是主 server，因此 provider-gateway 也在同一台机器上启动，用与普通计算节点相同的 WebSocket 方式接入 core。Code Queue 不再属于主 server Compose，也不再由 backend-core 通过 provider-gateway 直连业务容器；它作为 `k3sctl-managed` 用户服务经 D601 `k3sctl-adapter` 进入 k3s 标准服务路由。

## Services

- `database` 使用 `postgres:16-alpine`，数据保存到 named volume `unidesk_pgdata_10gb`，初始化 SQL 位于 `src/components/database/init/`。
- `backend-core` 是无状态核心服务，提供 Docker 内网 REST API、provider ingress WebSocket、任务调度入口和数据库访问层。源码拆分为 15 个职责单一的模块（`index.ts` 只做路由和启动），模块结构见 `docs/reference/repo-tree.md`。
- `frontend` 是唯一公开 Web 控制台，提供登录、从 TSX 转译出的 React 应用资产和到 backend-core 的同源代理。
- `provider-gateway` 是当前主 server 的本机计算节点代理，通过 WebSocket 主动连到 provider ingress，挂载 `/var/run/docker.sock` 作为自动任务执行主路径，使用 `pid: "host"` 读取节点级进程资源，并周期性上报系统资源指标、进程占用与 Docker daemon 状态；计算节点 provider-gateway 还必须把 egress proxy 仅发布到宿主 loopback `127.0.0.1:18789` 供本节点执行环境出网，维护用 Host SSH / WSL SSH 私钥目录只读挂载到 `/run/host-ssh`，不得作为自动任务调度主路径。
- `todo-note` 是主 server 承载的 Todo Note 纯后端用户服务，容器名 `todo-note-backend`，只在 Compose 内网暴露 `4211/tcp`，使用主 PostgreSQL 存储迁移后的 Todo Note 数据。
- `k3sctl-adapter` 是 D601 上由 UniDesk 直管的 k3s 控制面适配微服务，容器名 `k3sctl-adapter`，只绑定 `127.0.0.1:4266`，由 UniDesk frontend/backend-core 通过用户服务代理访问并提供 `/api/control-plane` 可见性。
- `code-queue` 是由 D601 原生 k3s/k8s 控制面代管的 Codex/OpenCode 队列用户服务，当前拓扑为 D601 内的 read/write/scheduler 多 Service，通过 `src/components/microservices/k3sctl-adapter/k3s/code-queue.k3s.json` 声明，运行对象通过 `code-queue.k8s.yaml` 创建 Kubernetes Deployment/ClusterIP Service；任务、queue、未读状态、控制状态和通知 outbox 一律写入主 PostgreSQL，不保留本地状态文件 fallback。浏览器只使用稳定的 `code-queue` 用户服务 ID，backend-core 在内部把只读请求分到 `code-queue-read`，把命令写入分到 `code-queue-write`，把执行端 `/health`、dev-container 和 running task control 分到 `code-queue-scheduler`。
- `project-manager` 是主 server 承载的项目管理用户服务，容器名 `project-manager-backend`，仅在 Compose 内网暴露 `4233/tcp`，项目清单写入主 PostgreSQL，浏览器只能通过 UniDesk frontend 同源代理执行增删改查、Excel 导入和 Excel 导出。
- `baidu-netdisk` 是主 server 承载的百度网盘存储用户服务，容器名 `baidu-netdisk-backend`，仅在 Compose 内网暴露 `4244/tcp`，OAuth/token/transfer 状态写入主 PostgreSQL，浏览器只能通过 UniDesk frontend 同源代理执行设备码登录、文件浏览和 staging 传输任务控制。

## Public Exposure Boundary

Docker Compose 对最终用户只公开 frontend host port 和 provider ingress host port。backend-core REST API 仍不得映射公网；PostgreSQL 和 OA Event Flow 只允许为了受控 Code Queue active/standby 节点使用受限端口映射，并必须由 `network.restrictedHostAccess.allowedSourceCidrs` 生成的 `DOCKER-USER` 规则限制到 D601 出口地址，不能作为浏览器或任意公网客户端入口。浏览器访问 core API 必须通过 frontend 的同源代理完成。

计算节点上的用户服务后端也遵守同一边界：业务容器端口只绑定节点本机地址，并由 provider-gateway 主动连出 WebSocket 后接受 backend-core 的 `microservice.http` 调度访问。主 server 不为用户服务新增面向浏览器的公网反向代理端口；最终用户只通过 UniDesk frontend 的 React 页面访问结构化业务控件。

## Docker Compose Runtime

CLI 会优先使用 `docker compose` v2 plugin；当 v2 plugin 不存在时才回退到 `docker-compose` v1。主 server 可以通过安装系统包形式的 Compose v2 plugin 完成无中断升级，因为该动作只增加 Docker CLI plugin，不要求重启 Docker daemon，也不要求重建或停止已有容器。若 Compose v2 build 提示 buildx plugin 不存在，应同样以系统包安装 buildx CLI plugin，而不是重启 Docker 或重建业务容器。

Compose v2 安装后仍然必须遵守 UniDesk 的服务控制入口：全栈生命周期用 `server start` / `server stop`，单服务重建用 `server rebuild <service>`。不要因为 v2 可用就直接在生产栈上手工执行未纳入 CLI 的 `up --build`、`down -v` 或跨项目清理命令；所有会影响容器的动作都应保持 job 可观测、Compose project 固定、database named volume 保留。

版本化用户服务部署优先使用 `bun scripts/cli.ts deploy apply`。`deploy.json` 只声明服务 `id`、`repo` 和 `commitId`；目标节点、Dockerfile、Compose、Kubernetes manifest、健康检查和代理路径继续来自 `config.json` 与现有 manifest。部署必须遵循 target-side build：服务部署到哪台 target，就在哪台 target 从 remote commit 导出源码、一次性代理构建镜像并部署；不得把中心构建镜像作为默认分发路径，也不得用 `docker commit` 或脏 worktree 作为部署输入。完整规则见 `docs/reference/deploy.md`。

## Start And Stop

`bun scripts/cli.ts server start` 与 `bun scripts/cli.ts server stop` 都是异步 job。启动 job 只执行固定 Compose project 的 `up -d --build --remove-orphans`，不得先 `down`，避免在 provider-gateway 旧容器或网络冲突时把长驻控制面容器先删掉又启动失败；停止 job 才允许执行 `down --remove-orphans`。启动和停止流程都禁止删除 Docker named volume。所有会改变主 server Compose 状态的 job 必须通过 `.state/locks/server-compose.lock` 串行化；连续 `server rebuild` 命令只代表连续创建异步 job，不能代表第一个 job 已结束，实际容器变更仍必须由 Compose lock 串行执行。

## Single Service Rebuild

前端、backend-core、本机 provider-gateway 或主 server 承载的 Todo Note/Project Manager/Baidu Netdisk/OA Event Flow 用户服务需要重建时，统一使用 `bun scripts/cli.ts server rebuild <service>`，其中 `<service>` 只能是 `backend-core`、`frontend`、`provider-gateway`、`todo-note`、`project-manager`、`baidu-netdisk` 或 `oa-event-flow`。Code Queue、File Browser、FindJob、Pipeline、MET Nonlinear 和 ClaudeQQ 部署在计算节点，不属于主 server Compose 可重建服务；其中 D601 Code Queue 的正式入口是 `bun scripts/cli.ts codex deploy <commitId>`。该命令先执行目标服务镜像构建，构建成功后才通过 `up -d --no-deps --force-recreate <service>` 替换目标容器，避免构建失败导致运行中的服务被提前停掉。

frontend 改动必须明确上线到公网：修改 `src/components/frontend/src/`、`src/components/frontend/public/style.css`、frontend 使用的共享 TSX/TS 模块或 WebUI 导航后，必须在同一变更集中执行 `bun scripts/cli.ts server rebuild frontend`，并等待 job 成功。公网 WebUI 的 `/app.js` 是 `unidesk-frontend` 容器启动时从镜像内源码转译生成的运行时 bundle；只改工作区文件、只跑 `bun run check`、只跑 `Bun.build` 或只刷新浏览器都不会替换已经运行的容器。

frontend 的 Docker 上线顺序为：先运行必要的本地校验，例如 `bun scripts/cli.ts check` 或对应的 frontend bundle 检查；然后运行 `bun scripts/cli.ts server rebuild frontend`；再用 `bun scripts/cli.ts job status latest` 或具体 job id 确认 `status=succeeded`；最后用 `bun scripts/cli.ts server status`、`curl -fsS http://74.48.78.17:18081/health` 和真实页面/深链接验证。若必须手工复现 CLI 行为，等价 Docker Compose 命令只能是固定 project/env 的 `docker compose --env-file .state/docker-compose.env -f docker-compose.yml -p unidesk build frontend`，随后 `docker compose --env-file .state/docker-compose.env -f docker-compose.yml -p unidesk up -d --no-deps --force-recreate frontend`；手工路径仍必须做同样的健康检查，不得改用全栈 `up --build`、`down`、删除容器或重启 database/backend-core 来“顺手上线”前端。

单服务重建必须由 CLI 解析出的 Compose 命令执行，且只能影响目标 service，不得连带重启 database、backend-core 或其他未指定服务。重建后 job 必须按 Docker Compose label 校验目标容器：`com.docker.compose.project` 等于 `config.json` 中的固定 project name，`com.docker.compose.service` 等于目标服务名，并等待容器进入 `healthy`；没有 healthcheck 的服务至少要进入 `running`。如果验证失败，job 必须失败并输出目标容器状态，禁止把“无输出”或“只完成 build”当作成功。

紧急灾备或数据迁移期间如需手工启动单个 Compose service，也必须保持与 CLI 相同的隔离语义：使用固定 `--env-file .state/docker-compose.env` 和 `up -d --no-deps <service>`，只启动目标容器；如果需要刷新 backend-core 的服务目录或环境变量，应把 `backend-core` 作为显式目标单独重建/替换，不能依赖 `up` 的依赖解析顺手重建 database、backend-core 或其他服务。

正式流程不得依赖人工 `docker rm` 兜底；手工删除旧容器后若 job、Docker client 或 daemon 在 `up` 前中断，会直接造成用户服务代理失败。`server rebuild <service>` 和 `codex deploy <commitId>` 都必须是可观测 job：build-first、受控替换、post-up validation、保留命名卷或 `.state` 运行态目录。Code Queue 等计算节点长任务服务即使被重建也必须依赖服务自身 restart-recovery 恢复任务，不能用“避免重建”掩盖恢复缺陷。

## User Service Restart Recovery

主 server 用户服务和计算节点用户服务都必须按 `docs/reference/microservices.md` 的 Runtime Configuration And Persistence Contract 设计。主 server Compose 内的用户服务通过固定 project、`restart` policy、健康检查、PostgreSQL 权威状态和 `server rebuild <service>` 的 no-deps force-recreate 恢复；计算节点用户服务通过节点本地 Compose/docker run、host bind mount 或业务数据库持久化、Docker restart policy、节点级 autorecover 脚本和 provider-gateway 私有代理验收恢复。上线说明不能只写“容器已启动”，必须说明 Docker daemon 或机器重启后如何重新进入 ready 状态，以及哪些路径/表保存了登录、队列、token、订阅和业务数据。

## Health Criteria

服务跑通的最低标准是：backend-core 内网 `/health` 返回 ok，frontend 公网 `/health` 返回 ok，provider ingress 公网 `/health` 返回 ok，database 在容器内 `pg_isready` 可用，Todo Note 后端 `/api/health` 返回 `storage=postgres`，`k3sctl-adapter` `/api/control-plane` 可见 `unidesk-k3s` Kubernetes API service proxy 状态、D601 scheduler serving healthy、D601 read/write Service healthy、`presentNodeIds` 包含 `D601`、`missingNodeIds=[]` 和 no-fallback 路径，Code Queue `/health` 经 k3s scheduler Service 返回轻量 readiness、默认模型、`queue.storage` 和 `egressProxy.connected=true`，`/api/tasks/overview` 经 `code-queue-read` 返回 PostgreSQL 队列总览，写入类入口经 `code-queue-write` 入库后由 `code-queue-scheduler` 轮询并执行，Project Manager `/health` 返回 `storage.primary=postgres` 和项目数量，backend-core `/api/performance` 返回性能指标，`/api/nodes` 中出现 `main-server`、`D601` 和 `D518` provider 且状态为 `online`，`/api/nodes/system-status` 中出现对应 CPU/内存/硬盘采样，`/api/nodes/docker-status` 中能看到主 server、D601 与 D518 Docker 快照。D601/D518 上不得存在 active `rancher/k3s` 容器；D518 只有在原生 k3s-agent 与稳定 Kubernetes 网络完成验证后才可加入 Code Queue expected nodes。交付前还必须运行 `bun scripts/cli.ts e2e run`，并以 `docs/reference/e2e.md` 的门禁作为最终判定。

## Code Queue D601 Resource Budget

Code Queue 已从主 server 迁移到 D601 k3s/k8s，但仍必须保持明确的 memory/swap 硬上限，默认 `CODE_QUEUE_MAX_ACTIVE_QUEUES=0` 以恢复 queue 间并行，仍保持 `CODE_QUEUE_IN_MEMORY_OUTPUT_RECORDS=10`、`CODE_QUEUE_IN_MEMORY_EVENT_RECORDS=10` 这类小热窗口；任务历史、队列统计和 Trace/output 读取必须优先从 PostgreSQL 直读或聚合，`/health` 只做轻量 readiness，不能为了性能便利在 Bun 进程内缓存全量历史。任何提高 Code Queue 热窗口、日志缓冲、Playwright/Codex 子进程常驻规模或容器上限的变更，或把 `CODE_QUEUE_MAX_ACTIVE_QUEUES` 显式改成正数，都必须在同一任务里说明 D601 资源预算来源，并通过 D601 `KUBECONFIG=/home/ubuntu/unidesk-code-queue-deploy/.state/k3s/kubeconfig kubectl -n unidesk get deploy,svc,pod`、`kubectl -n unidesk top pod` 或等价 Docker stats、`microservice health code-queue` 和对应 E2E 证明未重新引入内存爆炸风险。

## Database Connection Budget

主 PostgreSQL 的内存预算按“少量长驻服务连接池 + 短查询按需连接”设计，不允许每个 Bun 服务沿用默认 8 到 10 个连接。`backend-core` 默认 `DATABASE_POOL_MAX=4`，主 server 上的 `oa-event-flow`、`baidu-netdisk` 默认 `DATABASE_POOL_MAX=2`，`project-manager` 默认 `DATABASE_POOL_MAX=1`，D601 `code-queue` 默认 `CODE_QUEUE_DATABASE_POOL_MAX=2`；如需提高任一连接池上限，必须同时说明并发 SQL 需求、验证 `pg_stat_activity` 中该服务没有长期 idle 堆积，并确认 `max_connections` 仍有足够余量。PostgreSQL 基础配置固定保守值：`shared_buffers=128MB`、`work_mem=4MB`、`maintenance_work_mem=64MB`、`max_connections=50`，避免主 server 低内存环境被空闲 backend 和过大的 per-query 内存预算挤占。

排查 PostgreSQL 内存时以 `docker stats unidesk-database`、`pg_stat_activity` 分组和 `pg_settings` 为准；主机 `ps` 中每个 `postgres` 进程的 RSS 会重复计入共享内存，不能把所有 backend RSS 简单相加当作真实容器占用。所有 UniDesk PostgreSQL 客户端都必须设置可识别的 `application_name`，便于按服务统计连接数、状态和慢查询归属。

## Database Volume

架构要求数据库使用 10 GB named volume；当前实现将 volume 命名为 `unidesk_pgdata_10gb` 以固定生命周期。Docker named volume 默认不强制容量上限；如需硬配额，应在主机存储层或 Docker volume driver 层配置。CLI server 控制只能使用不删除 volume 的 `down` / `up` 流程，禁止使用 `down -v`、`docker volume rm` 或删除 `unidesk_pgdata_10gb`。

`/api/overview` 会返回 `pgdata` 字段，frontend `态势总览 / 核心指标` 必须展示当前 PostgreSQL 数据库占用、命名卷名称和配置容量。Docker 状态页中 `unidesk_pgdata_10gb` 的命名卷检测只对 `main-server` Provider 生效，其他计算节点不需要也不应被要求存在该数据库卷。
