|

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 容器启动顺序控制

为了保证容器内服务的真正就绪,项目引入了健康检查机制:

  1. 在 PostgreSQL 容器中定义了 healthcheck,使用 pg_isready 命令定期检测数据库服务是否可用。
  2. 在 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 构建了一套轻量、安全且职责清晰的后端部署方案。这种架构模式降低了运维门槛,并具有良好的模块隔离性,非常适合中小型自托管项目的发布与维护。

原创

Ownd 架构设计与 CI/CD 部署实践

本文链接: Ownd 架构设计与 CI/CD 部署实践

本文采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

评论交流

文章目录