Open WebUI

ChatGPT-style interface over the local models, with per-user history and presets

Open WebUI is the single chat interface over the model server: prompt libraries, model switching, and retrieval against Qdrant all live here.

ArgoCD configuration

Excerpt from argocd-apps/values.yaml in the argocd-apps chart, with annotations added for this site:

open-webui:
  application: true
  project: cluster-services
  # false
  autoSync: false

Chart values

The full open-webui/values.yaml from the service's own chart:

image: registry.opi5cluster.co.uk/open-webui:v0.11.0
port: 8080
url: open-webui.opi5cluster.co.uk

## Data Volume
volume:
  storageClass: longhorn-ssd-small
  size: 10Gi
  accessModes: ReadWriteOnce

resources:
  requests:
    cpu: 1
    memory: 2Gi

qdrantHost: qdrant-service.qdrant.svc.cluster.local:6333
openvinoUrl: http://openvino-service.openvino.svc.cluster.local:8000/v3

database:
  type: postgresql
  name: open_webui

Manifests & templates

templates/data-pvc.yaml

Longhorn-backed PersistentVolumeClaim for persistent data.

Show manifest
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: {{ .Release.Namespace }}-data-pvc
  namespace: {{ .Release.Namespace }}
spec:
  accessModes:
  - {{ .Values.volume.accessModes }}
  resources:
    requests:
      storage: {{ .Values.volume.size }}
  storageClassName: {{ .Values.volume.storageClass }}

templates/http-route.yaml

Gateway API HTTPRoute exposing the service through the Istio gateway under opi5cluster.co.uk.

Show manifest
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: {{ .Release.Namespace  }}-httproute
  namespace: {{ .Release.Namespace }}
  annotations:
    link.argocd.argoproj.io/external-link: "https://{{ .Values.url }}"
spec:
  parentRefs:
    - name: istio-gateway
      namespace: istio
      sectionName: websecure
  hostnames:
    - {{ .Values.url }}
  rules:
    - backendRefs:
        - name: {{ .Release.Namespace }}-service
          port: {{ .Values.port }}

templates/rollout.yaml

Argo Rollout workload: container spec, probes, resources and rollout strategy.

Show manifest
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
  name: {{ .Release.Namespace }}-rollout
  namespace: {{ .Release.Namespace }}
  labels:
    app: {{ .Release.Namespace }}
spec:
  replicas: 1
  strategy:
    canary:
      steps:
      - setWeight: 25
      - pause: {duration: 30s}
      - setWeight: 50
      - pause: {duration: 45s}
      - setWeight: 100
  selector:
    matchLabels:
      app: {{ .Release.Namespace }}
  template:
    metadata:
      labels:
        app: {{ .Release.Namespace }}
    spec:
      securityContext:
        runAsUser: 1000
        runAsGroup: 1000
        fsGroup: 1000
      containers:
      - name: {{ .Release.Namespace }}
        image: {{ .Values.image }}
        env:
        - name: OPENAI_API_BASE_URL
          value: {{ .Values.openvinoUrl | quote }}
        - name: OPENAI_API_KEY
          valueFrom:
            secretKeyRef:
              name: {{ .Release.Namespace }}-secret
              key: openvino_api_key
        - name: WEBUI_SECRET_KEY
          valueFrom:
            secretKeyRef:
              name: {{ .Release.Namespace }}-secret
              key: open_webui_secret_key
        - name: VECTOR_DB
          value: qdrant
        - name: QDRANT_URI
          value: {{ .Values.qdrantHost }}
        - name: DATABASE_TYPE
          value: {{ .Values.database.type }}
        - name: DATABASE_USER
          valueFrom:
            secretKeyRef:
              name: {{ .Release.Namespace }}-secret
              key: username
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: {{ .Release.Namespace }}-secret
              key: password
        - name: DATABASE_HOST
          valueFrom:
            secretKeyRef:
              name: {{ .Release.Namespace }}-secret
              key: hostname
        - name: DATABASE_PORT
          valueFrom:
            secretKeyRef:
              name: {{ .Release.Namespace }}-secret
              key: db_port
        - name: DATABASE_NAME
          value: {{ .Values.database.name }}
        - name: WEBUI_NAME
          value: "Personal - Open WebUI"
        - name: ENABLE_EVALUATION_ARENA_MODELS
          value: "False"
        - name: ENABLE_WEB_SEARCH
          value: "True"
        - name: WEB_SEARCH_RESULT_COUNT
          value: "3"
        # Values can be brave or brave_llm_context
        - name: WEB_SEARCH_ENGINE
          value: "brave_llm_context"
        - name: BRAVE_SEARCH_API_KEY
          valueFrom:
            secretKeyRef:
              name: {{ .Release.Namespace }}-secret
              key: brave_api_key
        # BRAVE_SEARCH_CONTEXT_TOKENS Only for brave_llm_context
        - name: BRAVE_SEARCH_CONTEXT_TOKENS
          value: "8192"
        - name: WEB_SEARCH_CONCURRENT_REQUESTS
          value: "1"
        - name: ENABLE_IMAGE_GENERATION
          value: "False"
        - name: ENABLE_IMAGE_PROMPT_GENERATION
          value: "False"
        - name: ENABLE_IMAGE_UPLOAD
          value: "False"
        - name: ENABLE_IMAGE_DOWNLOAD
          value: "False"
        ports:
        - containerPort: {{ .Values.port }}
        resources:
          {{- toYaml .Values.resources | nindent 10 }}
        volumeMounts:
        - name: data
          mountPath: /app/backend/data
          readOnly: false
      dnsPolicy: ClusterFirst
      restartPolicy: Always
      schedulerName: default-scheduler
      volumes:
      - name: data
        persistentVolumeClaim:
          claimName: {{ .Release.Namespace }}-data-pvc

templates/secret.yaml

Secret resources for values managed outside Vault.

Show manifest
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: {{ .Release.Namespace }}-external-secret
  namespace: {{ .Release.Namespace }}
spec:
  refreshInterval: 24h
  secretStoreRef:
    kind: ClusterSecretStore
    name: vault-cluster-secret-store
  target:
    name: {{ .Release.Namespace }}-secret
    creationPolicy: Owner
  data:
    ## Database
    - secretKey: password
      remoteRef:
        key: postgres
        property: user_password
    - secretKey: username
      remoteRef:
        key: postgres
        property: user_name
    - secretKey: hostname
      remoteRef:
        key: postgres
        property: internal_host_name
    - secretKey: db_port
      remoteRef:
        key: postgres
        property: port

    # Open-WebUi
    - secretKey: open_webui_secret_key
      remoteRef:
        key: open-webui
        property: secret-key
    - secretKey: brave_api_key
      remoteRef:
        key: genai
        property: brave-api-key
    - secretKey: openvino_api_key
      remoteRef:
        key: genai
        property: openvino-api-key

templates/service.yaml

ClusterIP Service fronting the workload.

Show manifest
apiVersion: v1
kind: Service
metadata:
  name: {{ .Release.Namespace }}-service
  namespace: {{ .Release.Namespace }}
spec:
  type: ClusterIP
  selector:
    app: {{ .Release.Namespace }}
  ports:
    - name: port
      protocol: TCP
      port: {{ .Values.port }}
      targetPort: {{ .Values.port }}

Trade-offs

Decision. One chat UI for every model

Alternative. Per-application UIs

Why. Prompts, history, and RAG configuration stay in one place instead of scattering across tools.

← Back to AI Stack · All service groups