听歌 + 练琴曲目管理: - 数据:piece (title/artist/category/lyrics/play_count/notes) + attachments (audio/video/pdf/image; image 带 role=chord/numbered/staff) - 后端 axum + sqlite,附件流式落 PVC,ServeFile 支持 Range(视频拖动) - 前端 guitar 风格 player:左 sidebar + tabs(歌词/吉他谱/简谱/五线谱/PDF/视频),LRC 同步、快捷键、笔记自动保存 - ns cube-music + music.famzheng.me + bodylimit 5GiB - scripts/import_guitar.py 用于把 oci /data/guitar/ 旧曲库导入
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 拍板。
仓库结构:monorepo + cargo workspace
整个 cube 是一个 git 仓库 fam/cube,cargo workspace 模式。
cube/
├── Cargo.toml # workspace manifest
├── crates/
│ └── cube-core/ # 共享样板:base router / healthz / tracing / shutdown / config
├── apps/
│ ├── cube/ # app #0 = cube.famzheng.me 入口门户
│ │ ├── Cargo.toml # path 依赖 cube-core
│ │ ├── src/main.rs
│ │ ├── frontend/ # vite + vue 3 + ts
│ │ ├── Dockerfile
│ │ └── k8s/ # deployment / service / ingress
│ ├── portfolio/
│ ├── repo-vis/
│ └── ...
├── doc/
└── .gitea/workflows/ # CI;按 path 触发,per-app build/deploy
cube-core 是 path 依赖(cube-core = { path = "../../crates/cube-core" }),不发布、不打 tag、不走 git 依赖。改 cube-core 时所有 app 一起 rebuild,monorepo 的整个意义就在于"一次重构,全员同步"。
部署目标
单一目标:famzheng.me 节点的 k3s(kubectl context default),不双轨。
- 每 app 一个 k8s namespace,ns 名 =
cube-<app>(如cube-cube/cube-portfolio),便于跟其他 ns 隔离 + 一眼可见归属 - traefik ingress + 通配符 LE 证书自动签
域名
<app>.famzheng.me,wildcard A 记录已配,零 DNS 操作- 不嵌
cube子域(冲突检查 = 起新 ingress 时 traefik 自然报错,不需要额外心智) - 旧域名(如
portfolio.oci.euphon.net)让 oci ingress 兜底 308 redirect 到新地址,过渡期后下掉
后端:Rust + Axum
每个 app 是 workspace 里的一个 bin crate,单 axum 服务。
cube-core 提供:
/healthzrouter(200 = ok)ServeDir静态前端 fallback 到index.html(SPA 路由兼容)tracing配 JSON stdout- SIGTERM graceful shutdown
- env → struct 配置加载
业务 app 只写 /api/* 路由 + handler:
let app = cube_core::base("dist")
.nest("/api", api_routes());
cube_core::serve(app).await
前端: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。每个 app 的 build 命令固定:
cargo build --release --target x86_64-unknown-linux-musl -p <app>
(cd apps/<app>/frontend && npm ci && npm run build)
Dockerfile(每个 app 自带一份,结构一致):
FROM scratch
COPY target/x86_64-unknown-linux-musl/release/<app> /app
COPY apps/<app>/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/mochi/<app>:<sha> - ingress 已落地:
apps/cube/k8s/_registry-ingress.yaml(用mochibot 账户的 token push)
CI/CD
走 gitea Actions,复用现有 instance-level act_runner(fam 用户,host shell 模式,labels *:host,新 repo 不用注册 runner)。
monorepo 每 app 一份 workflow .gitea/workflows/deploy-<app>.yml,按 paths 触发(只改 apps/<app>/** 和 crates/cube-core/** 才跑)。固定 5 步:
cargo build --release --target x86_64-unknown-linux-musl -p <app>(cd apps/<app>/frontend && npm ci && npm run build)docker build -f apps/<app>/Dockerfile -t registry.famzheng.me/mochi/<app>:$GITHUB_SHA .docker push registry.famzheng.me/mochi/<app>:$GITHUB_SHAkubectl -n cube-<app> set image deploy/<app> <app>=registry.famzheng.me/mochi/<app>:$GITHUB_SHA
host shell PATH 注意:workflow 第一行 export PATH="$HOME/.cargo/bin:$PATH"(fam 的 rustup 装在 ~/.cargo)。
⚠️ workflow
runs-on:只能写 label 名(ubuntu-latest),不要带:host后缀 —— act_runner 注册到 gitea 时只 declare label name,写后缀会一直 queued。
不做 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 - 前端视图状态走 URL(path + query params),保证任意视图(筛选、分页、tab、详情)都能被 bookmark / share。可收藏的 state 必须在 URL 里,不准只活在 Pinia /
ref()里。Vue Router 的useRoute()+router.replace({ query: ... })是默认搭配。
当前状态
- app #0
cube已上线(2026-05-04):cube-core scaffold 完成,入口门户跑在cube.famzheng.me,CI 全链路(cargo musl → npm build → docker push → kubectl rollout)通了。 - app #1
simpleasm已上线(2026-05-04):从 oci 迁过来,跑在asm.famzheng.me。FastAPI 重写为 axum + rusqlite,前端 Vue3 原样搬。oci 端旧域名 308 永久 redirect。 - 迁移源端清单 + 已迁完成的见
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)