Compose 部署说明
deploy/compose/ 提供两种部署方式、三个模板文件:Lite(SQLite + memory,单容器)与生产(PostgreSQL + Redis,多容器)。
模板可用性(如实说明):所有模板均经过
docker compose config严格校验。Lite 形态已收敛为包含 Backend、Editor、Runtime、Renderer(内置 Chromium)和 Gateway 的单镜像容器;生产两种模板共享生产镜像栈。
官方发布的 Platform、SQLite Lite、Runtime 和 Renderer 镜像同时支持 linux/amd64 与 linux/arm64。Compose 文件不固定 platform,Docker 会按宿主机架构自动选择镜像;需要在本机交叉构建 ARM64 镜像时,应使用已启用 QEMU 的 Buildx 环境。
模板
| 文件 | 场景 | 特点 |
|---|---|---|
deploy/compose/compose.sqlite-lite.yml | 个人/小团队轻量部署(Lite 单容器) | platform-lite 单容器内置 Backend、Editor、Runtime、Renderer(内置 Chromium)和 Gateway。零必填变量、零密钥文件,开箱即用。使用 SQLite 与 memory runtime |
deploy/compose/compose.prod.yml | 小团队生产部署(runtime-all 兼容) | 拆分迁移、Backend、Runtime、Renderer 和 Gateway,通过 deploy/.env 管理变量;单 Runtime 同时承担 preview/build/check |
deploy/compose/compose.runtime-roles.yml | 官方生产主路径(分角色多容器) | runtime-preview / runtime-build / runtime-check 各一实例 + Renderer,每角色显式 CPU/内存 limits 与执行预算;Gateway 只代理预览 |
SQLite 轻量单容器
cd deploy
docker compose -f compose/compose.sqlite-lite.yml config
docker compose -f compose/compose.sqlite-lite.yml pull
docker compose -f compose/compose.sqlite-lite.yml up -d默认拉取 llmxpm/web-presentation:sqlite-lite,访问 http://127.0.0.1:8080。该模式不启动 PostgreSQL 和 Redis,DATABASE_URL 指向 /app/backend/data/web_presentation.db,REDIS_URL 使用 memory://lite(正式运行态适配器)。lite-data volume 同时保存 SQLite 数据库、本地资源、截图、构建产物和 Runtime RSA 私钥。重启后临时预览/构建运行态会丢失,主数据不丢;边界见 运行态存储适配器。
需要从源码验证轻量镜像时,直接在仓库根目录执行构建;deploy/docker/Dockerfile.lite 会把仓库原生的 runtime/ 源码和依赖一起打进单容器镜像。
docker build -f deploy/docker/Dockerfile.lite -t llmxpm/web-presentation:sqlite-lite .交叉构建 ARM64 轻量镜像时,可执行:
docker buildx build --platform linux/arm64 -f deploy/docker/Dockerfile.lite -t llmxpm/web-presentation:sqlite-lite-arm64 --load .轻量模式的 platform-lite 只支持单实例、单 Backend worker、单 Runtime server,不适合多副本或高并发写入。推荐规模(目标,不是 SLA):5–10 人小团队、预览并发约 3、常规演示文稿工作区;容量验收(D2)前不得当作硬承诺。故障域:Backend + Runtime + Nginx(及当前形态下的渲染)合并故障域,容器重启会丢失预览/构建临时运行态,业务数据(SQLite / lite-data)不丢。详见 Lite 规模与隔离决策。容器重启后短生命周期预览链接、内存锁和内存构建状态会失效,但用户、工作空间、项目、页面、资源和 AI 会话等主数据会保留在 SQLite 文件中。
production env 版(runtime-all 兼容 / Lite)
官方生产主路径请用下一节的分角色单机版。 本模板保留给小团队、兼容部署和尚未迁移到分角色拓扑的环境:单 Runtime 进程同时承担 preview/build/check,资源互相争抢,且 Runtime 容器会读取整份
deploy/.env。
cp deploy/.env.example deploy/.env
cd deploy
docker compose -f compose/compose.prod.yml config
docker compose -f compose/compose.prod.yml pull
docker compose -f compose/compose.prod.yml up -dproduction env 版适合把环境变量集中放在 deploy/.env 中维护。外部 PostgreSQL 和 Redis 需要提前准备。
分角色单机(Runtime 角色)— 官方生产主路径
deploy/compose/compose.runtime-roles.yml 把 Runtime 拆成 runtime-preview、runtime-build、runtime-check 三个角色容器(RUNTIME_ROLE 分别为 preview、build、check),与 Backend、迁移、Renderer 和 Gateway 组成分角色单机拓扑。Backend 仍从 deploy/.env 读配置;Runtime 三角色只读 deploy/runtime.env(复制 deploy/runtime.env.example),避免 DATABASE_URL / REDIS_URL / AI_SECRET_ENCRYPTION_KEY / RUNTIME_RSA_PRIVATE_KEY 等平台密钥进入会编译用户手写 SFC 的容器。runtime.env 中的域名、audience、JWKS 与路径必须与 deploy/.env 同名项一致,否则预览资源会指向错误域名或令牌校验失败。角色差异和资源约束写在模板内。
构建执行前提: 项目构建由 runtime-build 内的 Build Worker 通过 POST /internal/runtime/build-jobs/claim 拉取,续租走 .../{job_id}/renew,终态走 .../complete。Backend 与 runtime-build 必须持有相同的构建 Worker 凭证:分角色模板通过 Docker secret deploy/secrets/build_worker_credential 挂载(RUNTIME_BUILD_WORKER_CREDENTIAL_FILE),也可在两侧环境变量设置相同的 RUNTIME_BUILD_WORKER_CREDENTIAL。缺省时 Backend 拒绝领取(503 fail-closed)且 Worker 不启动,构建任务会一直停在 pending 直到总期限被收敛为失败。生产环境请使用强随机值,不要沿用示例占位符。
构建拉取模型的行为边界:
- 构建只有拉取这一条执行路径:Backend 不再向 Runtime 同步派发构建,Runtime 也不暴露构建 HTTP 入口。凭证缺失导致 Worker 未启动时不会多出任何执行路径,任务只留在队列里等待人工介入。
- secret 文件权限:该凭证具备跨工作空间领取任务的能力,挂载文件必须
0400/0600。构建子进程(Vite/Rollup、ZIP 归档)降权到rtchild(UID 10001),既不继承RUNTIME_BUILD_WORKER_CREDENTIAL(_FILE),也无权读取 secret 文件。已配置RUNTIME_BUILD_CHILD_UID/GID时,凭证对组/其他用户可读会先尝试chmod 0400自动收紧,仍过宽则 fail-closed(RUNTIME_BUILD_CREDENTIAL_LOOSE_MODE)。Compose secrets 的mode/uid/gid必须写在服务级长语法(顶层 secrets 不接受这些字段,写错会退回 0444);file secrets 多数实现是 bind-mount,请在宿主机chmod 400。详见执行隔离与权限矩阵。 - 子进程降权:镜像内置
RUNTIME_BUILD_CHILD_UID/GID=10001,主进程必须以 root 运行才能 setuid(镜像默认USER root)。cap_add: [SETUID, SETGID]对非 root 无效,不要依赖它。降权失败时构建以RUNTIME_BUILD_CHILD_IDENTITY_EPERM失败;不要通过清空 CHILD_UID「绕过」——那会退回同 UID,子进程可读/proc/<parent>/environ,W01 不再成立。 - 单实例并发:pull 模式下领取消费者数量等于 project lane 并发(
RUNTIME_VITE_TASK_CONCURRENCY,多消费者共享同一有界调度器),因此调高该预算才会真正增加单实例同时执行的构建任务数。 - 绝对期限:
PROJECT_BUILD_TOTAL_DEADLINE_SECONDS是任务创建时确定的 wall-clock 时刻,领取租约、attempt 令牌 TTL、续租后的新租约和 Runtime 执行预算都裁剪到该时刻之前;Worker 与 Backend 失联时也会在本地租约到期前主动中止构建。 - 归档接收上限:产物回传按分片流式写入对象存储(本地驱动临时文件 + 原子改名,S3 驱动分片上传并在失败时 abort),大小与 sha256 在写入过程中算出,超过
PROJECT_BUILD_ARTIFACT_MAX_BYTES立即中止;Backend 不会把整包归档读进进程内存。
生成 secret 示例:
python -c "import secrets; print(secrets.token_urlsafe(32))" > deploy/secrets/build_worker_credential
chmod 400 deploy/secrets/build_worker_credentialcp deploy/.env.example deploy/.env
cp deploy/runtime.env.example deploy/runtime.env
# 同步修改两份文件中的域名 / audience / 路径,保持一致
cd deploy
docker compose -f compose/compose.runtime-roles.yml config
docker compose -f compose/compose.runtime-roles.yml pull
docker compose -f compose/compose.runtime-roles.yml up -d拓扑与暴露面
| 服务 | 网络 | 宿主机端口 | 说明 |
|---|---|---|---|
runtime-preview | platform-net(别名 runtime、runtime-preview) | 无 | 预览角色;Gateway Nginx 固定代理主机名 runtime |
runtime-build | runtime-jobs-net | 无 | 构建角色;只出站领取 Backend,不接收入站构建请求 |
runtime-check | runtime-jobs-net | 无 | 源码检查角色;仅 Backend 可达 |
renderer | platform-net | 无 | 截图执行 |
backend | platform-net + runtime-jobs-net(别名 backend) | 无 | 两个网络的唯一交点 |
gateway | platform-net | 80 | 唯一公开入口,只代理预览角色 |
runtime-build / runtime-check 不发布宿主机端口,且不加入 platform-net;公开 Gateway 挂在 platform-net 上,只能通过主机名 runtime 到达 runtime-preview,构建与源码检查端点不经公开 Gateway 暴露,只在 runtime-jobs-net 内可达:构建任务由 runtime-build 主动出站领取 Backend(Backend 也在该网络上),源码检查则由 Backend 经该网络调用 runtime-check。浏览器访问关系与下文「访问关系」一致。
容器资源约束与执行预算(示例值,需按实测调整)
Docker 容器默认不施加 CPU/内存限制;只把一个 Runtime 拆成三个容器不会自动消除宿主机上的资源争抢。模板为每个角色设置 deploy.resources.limits,并同时给出匹配的 Runtime 内部执行预算,两者配套修改才算完成隔离。取值依据:
- CPU limits 取「可同时执行的 Worker 数 + 1 核」,给主进程与事件循环留出余量。
- 内存 limits ≈
RUNTIME_VITE_TASK_CONCURRENCY × RUNTIME_BUILD_WORKER_MAX_OLD_SPACE_MB(V8 老生代堆)+ 等量非堆开销(Buffer、文件页、归档、子进程)+ 主进程余量。--max-old-space-size只限制 V8 老生代堆,不能当作 Worker 或容器的内存上限。 - 下表按每角色 2 个并发 Worker 给出示例值。它们不是承诺值:应结合阶段 0 指标(构建归档阶段峰值 RSS、检查队列年龄、预览事件循环延迟)压测后覆盖,并在调整并发或老生代预算时同步调整 limits。
| 服务 | CPU limits | 内存 limits | 对应执行预算(关键项) |
|---|---|---|---|
runtime-preview | 2 | 2Gi | RUNTIME_VITE_TASK_CONCURRENCY=2;无构建 Worker 预算(角色门禁后不应有构建/诊断任务) |
runtime-build | 4 | 8Gi | RUNTIME_VITE_TASK_CONCURRENCY=2,RUNTIME_BUILD_WORKER_MAX_OLD_SPACE_MB=2048,队列 8 |
runtime-check | 2 | 6Gi | RUNTIME_VITE_TASK_CONCURRENCY=2,RUNTIME_BUILD_WORKER_MAX_OLD_SPACE_MB=1536,诊断 Worker 复用并按 RSS 比例回收 |
renderer | 2 | 4Gi | 渲染并发与队列仍由 deploy/.env 中 RENDER_* 决定 |
backend | 2 | 2Gi | AI 页面与渲染队列仍由 deploy/.env 决定 |
gateway | 1 | 512Mi | — |
健康检查与依赖顺序
Runtime 暴露两个探针,语义严格分离:
| 端点 | 语义 | 失败含义 |
|---|---|---|
GET /__runtime_healthz | 存活(liveness):进程与 Vite server 还在,附带角色、版本指纹与容量快照 | 进程卡死,应重启 |
GET /__runtime_readyz | 就绪(readiness):本角色声明的执行面是否真的可用 | 进程活着但不能承接该角色的任务,不得放行依赖 |
三个角色容器的 compose healthcheck 都打 /__runtime_readyz,探针只看本进程,不依赖 Backend。runtime-build 的就绪条件是「调度器未关闭 + 领取循环在运行 + 消费者数量等于 project lane 并发」;凭证缺失或 Worker 未启动时返回 503,depends_on: service_healthy 因此不会放行一个永远不会领取构建任务的空转副本。RUNTIME_ROLE=build 且凭证不可读或 Backend 地址缺失时,Runtime 直接启动失败(进程退出、容器重启),不再降级成「启动成功但 Worker disabled」。启动顺序为:renderer 与三个 runtime 角色就绪 → backend-migrate 成功完成 → backend 健康 → gateway 健康。Gateway 只依赖 backend 与 runtime-preview;runtime-check 由 Backend 按需调用,runtime-build 只主动出站领取任务,两者都不进入公开就绪链路。
镜像级 HEALTHCHECK 与 scripts/contracts/check-image-startup.py 仍使用 /__runtime_healthz:它们验证的是「交付镜像能否启动」,不绑定角色执行面。
RUNTIME_ROLE 由 Runtime 角色逻辑消费:preview 不开放诊断与轻量工具入口,build 只跑构建领取 Worker,check 只保留诊断与轻量工具执行面。模板已配置好 preview 与 check 两个 Backend 出站目标(RUNTIME_PREVIEW/CHECK_BASE_URL);不要把 RUNTIME_BASE_URL 单独指向 runtime-preview 后再逐项切换——preview 实例已注销诊断与轻量工具端点,那样会把请求打回 Vite 并收到 HTML/404 而不是结构化错误。改角色拓扑时同步改对应 RUNTIME_*_BASE_URL(S)。
计算副本扩容(Check/Build 多副本与全链路准入)
按实测瓶颈需要增加 runtime-build / runtime-check 副本时(规划 T2-3):
- 为新副本复制对应角色服务定义,改用独立容器名/别名(如
runtime-build-2),保持RUNTIME_ROLE与执行预算一致;副本只挂runtime-jobs-net,仍不发布宿主机端口。Build 副本必须与 Backend 使用同一领取凭证,但容器间不需要互相可见。 - Build 副本无需注册:每个
runtime-build实例自行claim,任务在数据库层被条件更新认领,天然互斥;加副本只增加消费者总数,Backend 侧不再有 build 选址或准入配置。 - Check 副本需要注册:在 Backend 环境用
RUNTIME_CHECK_BASE_URLS登记全部副本内网地址(逗号分隔或 JSON 数组)。Backend 按轮询选址;某副本满载(429/503)时自动切换其它空闲副本,全部满载返回稳定错误码RUNTIME_CAPACITY_EXCEEDED(503,可重试)。目标连续失败达到RUNTIME_TARGET_FAILURE_THRESHOLD后按RUNTIME_TARGET_COOLDOWN_SECONDS短暂冷却,冷却到期自动恢复。 - 按「副本数 × 单副本执行预算」上调
RUNTIME_CHECK_MAX_INFLIGHT(Check 仍走 Backend 在途计数):在途调用超限立即返回RUNTIME_ADMISSION_FULL(503,可重试),避免 Check 扩容后把 Renderer/Preview 打穿。preview仍保持单副本,不参与多目标选址。
Runtime 本地队列(RUNTIME_VITE_TASK_*)仅承担单实例容量保护,不做全局公平;Check 的跨副本分摊由上述选址与准入负责,Build 的跨副本分摊由持久任务的租约认领负责。满载与准入拒绝均属可重试容量问题,不应记为业务失败。
预览多副本与滚动发布
公开预览池由 Gateway Nginx 的 upstream runtime_preview_pool 承载(deploy/docker/nginx/web-presentation.conf)。实例列表可直接编辑后 nginx -s reload 更新,不引入服务发现组件:
- 扩容:复制
runtime-preview服务为runtime-preview-2(独立别名,见compose.runtime-roles.yml中注释示例),取消 Gateway 配置中对应server注释后 reload。每个server声明带max_fails/fail_timeout被动摘流,连续失败的实例在窗口内不再路由,到期自动恢复;可重试请求经proxy_next_upstream交给池内其它实例。WebSocket Upgrade 由$connection_upgrade映射透传,软亲和(如ip_hash)只能作为缓存命中优化,不能成为正确性前提——任一副本必须凭当前请求和受信 Backend 独立完成预览鉴权与 artifact 读取。 - 版本指纹:
GET /__runtime_healthz返回runtime_kit_version与build_id。镜像默认使用构建阶段生成的内容 hash,RUNTIME_BUILD_ID仅作同版统一覆盖;副本名称放入RUNTIME_INSTANCE_ID,不参与指纹。Backend 绑定签名版本,模块/CSS/HMR 使用版本路径;跨版请求明确拒绝,完整浏览器验收前按兼容矩阵保持同版池。 - 跨版排空切换顺序:
- 新副本就绪:启动新版预览副本,等待 healthcheck(
/__runtime_readyz)通过,并确认/__runtime_healthz的runtime_kit_version/build_id为目标版本。 - 暂停新入口并排空:新版暂不加入旧池;暂停新的预览入口,等待旧连接和子请求结束。HMR 长连接设定排空上界,超时强制下线并记录。
- 整体流量切换:将预览池整体切换为新版并 reload,再恢复入口。旧预览跨版会明确拒绝,需刷新;不得据此承诺滚动无中断。
- 旧副本下线:排空完成后停止旧容器。
- 新副本就绪:启动新版预览副本,等待 healthcheck(
- 连接排空:下线前先在 upstream 中将实例标记
down(或移出列表)并 reload,再停容器;不要先停容器导致在途请求 502。副本故障或重启后可由池内其它副本恢复,本地缓存丢失不影响正确性。
Renderer Worker 扩容与摘除
Renderer 仍是唯一浏览器执行角色;扩容使用不同 worker_id 与受信直达地址(RENDER_WORKERS_CONFIG),每个 Worker 进程持有自己的 RENDER_WORKER_ID / RENDER_WORKER_EPOCH:
新增 Worker:启动独立 Renderer 容器,设置唯一
RENDER_WORKER_ID与直达base_url,追加进RENDER_WORKERS_CONFIG后重启 Backend(或滚动替换)。Worker 心跳会注册当前 epoch;同worker_id的旧 epoch 会被隔离,避免陈旧行再次派发。摘除前核对未释放 attempt:停止接收新派发前,必须执行只读门禁 CLI:
powershelluv run --project backend python -m app.scripts.check_render_worker_removal --worker-id <id> --format summary退出码
0表示可安全摘除,2表示仍有active_occupancy=1的 attempt。仍有占用时等待租约收敛、取消在途 attempt,或接受超时后由协调器重试到其它 Worker。禁止只删配置行就下线。epoch 变更:Worker 重启会生成新 epoch;旧 epoch 的迟到回执不得覆盖新结果。摘除旧实例前核对未释放 attempt,不要只删配置行。
访问关系
浏览器访问平台 Gateway。Gateway 代理 /api、/public、/build-artifacts、/preview 到 Backend,并代理 /runtime/ 到 Runtime。旧 /media 及其子路径由 Gateway 返回 404,Backend 也不再提供该静态挂载。