262 lines
8.5 KiB
Markdown
262 lines
8.5 KiB
Markdown
# 技术栈构建约定(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. **本控制面 = 自部署(self-host)**,不是飞书妙搭/TCE 平台托管。
|
||
|
||
### 飞书 APaaS / 妙搭模板(`@lark-apaas/*`)自部署
|
||
|
||
模板里的 `PlatformModule.forRoot()` 会启用平台 HttpClient(`platform.enabled: true`),启动时**必须**有:
|
||
|
||
```text
|
||
FORCE_AUTHN_INNERAPI_DOMAIN=<基础域名>
|
||
```
|
||
|
||
否则:
|
||
|
||
```text
|
||
Error: 平台模式需要基础域名,请设置环境变量 FORCE_AUTHN_INNERAPI_DOMAIN
|
||
```
|
||
|
||
**控制面在 `apply` 时自动注入**(无需用户填妙搭平台域名):
|
||
|
||
| 变量 | 值 |
|
||
|------|-----|
|
||
| `FORCE_AUTHN_INNERAPI_DOMAIN` | 应用公网 URL(`https://buxi*****.DOMAIN_ROOT`) |
|
||
| `FORCE_FRAMEWORK_DISABLE_DATAPASS` | `true`(关掉 TCE datapass 中间件) |
|
||
| `FORCE_FRAMEWORK_ENVIRONMENT` | 默认 `development` |
|
||
| `SERVER_HOST` / `SERVER_PORT` / `PORT` | `0.0.0.0` + `spec.port` |
|
||
| `COMPANY_DEPLOY_SELF_HOST` | `true` |
|
||
|
||
说明:
|
||
|
||
- 这只保证 **进程能启动**;飞书开放平台能力(多维表格等)仍需业务侧配置 `FEISHU_*` / 应用凭证。
|
||
- 若应用硬依赖妙搭登录态,自部署还需业务改登录(本地账号 / 自建 OAuth),与控制面注入无关。
|
||
- 已上线的老应用:再 `apply` 一次,或 Dokploy 手动补上表中变量后 Redeploy。
|
||
|
||
---
|
||
|
||
## 持久化与 `/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
|