Tech Stack & Deployment

技术栈与部署

本页只写这个项目真正会用到的组件,不堆砌名词。整套系统跑在一台 Ubuntu 26.04 LTS 上:Nginx 在门口,Nuxt 负责 SSR,Rust 提供业务 API,MySQL 存主数据,Redis 管会话与热度,Meilisearch 做检索。

操作系统
Ubuntu 26.04
常驻服务
7 个
存储引擎
MySQL 8.4
初始部署
单机

技术选型与取舍

每个选择都写清「为什么是它」和「为什么不是别的」。选型的标准是:能少一个进程就少一个进程,能少一个需要运维的东西就少一个。

操作系统

Ubuntu 26.04 LTS

✓长期支持版本,软件源新(MySQL 8.4 / Redis 7 可直接 apt 安装),社区资料最多,出问题最容易搜到答案。

✕不上 Debian testing / Arch:服务器要的是可预测,不是最新。

边缘与接入

Nginx

✓TLS 终止、HTTP/2、静态资源、gzip/brotli、反向代理与限流一站式解决;本身极稳定,几乎不需要调优。

✕不上 Caddy:自动 HTTPS 很方便,但可预期的配置能力与排错资料不如 Nginx。

前端

Nuxt 4 SSR(Vue 3 SFC + Tailwind 4)

✓SSR 直出保证社区内容可被搜索引擎抓取;SFC 与动态组件加载天然适合「每个菜单一个独立页面」的架构。

✕不做纯 SPA:社区的核心价值是内容可被检索与分享,纯 SPA 在 SEO 上先天吃亏。

后端

Rust + axum(tokio)

✓内存安全 + 低资源占用,单机小配置也能扛住社区读写;axum 基于 tower,中间件生态与类型安全都很好。

✕不上 Node 后端:SSR 已经占了 Node 进程,业务再挤进去会让内存与 CPU 争抢变得难以定位。

主数据库

MySQL 8.4 LTS

✓窗口函数、CTE、JSON 类型与函数索引足够支撑评论树与菜单树;运维生态最成熟,备份恢复方案最省心。

✕不上 PostgreSQL:能力上完全够用,但本项目的运维熟练度在 MySQL,降低踩坑概率优先。

缓存与队列

Redis 7

✓一份 Redis 解决五件事:会话、热点缓存、ZSet 热度排行、限流计数、outbox 的轻量调度。

✕不引入 Kafka / RabbitMQ:在这个规模下只是多一个要运维的进程。

全文检索

Meilisearch

✓开箱即用的中文分词与容错搜索,毫秒级响应,单二进制部署;对社区「搜帖子 / 搜词条 / 搜人」的场景足够。

✕不上 Elasticsearch:JVM 内存开销在单机上会与 MySQL 直接冲突,收益不成比例。

媒体

本地目录 + Nginx 直出

✓V1 只做图片与短链视频,落盘 + Nginx 托管是最简单可靠的方案,不额外引入对象存储依赖。

✕不立刻上云对象存储:等流量真的上来再迁,接口抽象提前留好即可。

单机部署拓扑

初始阶段所有服务跑在一台机器上,但边界已经划清:只有 Nginx 对外,其余服务全部只监听 127.0.0.1。这样后续把 MySQL 或 Meilisearch 搬到独立主机时,配置改动很小。

浏览器SSR 首屏直出443Ubuntu 26.04 LTS · 单台服务器(东京)仅 22 / 80 / 443 对外开放NginxTLS · HTTP/2 · gzip静态资源 · 限流//api/*Nuxt Nitro:3000(本机) · SSRRust axum:8080(本机) · 业务 APIruyi-workeroutbox 消费 · 异步任务MySQL 8.4 LTS:3306 · InnoDB / utf8mb4Redis 7:6379 · 会话 / 缓存 / ZSet / 队列Meilisearch:7700 · 全文检索全部只监听 127.0.0.1本地磁盘/srv/ruyi/media · /_nuxt由 Nginx 直接托管三条关键数据流读:浏览器 → Nginx → Nitro(SSR 查 MySQL / 命中 Redis 缓存)→ HTML 直出写:浏览器 → Nginx → axum → MySQL 事务(含 outbox 事件)→ 返回 ID异步:worker 轮询 outbox → 写 Meilisearch 索引 / 生成通知 / 刷新统计失败处理:索引失败只影响搜索,不阻塞发帖;Redis 不可用时读请求直连 MySQL 并降级。
服务端口职责托管方式
nginx80 / 443反向代理、TLS、静态资源、限流systemd / apt
ruyi-web(Nuxt Nitro)3000(仅本机)SSR 渲染、server routes 聚合systemd
ruyi-api(Rust axum)8080(仅本机)业务 API,唯一写入口systemd
ruyi-worker(Rust)—outbox 消费:索引同步、通知扇出、统计刷新systemd
mysql3306(仅本机)主数据存储,InnoDB + utf8mb4systemd / apt
redis6379(仅本机)会话、缓存、ZSet、限流、轻量队列systemd / apt
meilisearch7700(仅本机)全文检索,仅由 api / worker 访问systemd(官方二进制)

数据层约定

