侧边栏壁纸
  • 累计撰写 238 篇文章
  • 累计创建 245 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

用 Ollama + Open WebUI 快速搭建本地 AI 体验环境

zhanjie.me
2025-09-24 / 0 评论 / 0 点赞 / 3 阅读 / 0 字

到目前为止,这条系列路线已经走过了四步:

  • 先把大模型私有化部署的路线拆清楚
  • 再把 GPU、驱动、CUDA、容器运行时准备好
  • 再把原生 K8S 集群搭起来
  • 最后补齐了 MetalLBGateway APINFS 动态供给

如果这时候你还继续停留在“底座准备”,其实很容易产生一种错觉:
环境搭了不少,但还没有真正看到一个能交互、能访问、能体验的 AI 服务。

这就是为什么在进入 vLLM 或 SGLang 之前,我很建议先跑一遍 Ollama + Open WebUI

原因很简单:

  • Ollama 上手门槛低,适合先把模型服务跑起来
  • Open WebUI 可以立刻把结果变成一个可交互的 Web 页面
  • 这套组合很适合验证前面 1 到 4 篇的底座能力是否真的都打通了

换句话说,这一篇的目标不是追求最强推理性能,而是先把一条完整闭环跑通:

用户浏览器 -> Gateway API -> Open WebUI -> Ollama -> DeepSeek 模型 -> 返回结果

只要这条链路跑通,后面再往 vLLM、SGLang 这些更正式的推理服务走,心里会踏实很多。

本文仍然按可实操的方式来写:

  • 先解释为什么做
  • 再给命令
  • 关键步骤补输出示例
  • 明确告诉你什么样算成功

文中镜像版本、IP、Service 名称、Pod 名称和端口基于课程环境整理,与你的实际环境不完全一致是正常现象。
重点是链路和验收方法。

摘要

如果你只想先拿结论,可以先记住这个部署顺序:

  1. 在 K8S 中创建 ollama 命名空间
  2. StatefulSet + PVC 部署 Ollama
  3. 进入 Ollama 容器拉起 deepseek-r1:1.5b
  4. 创建 ClusterIP 类型的 Ollama Service
  5. 部署 Open WebUI,并通过环境变量连接 Ollama
  6. 创建 Open WebUI 的 ClusterIP Service
  7. Gateway + HTTPRoute 把 Open WebUI 暴露到集群外
  8. 通过域名或 Host 访问 Web 页面,验证对话是否通路

这篇文章的三个核心验收点是:

  • ollama-0 Pod 正常 Running,并且容器内能执行 ollama run deepseek-r1:1.5b
  • webui Pod 正常 Running,并且能通过 Service 访问
  • Gateway 生效后,Envoy 的 LoadBalancer Service 能拿到外部 IP,并能打开 Open WebUI 页面

系列导航

这是“大模型私有化部署实践”系列的第五篇。当前系列顺序如下:

  1. 本地、Docker、K8S:大模型私有化部署路线怎么选
  2. 大模型推理环境准备实战:GPU、驱动、CUDA、容器运行时
  3. 基于 Ubuntu 24.04 搭建 AI 推理用原生 K8S 集群
  4. 为 K8S 补齐入口与存储:MetalLB、Gateway API、NFS 动态供给
  5. 用 Ollama + Open WebUI 快速搭建本地 AI 体验环境
  6. vLLM 私有化部署实战:本地部署、Docker 部署、接口验证
  7. vLLM 上 K8S:服务部署、对外暴露、监控与验证
  8. SGLang 私有化部署实战:本地部署、Docker 部署、能力体验
  9. SGLang 上 K8S:接入 Open WebUI、服务发布与 GPU 运维
  10. vLLM 和 SGLang 到底怎么选

如果说前四篇解决的是“底座怎么搭”,这一篇解决的就是:

如何把前面搭好的底座真正用起来,快速做出一套可交互、可访问、可演示的本地 AI 体验环境。

1. 为什么这里先选 Ollama,而不是直接上 vLLM 或 SGLang

