背景
在单机环境中使用 Docker Compose 同时运行 Ollama(推理核心)与 Open-WebUI(交互界面)是一个常见做法。初期将两者放在同一个 compose 文件中管理较为方便,但随着系统逐步演进,这种方式会带来以下问题:
- 服务职责耦合,后期维护和扩展不清晰
- 难以引入新的 AI 服务(如工作流、代理、API 网关)
- 对数据与网络边界的控制不够明确
因此,本次实践的目标是:
在不丢失任何既有数据的前提下,将 Ollama 与 Open-WebUI 拆分为两个独立的 Docker Compose 栈,并通过一个核心网络进行连接。
设计原则
- 核心明确:Ollama 作为推理核心,其他服务围绕其运行
- 网络唯一:仅存在一个核心网络,用于 AI 内部通信
- 数据不可丢失:所有历史模型与 WebUI 数据必须完整保留
- 职责分离:每个栈只负责自己的服务,不跨栈依赖启动顺序
- Compose 原生:尽量使用 Compose 行为,不依赖手工创建资源
原始状态分析
在最初的一体化部署中,Docker 会自动为服务创建数据卷。拆分为多个栈后,如果未显式指定旧数据卷名称,Docker 会为新栈创建新的空卷,从而导致“历史数据消失”的错觉。
需要明确的是:
数据通常并未被删除,而是新容器未挂载到正确的旧数据卷。
因此,拆分过程中最关键的工作并不是服务启动顺序,而是数据卷的精确绑定。
拆分思路概述
整体架构被拆分为两类栈:
- 核心栈:仅包含推理服务,负责创建并维护核心内部网络
- 外围栈:包含界面或其他 AI 服务,通过外部方式接入核心网络
所有服务间通信均通过 Docker 内部网络完成,核心服务不直接对外暴露。
核心栈配置要点(示意)
核心栈的职责包括:
- 运行推理服务
- 挂载既有模型数据卷
- 创建并命名核心内部网络
配置示意(省略非关键字段):
services:
core-service:
image: <image>
volumes:
- core-data:/data
networks:
- core-net
volumes:
core-data:
external: true
networks:
core-net:
name: core-net
外围栈配置要点(示意)
外围栈的职责包括:
- 提供用户界面或辅助能力
- 精确挂载原有数据卷
- 通过外部方式使用核心网络
配置示意(省略非关键字段):
services:
ui-service:
image: <image>
volumes:
- ui-data:/data
networks:
- core-net
volumes:
ui-data:
external: true
networks:
core-net:
external: true
常见问题与判断方法
为什么拆分后看起来“数据没了”?
- Compose 项目名称变化
- 数据卷名称未显式指定
- Docker 默认行为是“新建而非猜测”
如何判断旧数据卷是否仍然存在?
可通过 Docker 的卷列表与磁盘占用信息进行判断。通常:
- 旧数据卷体积明显更大
- 新创建的数据卷体积接近为空
最终状态
在完成拆分与修正后,系统应达到以下状态:
- 核心服务与界面服务完全解耦
- 核心网络只创建一次并长期复用
- 所有历史数据完整保留
- 架构可自然扩展新的 AI 服务栈
总结
本次实践的核心经验可以归纳为一句话:
Docker 不会替用户判断应当使用哪个历史数据卷,所有关键数据都必须显式绑定。
通过明确:
- 核心网络的唯一性
- 核心数据卷的归属
- 外围服务对核心资源的外部依赖关系
可以在不增加额外心智负担的前提下,构建一个长期稳定、可维护、可扩展的个人 AI 基础设施。