Ownd 是一个开源的个人数字资产管理平台,前后端分别基于 Flutter 与 NestJS 开发。为了降低用户的部署和维护成本,系统在设计之初就考虑了自托管(Self-hosted)的便利性。本文主要分享该系统的后端服务架构、多容器编排以及基于 GitHub Actions 的 CI/CD 实践细节。
1. 后端服务架构设计
Ownd 的系统架构以轻量化和数据自主性为原则,将后端服务及其核心依赖(数据库、缓存、对象存储、反向代理)均进行了容器化收拢。为了保证核心资产业务的独立性,博客等非核心系统在物理和配置上完全剥离,仅通过反向代理进行入口关联。
系统的整体拓扑结构如下:
graph TD
Client[Flutter 移动端 APP] -->|HTTPS 请求| Caddy[Caddy 2 反向代理]
Caddy -->|api.ownd.cc| NestJS[NestJS API 服务]
Caddy -->|minio.ownd.cc / console.ownd.cc| MinIO[MinIO 对象存储]
subgraph 主应用容器组 (Docker Compose)
Caddy
NestJS -->|Prisma ORM| Postgres[(PostgreSQL 16 数据库)]
NestJS -->|Redis 客户端| Redis[(Redis 7 缓存与速率限制)]
NestJS -->|S3 SDK| MinIO
end
subgraph 宿主机挂载卷 (Host Volume)
Postgres -->|挂载数据| postgres_data_prod
MinIO -->|挂载数据| minio_data_prod
Caddy -->|挂载证书| caddy_data_prod
end
1.1 业务核心:NestJS 与 Prisma ORM
后端采用 NestJS 框架,利用其模块化和依赖注入(DI)机制来组织业务代码。
- 数据操作:使用 Prisma ORM 进行数据库建模与查询。Prisma 提供了类型安全的 Schema 定义方式,开发中可以通过声明式的迁移机制(Migration)来变更数据库结构。
- 日志系统:使用
nest-winston进行分级日志输出和定期文件轮转,以便在服务发生异常时进行追溯。
1.2 数据持久化与缓存层:PostgreSQL 与 Redis
- 关系型数据库:使用 PostgreSQL 16 存储用户数据、物品信息、资产分类以及系统审计日志。由于 PostgreSQL 原生支持字符串数组(
String[]),项目中直接使用该特性来存储物品标签,避免了多对多关联表的额外查询开销。 - 缓存与速率限制:使用 Redis 7 主要负责两项工作:
- JWT 令牌黑名单:在用户注销登录时,将当前 Token 写入 Redis 缓存并设置过期时间,实现无状态 Token 的主动失效。
- 请求速率限制:防止接口被频繁调用或暴力破解。
1.3 对象存储:MinIO
为了避免绑定特定的云厂商(如阿里云 OSS 或腾讯云 COS),系统容器化部署了兼容 S3 协议的 MinIO 服务。
- 用户上传的物品图片和备份文件均存储在本地挂载的 MinIO 实例中。
- 服务端实现了
MinioService,在容器初始化时自动校验并创建指定的 Bucket,上传文件时生成随机文件名并返回 S3 兼容的相对路径。
1.4 外部系统路由隔离
为了保证服务的健壮性,系统在架构上将主程序与博客等其他系统完全解耦。博客系统部署在独立的 Docker 网络中,主应用的 Caddy 反向代理仅通过宿主机内部网关(host.docker.internal)将域名解析流量安全路由给独立的博客实例。
2. 生产环境的防灾与安全设计
2.1 弹性缓存降级
在 Redis 无法连接的极端情况下,如果直接抛出异常会导致整个 API 服务启动失败。为此,项目在 app.module.ts 中对 Redis 进行了异步初始化,并捕获了连接异常。当 Redis服务不可用时,系统会自动降级使用内存存储(In-Memory Store),保障核心业务的连续性:
CacheModule.registerAsync({
isGlobal: true,
useFactory: async (configService: ConfigService) => {
try {
const host = configService.get<string>('REDIS_HOST', 'localhost');
const port = Number(configService.get<string>('REDIS_PORT', '6379'));
const store = await redisStore({ socket: { host, port }, ttl: 60000 });
return { store };
} catch (error) {
console.warn('Redis 连接失败,自动降级为内存缓存:', error);
return {}; // 降级为默认的内存存储
}
},
inject: [ConfigService],
})
2.2 双策略接口限流
基于 Redis 存储,系统针对不同接口类型配置了分层限流:
- 默认限流策略 (default):普通业务接口限制每个客户端每分钟最多请求 100 次。
- 认证限流策略 (auth):针对登录、注册等敏感接口,限制每个 IP 每分钟最多请求 10 次,防止账户密码被暴力破解。
2.3 审计日志脱敏
系统引入了全局的 AuditInterceptor 拦截器。当控制器方法标记了 @AuditAction() 时,拦截器会在请求执行成功后自动将操作者 IP、HTTP 方法、请求路径等信息写入 AuditLog 表。在此过程中,拦截器调用了 sanitize 工具函数对请求体进行扫描,移除 password 等敏感字段,避免明文密码落库。
3. 基于 Docker Compose 的多服务编排
在生产环境下,主项目的所有核心服务均在 docker-compose.prod.yaml 中统一定义。
3.1 容器启动顺序控制
为了保证容器内服务的真正就绪,项目引入了健康检查机制:
- 在 PostgreSQL 容器中定义了
healthcheck,使用pg_isready命令定期检测数据库服务是否可用。 - 在 NestJS API 容器的
depends_on中,加入了condition: service_healthy条件。这样可以确保只有在数据库就绪后,API 容器才会启动,避免了启动初期的数据库连接报错。
3.2 Caddy 反向代理与自动 HTTPS
项目使用 Caddy 2 作为流量入口。相比传统的 Nginx,Caddy 的配置更加扁平化,且原生集成了 ACME 协议:
- 证书自动申请与续期:Caddy 会根据配置文件中的域名(如
api.ownd.cc),在后台自动向 Let's Encrypt 或 ZeroSSL 申请免费的 SSL 证书并管理其生命周期。 - 混合代理路由:Caddy 容器不仅在主项目容器网络中反向代理
api服务和minio服务,还通过配置extra_hosts机制解析host.docker.internal,从而将外部博客域名路由至宿主机暴露端口上运行的独立服务中。
4. 持续集成与部署(CI/CD)实践
系统采用 GitHub Actions 来完成服务端的自动化编译与部署。
4.1 部署流程图解
sequenceDiagram
participant Developer as 开发者
participant GitHub as GitHub 仓库
participant Runner as GitHub Runner
participant Server as 云服务器
Developer->>GitHub: 推送代码至 master (ownd-api/**)
GitHub->>Runner: 触发 GitHub Actions 流水线
Note over Runner: 编译与测试阶段<br/>- 安装 Node & pnpm<br/>- 生成 Prisma Client<br/>- 运行 TypeScript 编译校验
Runner->>Server: SSH 连接并执行部署命令
Note over Server: 服务器更新与重构<br/>- git pull 拉取最新代码<br/>- docker compose 增量构建启动<br/>- 清理虚悬镜像释放磁盘空间
4.2 流水线的设计考量与优化
在流水线配置 .github/workflows/deploy-backend.yml 中,包含以下设计要点:
- 路径过滤保护:配置了
paths过滤规则,只有在ownd-api/**或部署文件发生变动时才会触发流水线,避免移动端应用(App)的代码提交触发无意义的服务器部署。 - 云端语法校验:在 GitHub 托管的虚拟环境中首先运行安装依赖、生成 Prisma 客户端以及代码打包(
pnpm run build)。如果提交的代码中包含 TypeScript 语法错误或不兼容的模型改动,流水线会立即熔断并发出警告,防止有问题的代码发布到生产环境。 - 虚悬镜像清理:在执行
docker compose up -d --build重新构建镜像后,服务器上会留下旧的未命名镜像(虚悬镜像)。流水线在最后追加了docker image prune -f命令,自动清理这些垃圾文件,避免因为频繁部署耗尽轻量级云服务器的磁盘空间。 - 迁移自动化:NestJS API 的
Dockerfile中,将启动命令配置为pnpm prisma:migrate:deploy && pnpm start:prod。容器每次启动时会自动在数据库上运行待执行的 SQL 迁移文件,确保数据库结构与应用代码同步更新,无需人工登录服务器手动干预。
5. 结语
通过使用 Docker 进行多服务容器化编排、引入 Caddy 实现自动证书托管与宿主机混合代理,以及通过 GitHub Actions 实现云端校验与自动部署,Ownd 构建了一套轻量、安全且职责清晰的后端部署方案。这种架构模式降低了运维门槛,并具有良好的模块隔离性,非常适合中小型自托管项目的发布与维护。