很多人会下意识觉得,既然最终要做正式推理服务,那就应该直接上 vLLM 或 SGLang。
这个想法并不完全错,但在当前这个阶段,Ollama 其实有一个很明显的优势:

  • 部署路径更短
  • 模型管理更直接
  • API 形式更容易理解
  • 非常适合先做“从浏览器到模型”的完整链路验证

对一套刚搭好的 K8S 集群来说,这种“先把体验跑通”的价值很高。
它能帮你非常快地回答几个问题:

  • 存储是不是已经真的能用
  • Gateway 链路是不是已经打通
  • Service 到 Service 的调用是不是正常
  • 团队成员能不能先看到结果

所以这一篇不是“性能最优路线”,而是“最快形成可用体验路线”。

2. 先看整体架构:这套链路里每一层在做什么

在正式敲命令之前,先把链路看清楚:

用户浏览器
  -> Gateway API / Envoy
  -> Open WebUI
  -> Ollama Service
  -> DeepSeek 模型
  -> 返回推理结果

这里每个组件的职责其实非常清楚:

  • Ollama:负责模型加载和推理接口
  • DeepSeek:实际被执行的模型
  • Open WebUI:负责用户交互界面
  • Gateway API:负责集群外访问入口
  • MetalLB:负责给入口 Service 分配外部 IP
  • NFS 动态供给:负责给 Ollama 和 Open WebUI 提供持久存储

如果你把这个关系先看清楚,后面排障时会轻松很多。

3. 创建命名空间:先把工作负载隔离开

建议把这一套体验环境单独放在一个命名空间里,便于后面统一查看和清理。

kubectl create ns ollama
kubectl get ns

预期输出里应该能看到:

NAME        STATUS   AGE
ollama      Active   3s

如果命名空间已经存在,kubectl create ns ollama 会提示已存在,这也属于正常情况。

4. 部署 Ollama:先把模型服务跑起来

在 K8S 里,Ollama 更适合用 StatefulSet 来部署。
原因也很直观:

  • 它需要持久化模型目录
  • 模型文件通常不希望随着 Pod 重建而丢失
  • 后面拉取的模型会写到固定目录里

4.1 准备 Ollama StatefulSet

创建 01-ollama-statefulset.yaml

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: ollama
  namespace: ollama
spec:
  serviceName: "ollama"
  replicas: 1
  selector:
    matchLabels:
      app: ollama
  template:
    metadata:
      labels:
        app: ollama
    spec:
      containers:
        - name: ollama
          image: docker.io/ollama/ollama:0.6.5
          ports:
            - containerPort: 11434
          resources:
            requests:
              cpu: "1000m"
              memory: "2Gi"
            limits:
              cpu: "4000m"
              memory: "4Gi"
              nvidia.com/gpu: "1"
          volumeMounts:
            - name: ollama-volume
              mountPath: /root/.ollama
  volumeClaimTemplates:
    - metadata:
        name: ollama-volume
      spec:
        storageClassName: nfs-client
        accessModes: ["ReadWriteOnce"]
        resources:
          requests:
            storage: 200Gi

这里有两点要特别注意:

  • 模型目录挂载到了 /root/.ollama
  • GPU 资源在 K8S 里要通过 limits 声明,而不是单独只写 requests

4.2 应用资源并看状态

kubectl apply -f 01-ollama-statefulset.yaml
kubectl get sts -n ollama
kubectl get pods -n ollama
kubectl get pvc -n ollama

正常情况下,你应该能看到类似输出:

kubectl get sts -n ollama

NAME     READY   AGE
ollama   1/1     2m

kubectl get pods -n ollama

NAME       READY   STATUS    RESTARTS   AGE
ollama-0   1/1     Running   0          2m

kubectl get pvc -n ollama

NAME                    STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
ollama-volume-ollama-0  Bound    pvc-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx   200Gi      RWO            nfs-client     2m

只要这里:

  • Pod 还是 Pending
  • PVC 没有 Bound
  • Pod 一直 ContainerCreating

就先不要继续拉模型。
通常要先回头检查:

  • GPU 节点资源是否满足
  • nfs-client 这个 StorageClass 是否已经存在
  • 集群里的 nvidia.com/gpu 资源是否已经可见

