本文档提供了在 Google Distributed Cloud (GDC) 网闸隔离配置环境中部署 Open WebUI(一种自托管、基于浏览器的聊天界面)的分步说明。Open WebUI 通过 Ollama 和 OpenAI 兼容 API 连接到使用 Ollama 和 vLLM 用户指南部署的推理后端,因此开发者和最终用户无需编写 API 调用即可与开放权重模型进行交互。
架构
Open WebUI 在标准集群的专属命名空间中作为单副本 Deployment 运行。其状态(用户账号、聊天、设置)存储在 ReadWriteOnce PersistentVolumeClaim 上。ConfigMap 包含推理后端的连接设置,这些后端通过其集群内 Service 名称访问,而 Secret 包含会话签名密钥。LoadBalancer 类型的 Kubernetes Service 会公开网页界面,而 NetworkPolicy/ProjectNetworkPolicy 对象会保护对该界面的访问。
准备工作
确保已按照将 Gemma 模型与 Ollama 搭配使用的用户指南、将 Gemma 模型与 vLLM 搭配使用的用户指南或将 Mistral 模型与 vLLM 搭配使用的用户指南部署了至少一个推理后端,并且您有权访问工作站上相应参考实现的 ${HOME}/gdcag-solutions 目录。
Identity and Access Management
确保已正确配置必要的 IAM 账号、角色和权限。
组织的 GDC 用户角色:
- 可选:Standard Cluster Admin(通过控制台查看集群)
项目的 GDC User 角色:
- Standard Cluster Admin (
standard-cluster-admin)
标准集群上的 GDC 用户角色:
StandardClusterRoleBinding到StandardClusterRolecluster-admin(由 Project IAM Admin 创建,请参阅参考实现的 Identity and Access Management 部分)
工作站
除非某个步骤另有说明,否则请在具有所需工具且可连接到环境和互联网的工作站上运行本指南中的所有命令。
创建用户指南目录结构:
mkdir -p ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.d mkdir -p ${HOME}/gdcag-solutions/open-weight-models/open-webui/secrets chmod u=rwx,go= ${HOME}/gdcag-solutions/open-weight-models/open-webui/secrets创建会话签名密钥:
openssl rand -hex 32 > ${HOME}/gdcag-solutions/open-weight-models/open-webui/secrets/open_webui_secret_key chmod u=rw,go= ${HOME}/gdcag-solutions/open-weight-models/open-webui/secrets/open_webui_secret_key创建用户指南环境配置文件:
cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.d/open-webui.sh && echo "Successfully created." || echo "Failed to create!" # Open WebUI export GDCS_OPEN_WEBUI_IMAGE_TAG="v0.11.3" export GDCS_OPEN_WEBUI_NAME="open-webui" export GDCS_OPEN_WEBUI_PVC_SIZE="20Gi" export GDCS_OPEN_WEBUI_SECRET_KEY="$(cat ${GDCS_USER_GUIDE_HOME}/secrets/open_webui_secret_key)" # Inference backends (in-cluster Service addresses, several backends separated by ';'). # The user guides name the Service after the model: Ollama uses GDCS_OLLAMA_MODEL_KUBERNETES # (gemma4:e4b -> gemma4-e4b) and vLLM uses GDCS_HF_MODEL_KUBERNETES (such as vllm-gemma or vllm-mistral) # (mistralai/Mistral-7B-Instruct-v0.2 -> mistralai-mistral-7b-instruct-v0-2); kubectl get service # in the workload namespace (ollama-gemma, vllm-gemma or vllm-mistral) lists them. export GDCS_OPEN_WEBUI_ENABLE_OLLAMA_API="OPEN_WEBUI_ENABLE_OLLAMA_API" export GDCS_OPEN_WEBUI_OLLAMA_BASE_URLS="OPEN_WEBUI_OLLAMA_BASE_URLS" export GDCS_OPEN_WEBUI_ENABLE_OPENAI_API="OPEN_WEBUI_ENABLE_OPENAI_API" export GDCS_OPEN_WEBUI_OPENAI_API_BASE_URLS="OPEN_WEBUI_OPENAI_API_BASE_URLS" export GDCS_OPEN_WEBUI_OPENAI_API_KEYS="none" # vLLM does not check API keys unless started with --api-key # Workload export GDCS_WORKLOAD_NAMESPACE="open-webui" # CPU resources export GDCS_WORKLOAD_CPU_LIMIT="2" export GDCS_WORKLOAD_CPU_REQUEST="1" # Memory resources export GDCS_WORKLOAD_MEMORY_LIMIT="4Gi" export GDCS_WORKLOAD_MEMORY_REQUEST="2Gi" EOF替换以下内容:
OPEN_WEBUI_ENABLE_OLLAMA_API:如果您部署了 Ollama 后端,则为true,否则为false。OPEN_WEBUI_OLLAMA_BASE_URLS:Ollama 服务的集群内网址,以;分隔,例如http://gemma4-e4b.ollama-gemma.svc.cluster.local。如果您未部署 Ollama 后端,请将其留空。OPEN_WEBUI_ENABLE_OPENAI_API:如果您部署了 vLLM 后端,则为true,否则为false。OPEN_WEBUI_OPENAI_API_BASE_URLS:具有/v1路径的 vLLM 服务的集群内网址,以;分隔,例如http://mistralai-mistral-7b-instruct-v0-2.vllm-mistral.svc.cluster.local/v1或http://google-gemma-4-e2b-it.vllm-gemma.svc.cluster.local/v1。如果您未部署 vLLM 后端,请将其留空。
创建用户指南环境加载器文件:
cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.sh && echo "Successfully created." || echo "Failed to create!" source "${HOME}/gdcag-solutions/open-weight-models/IMPLEMENTATION/env.sh" export GDCS_USER_GUIDE_HOME="${HOME}/gdcag-solutions/open-weight-models/open-webui" echo "GDCS_USER_GUIDE_HOME=${GDCS_USER_GUIDE_HOME}" # Sourced in dependency order source "${GDCS_USER_GUIDE_HOME}/env.d/open-webui.sh" EOF替换以下内容:
IMPLEMENTATION:ollama或vllm,即您在此工作站上完成的参考实现。
使用您首选的编辑器修改和查看环境文件:
${EDITOR:-vi} ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.d/open-webui.sh获取环境文件:
source ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.sh输出类似于以下内容:
GDCS_ROOT_HOME=HOME_DIRECTORY_PATH/gdcag-solutions GDCS_IMPLEMENTATION_HOME=HOME_DIRECTORY_PATH/gdcag-solutions/open-weight-models/IMPLEMENTATION GDCS_USER_GUIDE_HOME=HOME_DIRECTORY_PATH/gdcag-solutions/open-weight-models/open-webui
集群
配置
gdcloud:gdcloud config set core/account "default-user" gdcloud config set core/organization_console_url "${GDC_ZONAL_CONSOLE_URL}" gdcloud config set core/project "${GDC_PROJECT}" gdcloud config set core/zone "${GDC_ZONE}"向 GDC 环境进行身份验证:
gdcloud auth login检索集群凭证:
gdcloud clusters get-credentials "${GDC_STANDARD_CLUSTER_NAME}" \ --project="${GDC_PROJECT}" \ --standard \ --zone="${GDC_ZONE}"验证环境文件中引用的推理后端
Service是否存在:for url in $(echo "${GDCS_OPEN_WEBUI_OLLAMA_BASE_URLS};${GDCS_OPEN_WEBUI_OPENAI_API_BASE_URLS}" | tr ';' ' '); do host=$(echo "${url}" | sed -E 's#https?://([^/:]+).*#\1#') kubectl get service "${host%%.*}" --namespace="$(echo "${host}" | cut -d. -f2)" --output=wide done
实现
工件迁移
定义解决方案所需的容器映像列表:
declare -a GDCS_REGISTRY_IMAGES=( "ghcr.io/open-webui/open-webui:${GDCS_OPEN_WEBUI_IMAGE_TAG}" ) export SERIALIZED_IMAGES=$(declare -p GDCS_REGISTRY_IMAGES)使用参考实现的脚本将所需的容器映像植入到制品注册表中:
${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh
命名空间
创建此命名空间:
kubectl create namespace "${GDCS_WORKLOAD_NAMESPACE}"添加
imagePullSecret:kubectl create secret docker-registry "${GDCS_HARBOR_K8S_PULL_SECRET}" \ --dry-run=client \ --from-file=.dockerconfigjson=${GDCS_HARBOR_K8S_DOCKER_CONFIG}/config.json \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ --output=yaml | kubectl apply -f -使用会话签名密钥创建
Secret:kubectl create secret generic "${GDCS_OPEN_WEBUI_NAME}" \ --dry-run=client \ --from-literal=WEBUI_SECRET_KEY="${GDCS_OPEN_WEBUI_SECRET_KEY}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ --output=yaml | kubectl apply -f -
存储
为 Open WebUI 数据创建
PersistentVolumeClaim的清单:cat <<EOF > "${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-rwo-pvc.yaml" && echo "Successfully created." || echo "Failed to create!" apiVersion: v1 kind: PersistentVolumeClaim metadata: name: ${GDCS_OPEN_WEBUI_NAME}-rwo spec: accessModes: - ReadWriteOnce resources: requests: storage: ${GDCS_OPEN_WEBUI_PVC_SIZE} storageClassName: standard-rwo volumeMode: Filesystem EOF应用
PersistentVolumeClaim的清单:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-rwo-pvc.yaml" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
网页界面
为 Open WebUI
ConfigMap、Deployment和Service创建清单。Open WebUI 在离线模式 (OFFLINE_MODE) 下运行,将此ConfigMap作为可信来源 (ENABLE_PERSISTENT_CONFIG=false),并停用需要互联网访问权限的功能:cat <<EOF > "${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-deployment.yaml" && echo "Successfully created." || echo "Failed to create!" apiVersion: v1 kind: ConfigMap metadata: name: ${GDCS_OPEN_WEBUI_NAME} data: DATA_DIR: "/app/backend/data" ENABLE_COMMUNITY_SHARING: "false" ENABLE_IMAGE_GENERATION: "false" ENABLE_OLLAMA_API: "${GDCS_OPEN_WEBUI_ENABLE_OLLAMA_API}" ENABLE_OPENAI_API: "${GDCS_OPEN_WEBUI_ENABLE_OPENAI_API}" ENABLE_PERSISTENT_CONFIG: "false" ENABLE_SIGNUP: "true" ENABLE_VERSION_UPDATE_CHECK: "false" ENABLE_WEB_SEARCH: "false" OFFLINE_MODE: "true" OLLAMA_BASE_URLS: "${GDCS_OPEN_WEBUI_OLLAMA_BASE_URLS}" OPENAI_API_BASE_URLS: "${GDCS_OPEN_WEBUI_OPENAI_API_BASE_URLS}" OPENAI_API_KEYS: "${GDCS_OPEN_WEBUI_OPENAI_API_KEYS}" PORT: "8080" --- apiVersion: apps/v1 kind: Deployment metadata: labels: app: ${GDCS_OPEN_WEBUI_NAME} name: ${GDCS_OPEN_WEBUI_NAME} spec: replicas: 1 selector: matchLabels: app: ${GDCS_OPEN_WEBUI_NAME} strategy: type: Recreate template: metadata: labels: app: ${GDCS_OPEN_WEBUI_NAME} spec: containers: - envFrom: - configMapRef: name: ${GDCS_OPEN_WEBUI_NAME} - secretRef: name: ${GDCS_OPEN_WEBUI_NAME} image: ${GDCS_HARBOR_PROJECT_URI}/open-webui/open-webui:${GDCS_OPEN_WEBUI_IMAGE_TAG} imagePullPolicy: IfNotPresent livenessProbe: failureThreshold: 3 httpGet: path: /health port: 8080 periodSeconds: 30 timeoutSeconds: 10 name: open-webui ports: - containerPort: 8080 name: http readinessProbe: failureThreshold: 3 httpGet: path: /health port: 8080 periodSeconds: 10 successThreshold: 1 timeoutSeconds: 5 resources: limits: cpu: "${GDCS_WORKLOAD_CPU_LIMIT}" memory: "${GDCS_WORKLOAD_MEMORY_LIMIT}" requests: cpu: "${GDCS_WORKLOAD_CPU_REQUEST}" memory: "${GDCS_WORKLOAD_MEMORY_REQUEST}" securityContext: allowPrivilegeEscalation: false startupProbe: failureThreshold: 60 httpGet: path: /health port: 8080 periodSeconds: 10 timeoutSeconds: 5 volumeMounts: - mountPath: /app/backend/data name: data imagePullSecrets: - name: ${GDCS_HARBOR_K8S_PULL_SECRET} volumes: - name: data persistentVolumeClaim: claimName: ${GDCS_OPEN_WEBUI_NAME}-rwo --- apiVersion: v1 kind: Service metadata: name: ${GDCS_OPEN_WEBUI_NAME} spec: ports: - name: http port: 80 protocol: TCP targetPort: 8080 selector: app: ${GDCS_OPEN_WEBUI_NAME} type: LoadBalancer EOF应用 Open WebUI
ConfigMap、Deployment和Service的清单:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-deployment.yaml" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"为 Open WebUI
NetworkPolicy创建清单:cat <<EOF > "${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-netpol.yaml" && echo "Successfully created." || echo "Failed to create!" apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-${GDCS_OPEN_WEBUI_NAME}-ingress namespace: ${GDCS_WORKLOAD_NAMESPACE} spec: ingress: - from: - ipBlock: cidr: 0.0.0.0/0 - namespaceSelector: {} ports: - port: 8080 protocol: TCP podSelector: {} policyTypes: - Ingress EOF应用 Open WebUI
NetworkPolicy的清单:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-netpol.yaml"
等待
Pod变为“正在运行”状态:watch --color --interval 5 --no-title \ "kubectl get pods --selector=app=${GDCS_OPEN_WEBUI_NAME} \ --namespace=${GDCS_WORKLOAD_NAMESPACE} | GREP_COLORS='mt=01;92' grep -E --color=always -e '^' -e 'Running'"等待
Deployment准备就绪:watch --color --interval 5 --no-title \ "kubectl get deployment/${GDCS_OPEN_WEBUI_NAME} \ --namespace=${GDCS_WORKLOAD_NAMESPACE} | GREP_COLORS='mt=01;92' grep -E --color=always -e '^' -e '1/1 1 1'"可选:流式传输容器日志以监控启动进度:
kubectl logs deployment/${GDCS_OPEN_WEBUI_NAME} \ --follow \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
验证
检索 Open WebUI 的 LoadBalancer IP 地址:
export OPEN_WEBUI_LB_IP=$(kubectl get service/${GDCS_OPEN_WEBUI_NAME} --namespace="${GDCS_WORKLOAD_NAMESPACE}" --output=jsonpath='{.status.loadBalancer.ingress[*].ip}') echo "OPEN_WEBUI_LB_IP=${OPEN_WEBUI_LB_IP}"使用负载均衡器检查健康状况和版本端点:
curl --show-error --silent http://${OPEN_WEBUI_LB_IP}/health && echo curl --show-error --silent http://${OPEN_WEBUI_LB_IP}/api/version && echo输出类似于以下内容:
{"status":true} {"version":"0.11.3"}使用端口转发检查健康状况和版本端点:
kubectl port-forward service/${GDCS_OPEN_WEBUI_NAME} \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" 8080:80 & PF_PID=$! sleep 2 curl --show-error --silent http://127.0.0.1:8080/health && echo curl --show-error --silent http://127.0.0.1:8080/api/version && echo kill ${PF_PID}验证 Open WebUI 是否可以访问推理后端:
kubectl exec deployment/${GDCS_OPEN_WEBUI_NAME} \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ -- sh -c 'for url in $(echo "${OLLAMA_BASE_URLS};${OPENAI_API_BASE_URLS}" | tr ";" " "); do case "${url}" in *"/v1") echo "${url}/models"; curl --max-time 10 --show-error --silent "${url}/models" | head -c 300; echo;; *) echo "${url}/api/tags"; curl --max-time 10 --show-error --silent "${url}/api/tags" | head -c 300; echo;; esac; done'在浏览器中打开
http://${OPEN_WEBUI_LB_IP}并创建第一个账号;创建的第一个账号将成为管理员。在对话视图中选择一个模型,然后发送提示。
操作
添加或更改推理后端
更新
env.d/open-webui.sh中的GDCS_OPEN_WEBUI_OLLAMA_BASE_URLS或GDCS_OPEN_WEBUI_OPENAI_API_BASE_URLS,获取环境文件,然后再次运行网页界面中的清单步骤。然后应用清单并重启 Open WebUI,由于停用了持久配置,Open WebUI 会在每次启动时读取ConfigMap:kubectl apply \ --filename="${GDCS_USER_GUIDE_HOME}/${GDCS_OPEN_WEBUI_NAME}-deployment.yaml" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" kubectl rollout restart deployment/${GDCS_OPEN_WEBUI_NAME} \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
升级
在环境文件中设置新的
GDCS_OPEN_WEBUI_IMAGE_TAG,获取该文件,为新映像设定种子,然后滚动Deployment:source ${HOME}/gdcag-solutions/open-weight-models/open-webui/env.sh declare -a GDCS_REGISTRY_IMAGES=( "ghcr.io/open-webui/open-webui:${GDCS_OPEN_WEBUI_IMAGE_TAG}" ) export SERIALIZED_IMAGES=$(declare -p GDCS_REGISTRY_IMAGES) ${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh kubectl set image deployment/${GDCS_OPEN_WEBUI_NAME} \ open-webui="${GDCS_HARBOR_PROJECT_URI}/open-webui/open-webui:${GDCS_OPEN_WEBUI_IMAGE_TAG}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}"
备份
创建数据卷的
VolumeSnapshot(standardVolumeSnapshotClass在 GDC 标准集群上可用):cat <<EOF | kubectl apply --namespace="${GDCS_WORKLOAD_NAMESPACE}" -f - apiVersion: snapshot.storage.k8s.io/v1 kind: VolumeSnapshot metadata: name: ${GDCS_OPEN_WEBUI_NAME}-$(date +%Y%m%d-%H%M) spec: source: persistentVolumeClaimName: ${GDCS_OPEN_WEBUI_NAME}-rwo volumeSnapshotClassName: standard EOF
问题排查
| 症状 | 可能的原因 | 操作 |
|---|---|---|
| 对话视图中未提供任何模型 | 后端 Service 地址错误、后端未就绪,或后端 NetworkPolicy 不允许 Open WebUI 命名空间 |
在验证中运行后端验证步骤;检查后端 Deployment 及其 NetworkPolicy |
Pod在启动期间重启 |
PersistentVolume首次启动用时过长 |
检查 kubectl describe pod;startupProbe 允许 10 分钟 |
| 文档上传 (RAG) 失败 | 离线模式会阻止下载默认嵌入模型 | 使用 Ollama 提供的嵌入模型配置 RAG_EMBEDDING_ENGINE=ollama,或让 RAG 保持停用状态 |
LoadBalancer IP 保持 <pending> 或无法访问 |
环境中没有负载均衡器,或缺少 ProjectNetworkPolicy |
使用端口转发验证步骤;向项目 NetworkPolicy Admin 申请 ProjectNetworkPolicy |
Pod 重启后会话无效 |
更改了WEBUI_SECRET_KEY |
保持 Secret 稳定;从 secrets/open_webui_secret_key 恢复 |
清理
删除 Open WebUI 资源:
kubectl delete namespace "${GDCS_WORKLOAD_NAMESPACE}"删除本地文件:
rm -rf ${HOME}/gdcag-solutions/open-weight-models/open-webui