Ubuntu 26.04 LTS
✓长期支持版本,软件源新(MySQL 8.4 / Redis 7 可直接 apt 安装),社区资料最多,出问题最容易搜到答案。
✕不上 Debian testing / Arch:服务器要的是可预测,不是最新。
Tech Stack & Deployment
本页只写这个项目真正会用到的组件,不堆砌名词。整套系统跑在一台 Ubuntu 26.04 LTS 上:Nginx 在门口,Nuxt 负责 SSR,Rust 提供业务 API,MySQL 存主数据,Redis 管会话与热度,Meilisearch 做检索。
每个选择都写清「为什么是它」和「为什么不是别的」。选型的标准是:能少一个进程就少一个进程,能少一个需要运维的东西就少一个。
✓长期支持版本,软件源新(MySQL 8.4 / Redis 7 可直接 apt 安装),社区资料最多,出问题最容易搜到答案。
✕不上 Debian testing / Arch:服务器要的是可预测,不是最新。
✓TLS 终止、HTTP/2、静态资源、gzip/brotli、反向代理与限流一站式解决;本身极稳定,几乎不需要调优。
✕不上 Caddy:自动 HTTPS 很方便,但可预期的配置能力与排错资料不如 Nginx。
✓SSR 直出保证社区内容可被搜索引擎抓取;SFC 与动态组件加载天然适合「每个菜单一个独立页面」的架构。
✕不做纯 SPA:社区的核心价值是内容可被检索与分享,纯 SPA 在 SEO 上先天吃亏。
✓内存安全 + 低资源占用,单机小配置也能扛住社区读写;axum 基于 tower,中间件生态与类型安全都很好。
✕不上 Node 后端:SSR 已经占了 Node 进程,业务再挤进去会让内存与 CPU 争抢变得难以定位。
✓窗口函数、CTE、JSON 类型与函数索引足够支撑评论树与菜单树;运维生态最成熟,备份恢复方案最省心。
✕不上 PostgreSQL:能力上完全够用,但本项目的运维熟练度在 MySQL,降低踩坑概率优先。
✓一份 Redis 解决五件事:会话、热点缓存、ZSet 热度排行、限流计数、outbox 的轻量调度。
✕不引入 Kafka / RabbitMQ:在这个规模下只是多一个要运维的进程。
✓开箱即用的中文分词与容错搜索,毫秒级响应,单二进制部署;对社区「搜帖子 / 搜词条 / 搜人」的场景足够。
✕不上 Elasticsearch:JVM 内存开销在单机上会与 MySQL 直接冲突,收益不成比例。
✓V1 只做图片与短链视频,落盘 + Nginx 托管是最简单可靠的方案,不额外引入对象存储依赖。
✕不立刻上云对象存储:等流量真的上来再迁,接口抽象提前留好即可。
初始阶段所有服务跑在一台机器上,但边界已经划清:只有 Nginx 对外,其余服务全部只监听 127.0.0.1。这样后续把 MySQL 或 Meilisearch 搬到独立主机时,配置改动很小。
| 服务 | 端口 | 职责 | 托管方式 |
|---|---|---|---|
| nginx | 80 / 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 |
| mysql | 3306(仅本机) | 主数据存储,InnoDB + utf8mb4 | systemd / apt |
| redis | 6379(仅本机) | 会话、缓存、ZSet、限流、轻量队列 | systemd / apt |
| meilisearch | 7700(仅本机) | 全文检索,仅由 api / worker 访问 | systemd(官方二进制) |
Redis 的键名与 Meilisearch 的索引字段必须提前约定好。这两处一旦乱掉,后期排查成本极高 —— 因为你无法从数据本身看出它属于谁。
| Key | 类型 | 过期 |
|---|---|---|
| sess:{session_id} | Hash | TTL 7d |
| menu:tree:{community_id} | String(JSON) | 无过期(写时删) |
| hot:{community}:{range} | ZSet | TTL 10m |
| vote:count:{type}:{id} | Hash | TTL 1h |
| rate:{scope}:{key} | String | TTL 60s |
| unread:{user_id} | String | 无过期 |
| outbox:lock | String | TTL 30s |
统一前缀 {域}:{对象}:{标识},冒号分隔;禁止无过期时间的缓存键(菜单树除外,它由写操作主动删除)。
posts
comments
users
pages
menu_items
核心是「让 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 前端
ruyi-api/
Rust 工作区
deploy/
部署与运维
从裸机到可访问的完整顺序。每一步都应该可重复执行,避免「只有那台机器能跑」。
单机的优势是简单,代价是「所有资源都在同一个池子里」。下面这些是必须先立规矩的地方。
磁盘水位
单机最容易先撑爆磁盘。日志、媒体、binlog 三项都要设保留策略并告警。
内存分配
MySQL 与 Meilisearch 最容易互相抢内存。给两者都设上限,预留 2GB 给系统与 Node。
备份恢复
备份没验证过等于没有备份。每月至少做一次恢复演练。
连接池
Rust 侧连接池大小要与 MySQL max_connections 匹配,否则高并发下会自我排队。
证书续期
certbot 定时任务要监控,续期失败的后果是整站不可用。
日志轮转
journald 与 Nginx access log 都要限大小,否则悄悄吃掉几十 GB。