4.3 如果你暂时没有 GPU,能不能先跑通链路

可以,但要明确这是“体验版路径”。

如果只是想先把 Open WebUI 和 Gateway 链路跑通,可以先移除:

nvidia.com/gpu: "1"

这样可以先验证服务和入口,不代表后面正式推理时不需要 GPU。

5. 在 Ollama 容器里拉起 DeepSeek 模型

Pod 跑起来之后,下一步不是马上配 UI,而是先确认 Ollama 自己能正常工作。

5.1 进入 Ollama 容器

kubectl exec -it ollama-0 -n ollama -- /bin/bash

进入容器后,你可以先看一下版本:

ollama --version

5.2 拉起 deepseek-r1:1.5b

在容器里执行:

ollama run deepseek-r1:1.5b

第一次执行时,常见现象是会先下载模型。
输出可能会持续显示拉取进度,类似:

pulling manifest
pulling xxxxxxxxxxxx:  35% ...
pulling yyyyyyyyyyyy:  78% ...
verifying sha256 digest
writing manifest
success

如果模型已经下载过,通常会更快直接进入交互。

5.3 做一次最小交互验证

ollama run deepseek-r1:1.5b 的会话里输入一句测试问题,比如:

你好,请用一句话介绍你自己

只要模型能正常返回内容,就说明:

  • Pod 没问题
  • 模型目录可写
  • Ollama 运行没问题

退出会话:

/bye
exit

6. 创建 Ollama Service:让集群内其他服务能访问它

Open WebUI 后面不会直接连 Pod,而是通过 Service 去访问 Ollama。

6.1 创建 Service

准备 02-ollama-svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: ollama
  namespace: ollama
  labels:
    app: ollama
spec:
  type: ClusterIP
  ports:
    - port: 11434
      protocol: TCP
      targetPort: 11434
  selector:
    app: ollama

应用并查看:

kubectl apply -f 02-ollama-svc.yaml
kubectl get svc -n ollama

预期输出类似:

NAME     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)          AGE
ollama   ClusterIP   10.101.191.39    <none>        11434/TCP        10s

这里最关键的是:

  • TYPEClusterIP
  • 11434 端口已经暴露出来

因为后面的 Open WebUI 就是通过这个 Service 去请求模型服务。

6.2 用临时 Pod 测一下 Ollama API

这一步非常推荐做,因为它能在接入 UI 之前先确认 Service 到 Service 的链路没问题。

kubectl run curl-test -n ollama --rm -it --image=curlimages/curl -- sh

进入容器后测试:

curl http://ollama:11434/api/tags

如果模型已经拉取成功,返回通常类似:

{
  "models": [
    {
      "name": "deepseek-r1:1.5b",
      "modified_at": "2026-04-07T10:00:00Z"
    }
  ]
}

只要这里能返回模型列表,说明 Open WebUI 后面大概率也能连通。

7. 部署 Open WebUI:把模型能力变成可交互界面

如果说 Ollama 解决的是“模型服务跑起来”,那 Open WebUI 解决的就是“让人真的用起来”。

7.1 为什么这里要单独给 Open WebUI 一个 PVC

因为它本身会保存:

  • 用户配置
  • 对话记录
  • Web UI 自身的数据目录

所以即使它是 Deployment,也仍然建议挂一个持久卷。

7.2 创建 Open WebUI 资源

准备 03-open-webui.yaml

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: webui-pvc
  namespace: ollama
  labels:
    app: webui
spec:
  accessModes:
    - ReadWriteOnce
  storageClassName: nfs-client
  resources:
    requests:
      storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: open-webui
  namespace: ollama
spec:
  replicas: 1
  selector:
    matchLabels:
      app: webui
  template:
    metadata:
      labels:
        app: webui
    spec:
      containers:
        - name: open-webui
          image: ghcr.io/open-webui/open-webui:main
          ports:
            - containerPort: 8080
          env:
            - name: OLLAMA_BASE_URL
              value: http://ollama:11434
          volumeMounts:
            - name: webui-data
              mountPath: /app/backend/data
      volumes:
        - name: webui-data
          persistentVolumeClaim:
            claimName: webui-pvc

