159 lines
7.3 KiB
Markdown
159 lines
7.3 KiB
Markdown
# 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` 提供:
|
||
- `/healthz` router(200 = ok)
|
||
- `ServeDir` 静态前端 fallback 到 `index.html`(SPA 路由兼容)
|
||
- `tracing` 配 JSON stdout
|
||
- SIGTERM graceful shutdown
|
||
- env → struct 配置加载
|
||
|
||
业务 app 只写 `/api/*` 路由 + handler:
|
||
|
||
```rust
|
||
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 命令固定:
|
||
|
||
```bash
|
||
cargo build --release --target x86_64-unknown-linux-musl -p <app>
|
||
(cd apps/<app>/frontend && npm ci && npm run build)
|
||
```
|
||
|
||
Dockerfile(每个 app 自带一份,结构一致):
|
||
|
||
```dockerfile
|
||
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`(用 `mochi` bot 账户的 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 步:
|
||
|
||
1. `cargo build --release --target x86_64-unknown-linux-musl -p <app>`
|
||
2. `(cd apps/<app>/frontend && npm ci && npm run build)`
|
||
3. `docker build -f apps/<app>/Dockerfile -t registry.famzheng.me/mochi/<app>:$GITHUB_SHA .`
|
||
4. `docker push registry.famzheng.me/mochi/<app>:$GITHUB_SHA`
|
||
5. `kubectl -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`](doc/todo.md)。
|
||
|
||
## 迁移名单(截至 2026-05-04)
|
||
|
||
- `portfolio`(host systemd `portfolio.service`,uvicorn `:8890`,**不在 k3s 里**;迁过来要重写成 Rust + axum 还是直接装 python 运行时?待定)
|
||
- `repo-vis`
|
||
- `simpleasm`
|
||
- `guitar`
|
||
- `pyroblem`(详情待补)
|
||
|
||
## 相关
|
||
|
||
- 宿主节点:`famzheng.me` / hostname `famzheng.com` —— 详见 `~/.claude/memory/infra.md`
|
||
- 迁移源:`oci.euphon.net`(ARM Ubuntu 22.04 + k3s)
|