Redis 的键名与 Meilisearch 的索引字段必须提前约定好。这两处一旦乱掉,后期排查成本极高 —— 因为你无法从数据本身看出它属于谁。

Redis 键名规范

Key类型过期
sess:{session_id}HashTTL 7d
menu:tree:{community_id}String(JSON)无过期(写时删)
hot:{community}:{range}ZSetTTL 10m
vote:count:{type}:{id}HashTTL 1h
rate:{scope}:{key}StringTTL 60s
unread:{user_id}String无过期
outbox:lockStringTTL 30s

统一前缀 {域}:{对象}:{标识},冒号分隔;禁止无过期时间的缓存键(菜单树除外,它由写操作主动删除)。

Meilisearch 索引定义

posts

可搜索
title, body_text, tags, author_name, community_name
可筛选
community_id, author_id, post_type, created_at
备注
sortable: hot_score, created_at

comments

可搜索
body_text, author_name
可筛选
community_id, post_id
备注
不索引已删除评论

users

可搜索
nickname, bio, community_names
可筛选
trust_level
备注
排除已注销账号

pages

可搜索
title, body_text, slug_path
可筛选
community_id, page_type, status=1
备注
菜单 slug 页面,用于站内搜索直达

menu_items

可搜索
label, description, slug
可筛选
community_id
备注
命令面板(⌘K)的跳转来源

Nginx 配置要点

核心是「让 Nginx 干它擅长的事」:静态与媒体直接托管,动态请求才转发给 Node;请求 ID 在门口就生成,贯穿到 Rust 与日志。

# /etc/nginx/sites-available/ruyi.conf
server {
  listen 443 ssl http2;
  server_name community.example.com;

  ssl_certificate     /etc/letsencrypt/live/community.example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/community.example.com/privkey.pem;

  client_max_body_size 20m;          # 图片/短视频上传
  gzip on; gzip_types text/css application/javascript application/json image/svg+xml;

  # 静态产物与媒体直接由 Nginx 托管,不经过 Node
  location /_nuxt/ { alias /srv/ruyi/web/.output/public/_nuxt/; expires 1y; add_header Cache-Control "public, immutable"; }
  location /media/  { alias /srv/ruyi/media/;        expires 30d; add_header Cache-Control "public"; }

  # API 与限流
  location /api/ {
    limit_req zone=api burst=20 nodelay;
    proxy_pass http://127.0.0.1:8080;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Request-Id $request_id;   # 请求 ID 贯穿全链路
  }

  # 其余交给 Nuxt SSR
  location / {
    proxy_pass http://127.0.0.1:3000;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_set_header Host $host;
    proxy_set_header X-Request-Id $request_id;
  }
}
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/s;

仓库结构

前后端分离两个子项目,加一个 deploy 目录集中放运维资产。

ruyi-web/

Nuxt 4 前端

  • app/pages/
  • app/components/
  • app/data/
  • nuxt.config.ts

ruyi-api/

Rust 工作区

  • crates/api/
  • crates/domain/
  • crates/infra/
  • migrations/

deploy/

部署与运维

  • nginx/ruyi.conf
  • systemd/*.service
  • backup.sh
  • deploy.sh

部署步骤

从裸机到可访问的完整顺序。每一步都应该可重复执行,避免「只有那台机器能跑」。

  1. 1装基础环境apt 更新后安装 nginx、mysql-server、redis-server、build-essential、certbot;用 ufw 只放行 22/80/443。
  2. 2准备数据服务MySQL 建库(utf8mb4_0900_ai_ci)+ 专用账号;Redis 设 maxmemory 与淘汰策略;Meilisearch 装官方二进制并设 master key。
  3. 3跑迁移在 ruyi-api 执行 sqlx migrate run,建表与初始数据;确认索引与唯一键全部就位。
  4. 4构建前端npm ci && npm run build 产出 .output;静态资源交给 Nginx,Node 只跑 Nitro。
  5. 5构建后端cargo build --release 产出 ruyi-api / ruyi-worker 两个二进制,放到 /usr/local/bin。
  6. 6注册服务写三个 systemd 单元(web / api / worker),设置 Restart=always 与内存上限,enable 后启动。
  7. 7配 Nginx 与证书写入站点配置,certbot --nginx 申请证书并开启自动续期,验证 301 与 HSTS。
  8. 8备份与验证加每日 mysqldump + binlog 归档;跑一次「删库 → 恢复」演练,确认 RTO 达标。

单机部署最容易踩的坑

单机的优势是简单,代价是「所有资源都在同一个池子里」。下面这些是必须先立规矩的地方。

磁盘水位

单机最容易先撑爆磁盘。日志、媒体、binlog 三项都要设保留策略并告警。

内存分配

MySQL 与 Meilisearch 最容易互相抢内存。给两者都设上限,预留 2GB 给系统与 Node。

备份恢复

备份没验证过等于没有备份。每月至少做一次恢复演练。

连接池

Rust 侧连接池大小要与 MySQL max_connections 匹配,否则高并发下会自我排队。

证书续期

certbot 定时任务要监控,续期失败的后果是整站不可用。

日志轮转

journald 与 Nginx access log 都要限大小,否则悄悄吃掉几十 GB。