这里最关键的环境变量是:

OLLAMA_BASE_URL=http://ollama:11434

它决定了 Open WebUI 去哪里调用模型服务。

7.3 应用并验证

kubectl apply -f 03-open-webui.yaml
kubectl get pvc -n ollama
kubectl get deploy -n ollama
kubectl get pods -n ollama

正常情况下,你应该看到:

kubectl get pvc -n ollama

NAME        STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   AGE
webui-pvc   Bound    pvc-yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy   20Gi       RWO            nfs-client     30s

kubectl get deploy -n ollama

NAME         READY   UP-TO-DATE   AVAILABLE   AGE
open-webui   1/1     1            1           1m

kubectl get pods -n ollama

NAME                          READY   STATUS    RESTARTS   AGE
ollama-0                      1/1     Running   0          20m
open-webui-xxxxxxxxxx-xxxxx   1/1     Running   0          1m

如果这里 Open WebUI 起不来,优先看:

kubectl logs deploy/open-webui -n ollama
kubectl describe pod -n ollama <open-webui-pod-name>

通常要优先检查:

  • OLLAMA_BASE_URL 是否写对
  • webui-pvc 是否成功绑定
  • 镜像是否能正常拉取

8. 创建 Open WebUI 的 Service

Open WebUI 本身也需要一个集群内 Service,后面 Gateway 才能把流量转给它。

8.1 创建 Service

准备 04-open-webui-svc.yaml

apiVersion: v1
kind: Service
metadata:
  name: webui
  namespace: ollama
  labels:
    app: webui
spec:
  type: ClusterIP
  ports:
    - port: 8080
      protocol: TCP
      targetPort: 8080
  selector:
    app: webui

应用并查看:

kubectl apply -f 04-open-webui-svc.yaml
kubectl get svc -n ollama

课程里的典型输出类似:

NAME     TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)      AGE
ollama   ClusterIP   10.101.191.39    <none>        11434/TCP    124m
webui    ClusterIP   10.98.118.233    <none>        8080/TCP     40m

到这里,集群内的服务链路已经基本齐了:

  • webui Service 暴露了 Open WebUI
  • ollama Service 暴露了 Ollama
  • Open WebUI 通过 OLLAMA_BASE_URL 去调 Ollama

9. 用 Gateway API 把 Open WebUI 暴露到集群外

前面第 4 篇已经把 MetalLB 和 Gateway API 都装好了。
这一篇就把它们真正用起来。

9.1 创建 Gateway 和 HTTPRoute

准备 05-openwebui-gateway.yaml

apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: openwebui-gateway
  namespace: ollama
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
      hostname: "deepseek.kubemsb.com"
      allowedRoutes:
        namespaces:
          from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: openwebui-route
  namespace: ollama
spec:
  parentRefs:
    - name: openwebui-gateway
      namespace: ollama
  hostnames:
    - "deepseek.kubemsb.com"
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /
      backendRefs:
        - group: ""
          kind: Service
          name: webui
          port: 8080

应用并查看:

kubectl apply -f 05-openwebui-gateway.yaml
kubectl get gateway -n ollama
kubectl get httproute -n ollama
kubectl get pods,svc -n envoy-gateway-system

9.2 预期输出应该长什么样

课件里的一个典型成功状态如下:

NAME                                                       READY   STATUS    RESTARTS   AGE
pod/envoy-default-eg-e41e7b31-66cbf755f4-bpdfz             2/2     Running   0          58m
pod/envoy-gateway-8f5f58b8c-4pkln                          1/1     Running   0          60m
pod/envoy-ollama-openwebui-gateway-4944d481-57b954cc46-gx9fg 2/2   Running   0          101s

NAME                                        TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)                          AGE
service/envoy-default-eg-e41e7b31           LoadBalancer   10.111.180.42    192.168.10.240   80:31042/TCP                     58m
service/envoy-gateway                       ClusterIP      10.98.186.120    <none>           18000/TCP,18001/TCP,...         60m
service/envoy-ollama-openwebui-gateway-...  LoadBalancer   10.105.169.88    192.168.10.241   80:31614/TCP                     101s

