# 技术栈构建约定(Agent 改 Dockerfile 时遵循) ## 通用 1. 基础镜像:Docker Hub(`python:*` / `node:*` / `nginx:*`),避免默认 `ghcr.io`。 2. 多阶段或「依赖层 → 代码层」拆分。 3. BuildKit:`--mount=type=cache` 缓存包管理器目录。 4. 生产 `CMD`/`ENTRYPOINT` 明确;进程监听 **`spec.port`**(与 plan 一致)。 5. 健康检查:实现 `/healthz` 或约定路径,写入 `spec.healthcheckPath`。 6. Dokploy **不要每次 cleanCache**。 7. **有状态数据写挂载路径**(见下),不要只写容器可写层。 8. **本控制面 = 普通容器自部署**(Gitea + Dokploy),**不是**飞书妙搭 / TCE 托管平台。 --- ## 支持矩阵(Agent 选型) | 栈 | 状态 | Agent 默认行为 | |----|------|----------------| | 普通 Python / Node / 静态站 + Dockerfile | **支持** | 正常 create → apply → 交付 URL | | 自管 Nest/FastAPI/Flask(自己的登录与 DB) | **支持** | 同上 | | 飞书妙搭 / `@lark-apaas/*` 全栈模板 | **实验性 · 非主路径** | **不要承诺「可用」**;先告知用户边界;仅用户坚持且理解风险时才部署 | | 依赖妙搭网关登录 / `/spark` runtime / 平台对象存储 / 插件能力 | **不支持** | 引导 `lark-apps` 在妙搭发布,或业务侧去平台化后再上本控制面 | **主路径产品承诺:** 有 Dockerfile、监听约定端口、状态写 `/data`、可选 PG/MySQL → HTTPS 链接。 **不承诺:** 与妙搭线上一致的登录、权限、平台 API、观测、能力插件。 ### 妙搭 / Lark APaaS(仅 boot 兜底,非适配层) 若镜像里仍带着 `PlatformModule`(`platform.enabled: true`),**进程启动**可能需要: ```text FORCE_AUTHN_INNERAPI_DOMAIN=<应用公网 URL> ``` 控制面 `apply` **仅注入最小 boot env**(可关:`SELF_HOST_INJECT_ENV=false`),**不做**登录伪造、SPA 模板渲染、runtime API 补齐: | 变量 | 作用 | |------|------| | `FORCE_AUTHN_INNERAPI_DOMAIN` | 公网 URL,避免 HttpClient 构造失败 | | `FORCE_FRAMEWORK_ENVIRONMENT` | 默认 `development`(`SELF_HOST_FRAMEWORK_ENVIRONMENT`) | | `FORCE_FRAMEWORK_DISABLE_DATAPASS` | 默认 **`false`**(保留 DataPaas → `DRIZZLE_DATABASE`;勿默认关掉) | | `COMPANY_DEPLOY_SELF_HOST` | 标记自部署,供业务仓可选分支 | | `SERVER_HOST` / `SERVER_PORT` / `PORT` | `0.0.0.0` + `spec.port` | | `SUDA_DATABASE_URL` / `DATABASE_URL` | 仅当 plan 带了 `databases[]` 时写入 | **明确不在控制面 / skill 范围:** - 注入 `window.appId` / `__BASENAME__` / CSRF 与 hbs 模板(须业务仓自己处理或去平台化) - 伪造 `x-larkgw-suda-webuser`、业务 `users` 种子、角色权限 - `/spark`、`/__runtime__`、观测 collect、平台账号/对象存储 - 保证页面「能打开且接口正常」 Agent 话术:检测到 `@lark-apaas` 时,**先说明「仅可能进程可启动,接口与登录不保证」**;推荐改为普通栈或妙搭云端(`lark-apps`)。用户仍要求部署时,按普通 Dockerfile 流程走,**交付时写明实验性**。 --- ## 持久化与 `/data`(MCP 默认) 控制面默认为每个应用挂载 **容器内 `/data`**(可配置,见 `inspect_project` → `policy.storage`)。 | 约定 | 说明 | |------|------| | SQLite / 本地文件 | 路径放在 `/data/...`,例如 `/data/app.db` | | 上传目录 | 额外 `persistence`:`{ "name": "uploads", "mountPath": "/var/uploads" }` | | 日志 | ephemeral 可写容器内;需保留则挂卷 | | 多卷 | `spec.persistence[]`;若未包含 `/data`,控制面仍自动补默认卷 | 示例环境变量(应用代码侧): ```text DATA_DIR=/data DATABASE_PATH=/data/app.db ``` **禁止**假设宿主机路径;只认容器内 `mountPath`。 --- ## 托管数据库(Postgres / MySQL) plan 中声明 `spec.databases[]` 后,控制面注入连接串环境变量(默认名 `DATABASE_URL`)。 应用应: 1. 启动时读取 `process.env.DATABASE_URL`(或自定义 `envVar`) 2. **不要**在镜像里写死密码 3. 迁移在启动脚本或独立 job 中执行(注意并发与锁) 示例(Node): ```js const url = process.env.DATABASE_URL; if (!url) throw new Error("DATABASE_URL is required"); ``` 示例(Python): ```python import os url = os.environ.get("DATABASE_URL") if not url: raise RuntimeError("DATABASE_URL is required") ``` Docker 内网 host 由 Dokploy 管理;应用只消费完整 URL。 --- ## Python(uv 优先) 检测:`pyproject.toml` + `uv.lock`,或 `requirements.txt`。 推荐结构: ```dockerfile FROM python:3.12-slim-bookworm WORKDIR /app ENV UV_LINK_MODE=copy \ UV_CONCURRENT_DOWNLOADS=16 \ UV_HTTP_TIMEOUT=120 \ PYTHONUNBUFFERED=1 ARG PIP_INDEX_URL=https://mirrors.aliyun.com/pypi/simple/ ENV UV_DEFAULT_INDEX=${PIP_INDEX_URL} \ UV_INDEX_URL=${PIP_INDEX_URL} \ PIP_INDEX_URL=${PIP_INDEX_URL} RUN --mount=type=cache,target=/root/.cache/pip \ pip install --no-cache-dir "uv>=0.6.0" COPY pyproject.toml uv.lock README.md ./ RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev --no-install-project COPY src ./src # …其它源码… RUN --mount=type=cache,target=/root/.cache/uv \ uv sync --frozen --no-dev # 监听 0.0.0.0,端口与 plan.spec.port 一致 ENV DATA_DIR=/data EXPOSE 8000 CMD ["uv", "run", "your-entrypoint"] ``` 说明: - 慢点通常是 **PyPI 下包**,不是 `pip install uv` 本身。 - 主机 `~/.pip/pip.conf` **不会**自动进容器;必须 Dockerfile/build-arg。 - 飞书长连接类:`replicas=1` + 状态进 `/data`;`exposeWeb: false` 若无需公网页。 --- ## Node(pnpm / npm) 检测:`package.json`,优先 `pnpm-lock.yaml` → `package-lock.json` → `yarn.lock`。 ### Dockerfile 硬规则(踩过的坑,Agent 必守) | 错误 | 现象 | 正确做法 | |------|------|----------| | 在 **build 阶段 `npm ci` 之前** `ENV NODE_ENV=production` | `nest: not found` / `vite: not found`;日志里 packages 只有几百个(缺 devDependencies) | **先** `npm ci`(装全量含 dev)→ **再** `ENV NODE_ENV=production` → `npm run build` | | 不设 Node 堆、大前端 Vite 打包 | `FATAL ERROR: JavaScript heap out of memory` | build 阶段:`ENV NODE_OPTIONS=--max-old-space-size=3072`(小机 3~4G 可试 2560) | | multi-stage 里 **build 与 runtime 无依赖** | Docker **并行** 两个 `npm ci`,小机内存翻倍易 OOM | runtime 至少 `COPY --from=build …` 一件产物,让 build 先完成 | | 用用户 SSH 推 Gitea | Permission denied | 用 `ensure_repository` 的 `pushUrl`,见 SKILL | ### 推荐:Nest + Vite 全栈模板 ```dockerfile FROM node:22-bookworm-slim AS build WORKDIR /app # ① 提高堆;② 此时不要 NODE_ENV=production ENV NODE_OPTIONS=--max-old-space-size=3072 COPY package.json package-lock.json ./ RUN npm ci --ignore-scripts COPY . ./ # ③ 仅编译时 production ENV NODE_ENV=production RUN npm run build:prod && npm cache clean --force FROM node:22-bookworm-slim AS runtime WORKDIR /app ENV NODE_ENV=production \ SERVER_HOST=0.0.0.0 \ SERVER_PORT=3000 \ DATA_DIR=/data COPY package.json package-lock.json ./ # ④ 依赖 build,避免与 vite 并行 npm ci COPY --from=build /app/package.json /tmp/.build-done RUN npm ci --omit=dev --ignore-scripts && npm cache clean --force COPY --from=build /app/dist ./ EXPOSE 3000 CMD ["node", "server/main.js"] ``` ### 推荐:较轻的 Node API(pnpm) ```dockerfile FROM node:22-bookworm-slim AS build WORKDIR /app ENV NODE_OPTIONS=--max-old-space-size=2048 RUN corepack enable COPY package.json pnpm-lock.yaml ./ RUN --mount=type=cache,target=/root/.local/share/pnpm/store \ pnpm install --frozen-lockfile COPY . . ENV NODE_ENV=production RUN pnpm run build FROM node:22-bookworm-slim AS runner WORKDIR /app ENV NODE_ENV=production DATA_DIR=/data COPY --from=build /app/dist ./dist COPY --from=build /app/package.json ./ COPY --from=build /app/node_modules ./node_modules # 若 node_modules 含 dev,可改为 runner 单独 pnpm install --prod EXPOSE 3000 CMD ["node", "dist/index.js"] ``` 静态前端(Vite 等)可用 `nginx:stable-alpine` 拷 `dist`;`spec.port` 多为 `80`。 纯静态站构建同样需要 **devDependencies**(vite)在 build 阶段装全。 Registry 加速示例 `.npmrc`: ```ini registry=https://registry.npmmirror.com ``` ### 部署前 Agent 自检(Node) - [ ] Dockerfile **没有**在安装依赖前设置 `NODE_ENV=production` - [ ] `nest` / `vite` / `tsc` 等 CLI 在 **devDependencies** 且 build 阶段能装到 - [ ] 大前端:`NODE_OPTIONS=--max-old-space-size=3072`(或更高,视机器) - [ ] multi-stage:runtime 依赖 build,避免并行双 `npm ci` - [ ] 运行 `CMD` 只跑编译产物,**不**在 runtime 再 `nest build` --- ## Docker Compose - 当前 MCP MVP **仅** `buildType: dockerfile` 单应用。 - 多服务 Compose:legacy / 运维在 Dokploy 选 Compose 类型,或拆成多个 MCP 应用 + 托管 DB。 - 生产注意:DB 密码走 `databases[]` 注入;卷走 `persistence[]`。 --- ## 构建耗时预期(经验) | 场景 | 预期 | |------|------| | 首次全量(弱网 + 下包) | 10–40 分钟可能 | | 依赖层已缓存,只改代码 | 数分钟内 | | 仅拉预构建业务镜像 | 通常更短 | 若日志出现 `Prepared N packages in 30m`:换源 + 分层缓存 + 禁止 cleanCache;长期用私有镜像仓库。 --- ## 与 plan.spec 对齐检查 部署前 Agent 自检: - [ ] 容器监听 `0.0.0.0:$PORT`,且 `spec.port` 一致 - [ ] 有状态路径落在 `/data` 或其它 `persistence.mountPath` - [ ] 需要 PG/MySQL 时 plan 带 `databases`,代码读对应 env - [ ] Web 服务 `exposeWeb: true`(默认);Worker 显式 `false` - [ ] `.gitignore` 排除 `.env`、密钥、本地 db ### DataPaas / DRIZZLE(仅当镜像仍用 @lark-apaas DataPaas) - 保持 `FORCE_FRAMEWORK_DISABLE_DATAPASS=false`(默认) - plan 带 Postgres 时控制面会写 `SUDA_DATABASE_URL` + `DATABASE_URL` - 这只服务 **Nest 注入**;不代表业务接口已可用