把脚手架决策定下来,作为 cube 上所有 app 必须遵守的"宪法": - 域名:<app>.famzheng.me,零 DNS 操作 - 后端:Rust + Axum,每 app 独立仓库,cube-core crate 复用样板 - 前端:Vite + Vue 3 + TS(选 Vue 是因为 AI 写得稳) - 构建:host musl 编译 + scratch 容器 - Registry:gitea 自带(registry.famzheng.me 反代 /v2/*) - CI:gitea Actions + 现有 instance-level act_runner - 通用约定:/healthz, stdout JSON log, env+Secret 配置, 禁 config 文件挂载
cube
Fam 的小 app 平台。
是什么
cube 是一个跑在 famzheng.me 节点(hostname famzheng.com,单节点 k3s + traefik + gitea,公网 IP 178.104.186.206)上的小 app 平台,专门收纳 Fam 自己写的一堆小工具/玩具 web app。底层硬件、k3s、Gitea、act_runner 等基础设施详见 ~/.claude/memory/infra.md。
主要任务:把目前散落在 oci.euphon.net(Oracle Cloud ARM VM)上值得留下的 Fam 个人小 app 迁过来。oci 主机本身不退役,留给 Hera 同学继续用。
设计目标:反 Karpathy "web app 像拼宜家家具" 的困境 —— 全部自有基础设施,零跨 dashboard 配置,新 app 上线压缩到 5 分钟内。
平台约定
这是 cube 上所有 app 的"宪法",所有 app 必须遵守,破例必须改这份文档并经 Fam 拍板。
部署目标
单一目标:famzheng.me 节点的 k3s(kubectl context default),不双轨。
- 每 app 一个 k8s namespace(ns 名 = app 名,不加 cube- 前缀)
- traefik ingress + 通配符 LE 证书自动签
域名
<app>.famzheng.me,wildcard A 记录已配,零 DNS 操作- 不嵌
cube子域(冲突检查 = 起新 ingress 时 traefik 自然报错,不需要额外心智) - 旧域名(如
portfolio.oci.euphon.net)让 oci ingress 兜底 308 redirect 到新地址,过渡期后下掉
后端:Rust + Axum,每 app 独立仓库
- gitea repo
fam/<app>,单 axum 服务 - 公共代码通过
cube-corecrate 复用:cube-core = { git = "https://famzheng.me/gitea/fam/cube-core", tag = "v0.x" } cube-core提供:/healthzrouter(200 = ok)ServeDir静态前端 fallback 到index.htmltracing配 JSON stdout- SIGTERM graceful shutdown
- env → struct 配置加载(
envycrate)
- 业务 app 只写
/api/*路由 + handler:Router::new() .merge(cube_core::base("dist")) .nest("/api", api_routes()) - 不上 cargo workspace —— 每 app 一个 repo 彻底分
前端:Vite + Vue 3
- 默认栈:Vite + Vue 3 + TypeScript + Pinia + Vue Router
- 选 Vue 而不是 Svelte 的原因:AI(Claude / GPT)写 Vue 3
<script setup>+ Composition API 训练语料密度大、bug 少;Svelte 5 runes 出来后 AI 经常混 4/5 语法 - build 输出到
frontend/dist/,axum 用ServeDir同进程同容器同域名 serve - 不搞前后端分离部署、不搞独立前端域名、不搞独立 CI
构建:host musl + scratch 容器
host 上直接 cargo build,不在容器里跑 cargo:
cargo build --release --target x86_64-unknown-linux-musl
(cd frontend && npm ci && npm run build)
Dockerfile:
FROM scratch
COPY target/x86_64-unknown-linux-musl/release/<app> /app
COPY frontend/dist /dist
ENTRYPOINT ["/app"]
- 镜像 < 20MB
- runner 上不跑 docker buildx 套娃,build 速度爆表
- prereq(首次设置 host):
apt install musl-tools+rustup target add x86_64-unknown-linux-musl
Container Registry
- 用 gitea 自带 Container Registry(Packages 功能,gitea 1.20+ 自带)
- 坑:gitea 挂在
/gitea/子路径下,docker daemon 默认拼https://famzheng.me/v2/...会 404 - 方案:加一条 ingress
registry.famzheng.me反代/v2/*→gitea-svc:3000/v2/*,复用 gitea token 鉴权 - 镜像命名:
registry.famzheng.me/fam/<app>:<sha> - 未实施:第一个 app 上线时再搞 ingress;过渡期可本地 build 直接
docker save+k3s ctr image import
CI/CD
走 gitea Actions,复用现有 instance-level act_runner(gnoc 用户,host shell 模式,labels *:host,新 repo 不用注册 runner)。
每个 app repo 一份 .gitea/workflows/deploy.yml,固定 5 步:
cargo build --release --target x86_64-unknown-linux-musl(cd frontend && npm ci && npm run build)docker build -t registry.famzheng.me/fam/<app>:$GITHUB_SHA .docker push registry.famzheng.me/fam/<app>:$GITHUB_SHAkubectl -n <app> set image deploy/<app> <app>=registry.famzheng.me/fam/<app>:$GITHUB_SHA
host shell PATH 注意:workflow 第一行 export PATH="$HOME/.cargo/bin:$PATH"(gnoc 的 rustup 装在 ~/.cargo)。
不做 PR 预览环境 —— 个人小 app 不需要,徒增复杂度。
通用约定(每个 app 都要遵守)
- 暴露
/healthz(200 = ok),k8s liveness/readiness 共用同一个 endpoint - 日志统一 stdout(
cube-core的 tracing 配 JSON),让 k3s 收 - 配置走 env var + k8s Secret,禁止 config.yaml 文件挂载(partiverse 那个"空目录顶替 config 文件"的坑别再踩)
- 数据持久化只走 PVC + 每天 minio backup CronJob(
cube-core提供 base 模板) - 镜像 tag 用 git SHA,不用
latest
当前状态
- 仓库刚初始化(2026-05-04),脚手架/cube-core 都还没动手。
- 迁移源端清单见
doc/todo.md。
迁移名单(截至 2026-05-04)
portfolio(host systemdportfolio.service,uvicorn:8890,不在 k3s 里;迁过来要重写成 Rust + axum 还是直接装 python 运行时?待定)repo-vissimpleasmguitarpyroblem(详情待补)
相关
- 宿主节点:
famzheng.me/ hostnamefamzheng.com—— 详见~/.claude/memory/infra.md - 迁移源:
oci.euphon.net(ARM Ubuntu 22.04 + k3s)