这里最重要的信号有两个:

  • openwebui-gateway 相关的 Envoy Pod 已经起来了
  • 对应的 LoadBalancer Service 已经分配到了 192.168.10.241 这样的外部 IP

这一步说明:

  • Gateway 生效了
  • MetalLB 确实在给入口层分配地址

10. 通过域名访问 Open WebUI

到了这一步,服务其实已经基本通了。
剩下的事情,就是把域名或本地解析指到 MetalLB 分配的入口 IP。

10.1 做一条本地解析

如果你是 Windows 主机,可以在 hosts 文件里加一行:

192.168.10.241 deepseek.kubemsb.com

如果你是 Linux 或 macOS,本地 /etc/hosts 里也可以这么写:

192.168.10.241 deepseek.kubemsb.com

10.2 用 curl 先做一次 Host 头验证

在浏览器打开之前,建议先用 curl 看一下入口是否通了:

curl -H "Host: deepseek.kubemsb.com" http://192.168.10.241

如果返回了 HTML 内容,通常就说明:

  • Gateway -> Service -> Open WebUI 这条链路是通的

10.3 浏览器访问

直接打开:

http://deepseek.kubemsb.com

如果一切正常,你应该能看到 Open WebUI 的初始化页面或登录页面。
进入界面后,模型列表里应能看到前面在 Ollama 中拉起的 deepseek-r1:1.5b

11. 一次完整链路验收:什么样才算这篇真的做成了

很多人做到一半就觉得“差不多了”,但为了后面继续上 vLLM 和 SGLang,我建议你把这几项都确认掉。

11.1 Ollama 侧

确认:

kubectl get pods -n ollama
kubectl exec -it ollama-0 -n ollama -- ollama list

如果模型已经拉取成功,ollama list 里通常会包含:

NAME                 ID              SIZE      MODIFIED
deepseek-r1:1.5b     xxxxxxxxxxxx    x.x GB    ...

11.2 Open WebUI 侧

确认:

kubectl get deploy,svc -n ollama
kubectl logs deploy/open-webui -n ollama | tail -n 20

只要没有持续报 Ollama 连接失败、PVC 挂载失败,通常这一步就没问题。

11.3 Gateway 侧

确认:

kubectl get gateway,httproute -n ollama
kubectl get svc -n envoy-gateway-system | grep LoadBalancer

如果:

  • Gateway 已创建
  • HTTPRoute 已创建
  • Envoy LoadBalancer Service 已分到外部 IP

那入口层通常已经成立。

12. 这套方案的优势和边界

把这套链路完整跑下来之后,你会很直观地感受到它的优势:

  • 上手快
  • UI 友好
  • 非常适合作为团队内演示环境
  • 能快速验证前面 K8S、Gateway、存储的底座是否可靠

但也要看到它的边界:

  • 它更偏“先体验起来”,不是“吞吐最优方案”
  • 对正式大规模推理服务来说,Ollama 往往不是最终形态
  • 如果后面追求更高吞吐和更正式的 API 服务,通常还是要走 vLLM 或 SGLang

所以更准确的定位是:

Ollama + Open WebUI 非常适合作为 K8S 上的第一条 AI 体验闭环,但它通常是起点,而不是终点。

13. 结语:先把体验跑通,平台才更容易往前走

前面几篇文章一直都在搭底座。
而这一篇最大的意义,就是把那些看起来有点抽象的底座能力,真正变成一个“能被人点开、能聊起来、能演示”的 AI 服务。

当你把这套链路跑通之后,很多事情都会立刻变得具体起来:

  • Gateway 到底是不是通的
  • 存储到底是不是能用
  • 集群里的服务调用链路是不是正常
  • 团队到底能不能先看到结果

这也是为什么我很建议在走向 vLLM、SGLang 之前,先把这一步做完。
不是因为它最强,而是因为它最适合帮你快速建立一条完整、可验证、可交付的体验闭环。

下一篇文章,我们就从这条体验闭环继续往前走:
正式进入 vLLM 路线,分别完成本地部署、Docker 部署和 API 验证,把“体验环境”推进到“推理服务”。

0

评论区