到目前为止,这条系列路线已经走过了四步:
- 先把大模型私有化部署的路线拆清楚
- 再把 GPU、驱动、CUDA、容器运行时准备好
- 再把原生 K8S 集群搭起来
- 最后补齐了
MetalLB、Gateway API和NFS动态供给
如果这时候你还继续停留在“底座准备”,其实很容易产生一种错觉:
环境搭了不少,但还没有真正看到一个能交互、能访问、能体验的 AI 服务。
这就是为什么在进入 vLLM 或 SGLang 之前,我很建议先跑一遍 Ollama + Open WebUI。
原因很简单:
Ollama上手门槛低,适合先把模型服务跑起来Open WebUI可以立刻把结果变成一个可交互的 Web 页面- 这套组合很适合验证前面 1 到 4 篇的底座能力是否真的都打通了
换句话说,这一篇的目标不是追求最强推理性能,而是先把一条完整闭环跑通:
用户浏览器 -> Gateway API -> Open WebUI -> Ollama -> DeepSeek 模型 -> 返回结果
只要这条链路跑通,后面再往 vLLM、SGLang 这些更正式的推理服务走,心里会踏实很多。
本文仍然按可实操的方式来写:
- 先解释为什么做
- 再给命令
- 关键步骤补输出示例
- 明确告诉你什么样算成功
文中镜像版本、IP、Service 名称、Pod 名称和端口基于课程环境整理,与你的实际环境不完全一致是正常现象。
重点是链路和验收方法。
摘要
如果你只想先拿结论,可以先记住这个部署顺序:
- 在 K8S 中创建
ollama命名空间 - 用
StatefulSet + PVC部署 Ollama - 进入 Ollama 容器拉起
deepseek-r1:1.5b - 创建
ClusterIP类型的 Ollama Service - 部署 Open WebUI,并通过环境变量连接 Ollama
- 创建 Open WebUI 的
ClusterIPService - 用
Gateway + HTTPRoute把 Open WebUI 暴露到集群外 - 通过域名或 Host 访问 Web 页面,验证对话是否通路
这篇文章的三个核心验收点是:
ollama-0Pod 正常 Running,并且容器内能执行ollama run deepseek-r1:1.5bwebuiPod 正常 Running,并且能通过 Service 访问Gateway生效后,Envoy 的LoadBalancerService 能拿到外部 IP,并能打开 Open WebUI 页面
系列导航
这是“大模型私有化部署实践”系列的第五篇。当前系列顺序如下:
- 本地、Docker、K8S:大模型私有化部署路线怎么选
- 大模型推理环境准备实战:GPU、驱动、CUDA、容器运行时
- 基于 Ubuntu 24.04 搭建 AI 推理用原生 K8S 集群
- 为 K8S 补齐入口与存储:MetalLB、Gateway API、NFS 动态供给
- 用 Ollama + Open WebUI 快速搭建本地 AI 体验环境
- vLLM 私有化部署实战:本地部署、Docker 部署、接口验证
- vLLM 上 K8S:服务部署、对外暴露、监控与验证
- SGLang 私有化部署实战:本地部署、Docker 部署、能力体验
- SGLang 上 K8S:接入 Open WebUI、服务发布与 GPU 运维
- 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 分配外部 IPNFS 动态供给:负责给 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
这里最关键的是:
TYPE是ClusterIP11434端口已经暴露出来
因为后面的 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
到这里,集群内的服务链路已经基本齐了:
webuiService 暴露了 Open WebUIollamaService 暴露了 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 已经起来了 - 对应的
LoadBalancerService 已经分配到了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 验证,把“体验环境”推进到“推理服务”。
评论区