README: 平台约定(栈/部署/CI 一锅端)
把脚手架决策定下来,作为 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 文件挂载
This commit is contained in:
@@ -8,24 +8,120 @@ Fam 的小 app 平台。
|
||||
|
||||
主要任务:把目前散落在 `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-core` crate 复用:`cube-core = { git = "https://famzheng.me/gitea/fam/cube-core", tag = "v0.x" }`
|
||||
- `cube-core` 提供:
|
||||
- `/healthz` router(200 = ok)
|
||||
- `ServeDir` 静态前端 fallback 到 `index.html`
|
||||
- `tracing` 配 JSON stdout
|
||||
- SIGTERM graceful shutdown
|
||||
- env → struct 配置加载(`envy` crate)
|
||||
- 业务 app 只写 `/api/*` 路由 + handler:
|
||||
```rust
|
||||
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:
|
||||
|
||||
```bash
|
||||
cargo build --release --target x86_64-unknown-linux-musl
|
||||
(cd frontend && npm ci && npm run build)
|
||||
```
|
||||
|
||||
Dockerfile:
|
||||
|
||||
```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 步:
|
||||
|
||||
1. `cargo build --release --target x86_64-unknown-linux-musl`
|
||||
2. `(cd frontend && npm ci && npm run build)`
|
||||
3. `docker build -t registry.famzheng.me/fam/<app>:$GITHUB_SHA .`
|
||||
4. `docker push registry.famzheng.me/fam/<app>:$GITHUB_SHA`
|
||||
5. `kubectl -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),还没有任何部署清单 / 脚本。
|
||||
- 迁移名单和注意事项见 [`doc/todo.md`](doc/todo.md)。
|
||||
- 仓库刚初始化(2026-05-04),脚手架/cube-core 都还没动手。
|
||||
- 迁移源端清单见 [`doc/todo.md`](doc/todo.md)。
|
||||
|
||||
## 迁移名单(截至 2026-05-04)
|
||||
|
||||
- `portfolio`(host systemd `portfolio.service`,uvicorn `:8890`,**不在 k3s 里**)
|
||||
- `portfolio`(host systemd `portfolio.service`,uvicorn `:8890`,**不在 k3s 里**;迁过来要重写成 Rust + axum 还是直接装 python 运行时?待定)
|
||||
- `repo-vis`
|
||||
- `simpleasm`
|
||||
- `guitar`
|
||||
- `pyroblem`(详情待补)
|
||||
|
||||
## 迁移注意事项
|
||||
|
||||
- **域名**:oci 上 ingress 都挂在 `*.oci.euphon.net` 等域名下。迁过来后切到 `*.famzheng.me`(cube 所在节点的主域),DNS 已是 wildcard A 记录,无需新加。
|
||||
- **portfolio 特殊**:host systemd 跑的,不在 k3s 里。迁移时要么沿用 systemd(直接放本机 `/home/fam` 下跑),要么打 docker image 进 k3s。
|
||||
|
||||
## 相关
|
||||
|
||||
- 宿主节点:`famzheng.me` / hostname `famzheng.com` —— 详见 `~/.claude/memory/infra.md`
|
||||
|
||||
Reference in New Issue
Block a user