↓Skip to main content

LibreChat Helm Setup with Ollama and qwen3.5:9b

1988 words·
LibreChat Ollama ML LLM NVIDIA qwen3.5:9b Kubernetes Helm
Table of Contents

Setup Overview and Notes
#

For this setup I use a single node k3s instance with the following setup:

  • StorageClass: local-path
  • CNI: Cilium
  • Routing: kgateway with MetalLB

Ollama runs on a separate host at 10.0.150.101 and loads the selected LLM when LibreChat sends its first request.


Helm Chart Overview
#

# List available Helm chart versions
oras repo tags ghcr.io/danny-avila/librechat-chart/librechat

# Shell output:
2.0.13
2.0.14
# List Helm chart information
helm show chart oci://ghcr.io/danny-avila/librechat-chart/librechat --version 2.0.14
# Save Helm chart values
helm show values oci://ghcr.io/danny-avila/librechat-chart/librechat --version 2.0.14 > librechat-values-2.0.14.yml

Ansible / Installation
#

Playbook
#

- name: LibreChat Helm installation
  hosts: localhost
  connection: local
  gather_facts: false
  become: false
  vars:
    # Kubernetes Secret
    librechat_creds_key: "eabe28df2d7479e3a5bccec300b391a9ee5f183ba6dcbd11d8b38acc596ed947"
    librechat_creds_iv: "9761c7921a170028950ae28303e95d5f"
    librechat_jwt_secret: "274bb72b414f4ea39e0164b0894f1c22e9fa4a4b2c95dc455ab03bb1f10ba74f"
    librechat_jwt_refresh_secret: "9f6c93bc359d09d1414dc039ae3b5a993ae3a22458a8586d72ac1f2f452681be"
    librechat_meili_master_key: "d3900bf31ca85b7f48ad7fee39cbcbeb"
    # Helm Configuration
    helm_chart: "oci://ghcr.io/danny-avila/librechat-chart/librechat"
    helm_chart_version: "2.0.14"
    helm_release_name: "librechat"
    # Kubernetes Configuration
    kubernetes_namespace: "librechat"
    storage_class: "local-path"
    pvc_mongodb: "8Gi"
    pvc_librechat: "10Gi"
    pvc_meilisearch: "5Gi"
    # Routing
    librechat_domain: "librechat.jklug.work"
    gateway_name: "kgateway-metallb"
    gateway_namespace: "kgateway-infra"
    gateway_listener: "https"
    # LLM
    ollama_base_url: "http://10.0.150.101:11434/v1/"
    ollama_default_model: "qwen3.5:9b"

  roles:
    - k8s_librechat

Tasks
#

k8s_librechat/tasks/main.yml

- name: Create namespace
  kubernetes.core.k8s:
    api_version: v1
    kind: Namespace
    name: "{{ kubernetes_namespace }}"
    state: present


- name: Create LibreChat credentials Secret
  kubernetes.core.k8s:
    state: present
    definition:
      apiVersion: v1
      kind: Secret
      metadata:
        name: librechat-credentials-env
        namespace: "{{ kubernetes_namespace }}"
      type: Opaque
      stringData:
        CREDS_KEY: "{{ librechat_creds_key }}"
        CREDS_IV: "{{ librechat_creds_iv }}"
        JWT_SECRET: "{{ librechat_jwt_secret }}"
        JWT_REFRESH_SECRET: "{{ librechat_jwt_refresh_secret }}"
        MEILI_MASTER_KEY: "{{ librechat_meili_master_key }}"
  no_log: true


- name: Create LibreChat configuration ConfigMap
  kubernetes.core.k8s:
    state: present
    definition:
      apiVersion: v1
      kind: ConfigMap
      metadata:
        name: librechat-config
        namespace: "{{ kubernetes_namespace }}"
      data:
        librechat.yaml: "{{ lookup('template', 'librechat-cm.yml.j2') }}"
  register: librechat_configmap


- name: Install Helm Chart
  kubernetes.core.helm:
    name: "{{ helm_release_name }}"
    chart_ref: "{{ helm_chart }}"
    chart_version: "{{ helm_chart_version }}"
    release_namespace: "{{ kubernetes_namespace }}"
    create_namespace: false
    wait: true  # Ansible waits till all resources are ready
    wait_timeout: 5m0s
    atomic: false  # Auto-rollback on failure
    values: "{{ lookup('template', 'helm-values.yml.j2') | from_yaml }}"


- name: Create LibreChat HTTPRoute
  kubernetes.core.k8s:
    state: present
    definition:
      apiVersion: gateway.networking.k8s.io/v1
      kind: HTTPRoute
      metadata:
        name: librechat
        namespace: "{{ kubernetes_namespace }}"
      spec:
        hostnames:
          - "{{ librechat_domain }}"
        parentRefs:
          - group: gateway.networking.k8s.io
            kind: Gateway
            name: "{{ gateway_name }}"
            namespace: "{{ gateway_namespace }}"
            sectionName: "{{ gateway_listener }}"
        rules:
          - matches:
              - path:
                  type: PathPrefix
                  value: /
            backendRefs:
              - group: ""
                kind: Service
                name: "{{ helm_release_name }}-librechat"
                port: 3080
                weight: 1

Templates
#

ConfigMap
#

k8s_librechat/templates/librechat-cm.yml.j2

version: 1.3.13

cache: true

endpoints:
  custom:
    - name: "Ollama"
      apiKey: "ollama"
      baseURL: "{{ ollama_base_url }}"
      models:
        default:
          - "{{ ollama_default_model }}"
        fetch: true
      titleConvo: true
      titleModel: "current_model"
      summarize: false
      summaryModel: "current_model"
      modelDisplayLabel: "Ollama"

Values
#

k8s_librechat/templates/helm-values.yml.j2

Pulled: ghcr.io/danny-avila/librechat-chart/librechat:2.0.8
Digest: sha256:f9f7d824f1b27b4add9637c63797d01f46ae26df736f9c68a0e789525d093d89
# Default values for librechat.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1



global:
   # default Secret for envs/ only Passwords. Can be locally generated with: kubectl create secret generic librechat-secret-envs --from-env-file=.env.example --dry-run=client -o yaml > secret-envs.yaml
   # For better maintainabillity, you can put all vars directly in the config Section and only overwrite Secrets with this if nessesary.
   # Required Values:
   # - CREDS_KEY
   # - CREDS_IV
   # - JWT_SECRET
   # - JWT_REFRESH_SECRET
   # - MEILI_MASTER_KEY
  librechat:
    existingSecretName: "librechat-credentials-env"
    # Used for Setting the Right Key, can be something like AZURE_API_KEY, if Azure OpenAI is used
    existingSecretApiKey: OPENAI_API_KEY
    # Optionally add extra globally accessible environment variables here. These can be referenced under ConfigEnv to make them accessible inside LibreChat
    # env:
    #  - name: API_KEY
    #    valueFrom:
    #      secretKeyRef:
    #        name: api_access
    #        key: api_key
    #  - name: CLIENT_ID
    #    valueFrom:
    #      secretKeyRef:
    #        name: credentials
    #        key: client_id

librechat:
  # External admin panel base URL used for admin OAuth/SSO redirects.
  # Required when deploying the admin panel on a separate URL.
  # May include a path. Do not include a trailing slash.
  # Example: https://admin.example.com/admin
  adminPanelUrl: ""

  configEnv:
    DOMAIN_CLIENT: "https://{{ librechat_domain }}"
    DOMAIN_SERVER: "https://{{ librechat_domain }}"
    TRUST_PROXY: "1"
    NO_INDEX: "true"
    # User creation
    ALLOW_EMAIL_LOGIN: "true"
    ALLOW_REGISTRATION: "true"
    ALLOW_UNVERIFIED_EMAIL_LOGIN: "true"
    ALLOW_PASSWORD_RESET: "false"
    # Set unique, persistent values in global.librechat.existingSecretName before production use.
    # If omitted, LibreChat generates temporary values in .env.temp when the container filesystem is persistent.
    # Set Config Params here
    # ENV_NAME: env-value

    # existing Secret for all envs/ only Passwords. Can be locally generated with: kubectl create secret generic librechat-secret-envs --from-env-file=.env.example --dry-run=client -o yaml > secret-envs.yaml
    # For better maintainabillity, you can put all vars directly in the config Section and only overwrite Secrets with this if nessesary.
    # Required Values:
    # - MEILI_MASTER_KEY
  existingSecretName: "librechat-credentials-env"
  
  # For adding a custom config yaml-file you can set the contents in this var. See https://www.librechat.ai/docs/configuration/librechat_yaml/example
  configYamlContent: ""
  # configYamlContent: |
  #   version: 1.0.8

  #   cache: true

  #   interface:
  #     # Privacy policy settings
  #     privacyPolicy:
  #       externalUrl: 'https://librechat.ai/privacy-policy'
  #       openNewTab: true

  #     # Terms of service
  #     termsOfService:
  #       externalUrl: 'https://librechat.ai/tos'
  #       openNewTab: true

  #   registration:
  #     socialLogins: ["discord", "facebook", "github", "google", "openid"] 
  #   endpoints:
  #     azureOpenAI:
  #      # Endpoint-level configuration
  #      titleModel: "gpt-4o"
  #      plugins: true
  #      assistants: true
  #      groups:
  #           Group-level configuration
  #         - group: "my-resource-sweden"
  #           apiKey: "${SWEDEN_API_KEY}"
  #           instanceName: "my-resource-sweden"
  #           deploymentName: gpt-4-1106-preview
  #           version: "2024-03-01-preview"
  #           assistants: true
  #           # Model-level configuration
  #           models:
  #             gpt-4o: true
  #     custom:
  #       # OpenRouter.ai
  #       - name: "OpenRouter"
  #         apiKey: "${OPENROUTER_KEY}"
  #         baseURL: "https://openrouter.ai/api/v1"
  #         models:
  #           default: ["openai/gpt-3.5-turbo"]
  #           fetch: true
  #         titleConvo: true
  #         titleModel: "gpt-3.5-turbo"
  #         summarize: false
  #         summaryModel: "gpt-3.5-turbo"
  #         modelDisplayLabel: "OpenRouter"

  # name of existing Yaml configmap, key must be librechat.yaml
  existingConfigYaml: "librechat-config"

  # Volume used to store image Files uploaded to the Web UI
  imageVolume:
    enabled: true
    size: "{{ pvc_librechat }}"
    accessModes: ReadWriteOnce
    storageClassName: "{{ storage_class }}"

# only lite RAG is supported
librechat-rag-api:
  enabled: false
  # can be azure, openai, huggingface or huggingfacetei
  embeddingsProvider: openai


image:
  repository: danny-avila/librechat
  registry: registry.librechat.ai
  pullPolicy: IfNotPresent
  # Overrides the image tag whose default is the chart appVersion.
  tag: ""


imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/
serviceAccount:
  # Specifies whether a service account should be created
  create: true
  # Automatically mount a ServiceAccount's API credentials?
  automount: true
  # Annotations to add to the service account
  annotations: {}
  # The name of the service account to use.
  # If not set and create is true, a name is generated using the fullname template
  name: ""

lifecycle: {}
# # base for adding a custom banner // see https://github.com/danny-avila/LibreChat/pull/3952 for an example
#   postStart:
#     exec:
#       command: ["/bin/sh", "-c", "npm run update-banner <displayFrom(Format: yyyy-mm-ddTHH:MM:SSZ)> <displayTo(Format: yyyy-mm-ddTHH:MM:SSZ)> <message> <isPublic(true/false)>"]



podAnnotations: {}
podLabels: {}
deploymentAnnotations: {}
deploymentLabels: {}

# Enable or disable injection of service environment variables into pods.
# When running in namespaces with many services, the injected variables can cause
# "argument list too long" errors. Set to false to disable.
enableServiceLinks: true

podSecurityContext:
  fsGroup: 2000

securityContext:
  capabilities:
    drop:
    - ALL
  # readOnlyRootFilesystem: true # not supported yet
  runAsNonRoot: true
  runAsUser: 1000

service:
  type: ClusterIP # LoadBalancer, NodePort, ClusterIP
  port: 3080
  targetPort: 3080
  containerPort: 3080
  annotations: {}

ingress:
  enabled: false
  className: ""
  annotations: {}
    # kubernetes.io/ingress.class: nginx
    # kubernetes.io/tls-acme: "true"
  hosts:
    - host: chat.example.com
      paths:
        - path: /
          pathType: ImplementationSpecific
  tls: []
  #  - secretName: chart-example-tls
  #    hosts:
  #      - chat.example.com

resources: {}
  # We usually recommend not to specify default resources and to leave this as a conscious
  # choice for the user. This also increases chances charts run on environments with little
  # resources, such as Minikube. If you do want to specify resources, uncomment the following
  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
  # limits:
  #   cpu: 100m
  #   memory: 128Mi
  # requests:
  #   cpu: 100m
  #   memory: 128Mi


autoscaling:
  enabled: false
  minReplicas: 1
  maxReplicas: 100
  targetCPUUtilizationPercentage: 80
  # targetMemoryUtilizationPercentage: 80

livenessProbe:
  httpGet:
    path: /health
    port: 3080
readinessProbe:
  httpGet:
    path: /health
    port: 3080

# Additional init containers on the output Deployment definition.
initContainers: {}
#   foo: # the name of the init container
#     image: busybox
#     command: ['sh', '-c', 'echo The app is starting! && sleep 5']
#   # ... add more init containers as needed

# Additional volumes on the output Deployment definition.
volumes: []
# - name: foo
#   secret:
#     secretName: mysecret
#     optional: false

# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
#   mountPath: "/etc/foo"
#   readOnly: true

nodeSelector: {}

tolerations: []

affinity: {}

# Host aliases for custom domain-to-IP mappings (adds entries to /etc/hosts)
hostAliases: []
# Example - Redirect AWS Bedrock to proxy:
# hostAliases:
#   - ip: "10.1.2.3"
#     hostnames:
#       - "bedrock-runtime.eu-central-1.amazonaws.com"

# DNS Configuration
# Customize DNS resolution for redirecting traffic to proxy servers
dnsPolicy: ""  # Options: ClusterFirst, Default, None, ClusterFirstWithHostNet
dnsConfig: {}
# Example configuration for custom DNS:
# dnsPolicy: "None"  # Use only custom DNS settings
# dnsConfig:
#   nameservers:
#     - "10.0.0.10"  # Your custom DNS server
#     - "8.8.8.8"    # Fallback DNS
#   searches:
#     - "svc.cluster.local"
#     - "cluster.local"
#   options:
#     - name: ndots
#       value: "2"
#     - name: timeout
#       value: "1"

# Strategy for LibreChat deployment updates
updateStrategy:
  type: RollingUpdate

langfuseFanout:
  enabled: false
  replicaCount: 1
  image:
    repository: librechat-langfuse-fanout
    tag: "latest"
    pullPolicy: IfNotPresent
  otelCollector:
    receiverEndpoint: 127.0.0.1:4319
    image:
      repository: otel/opentelemetry-collector-contrib
      tag: "0.143.0"
      pullPolicy: IfNotPresent
    resources: {}
  service:
    type: ClusterIP
    port: 4318
    annotations: {}
  central:
    baseUrl: https://cloud.langfuse.com
    authHeaderSecret:
      name: ""
      key: LANGFUSE_FANOUT_CENTRAL_AUTH_HEADER
  metrics:
    secret:
      # Optional bearer token secret for scraping the gateway's /metrics endpoint.
      # When omitted, /metrics returns 401.
      name: ""
      key: METRICS_SECRET
  redis:
    # Redis stores short-lived Langfuse media upload plans so multiple gateway
    # replicas can handle create/upload requests. If empty and redis.enabled is
    # true, the chart derives the bundled Redis service URI.
    uri: ""
    username: ""
    passwordSecret:
      name: ""
      key: REDIS_PASSWORD
    keyPrefix: langfuse-fanout
  tenant:
    # Destination map keys are emitted by LibreChat as trace attributes and
    # matched by the gateway. Use lowercase keys matching ^[a-z][a-z0-9_-]*$.
    destinations:
      eu:
        baseUrl: https://cloud.langfuse.com
      us:
        baseUrl: https://us.cloud.langfuse.com
      jp:
        baseUrl: https://jp.cloud.langfuse.com
  upstreamTimeout: 30s
  # Optional override for one-time media upload URLs returned by the gateway.
  # When empty, the chart derives the internal fanout Service URL.
  publicUrl: ""
  traceCollectorUrl: http://127.0.0.1:4319
  memoryLimitMiB: 256
  memorySpikeLimitMiB: 64
  batchTimeout: 1s
  batchSendSize: 128
  metadataCardinalityLimit: 1000
  livenessProbe:
    httpGet:
      path: /healthz
      port: 4318
  readinessProbe:
    httpGet:
      path: /healthz
      port: 4318
  resources: {}
  podAnnotations: {}
  podLabels: {}

# Extra ConfigMaps to be created alongside the main ones
additionalConfigMaps: {}
#   custom: # suffix of the ConfigMap name
#     labels: {}
#     annotations: {}
#     data: {}
#     binaryData: {}
#     immutable: false
#   # ... add more ConfigMaps as needed

# MongoDB Parameters
mongodb:
  enabled: true
  # Bitnami moved versioned image tags to docker.io/bitnamilegacy on 2025-08-28.
  # See https://github.com/bitnami/charts/issues/35164
  image:
    repository: bitnamilegacy/mongodb
  auth:
    enabled: false
  databases:
   - LibreChat
  persistence: 
    size: "{{ pvc_mongodb }}"


meilisearch:
  enabled: true
  persistence:
    enabled: true
    storageClass: "{{ storage_class }}"
  image:
    tag: "v1.7.3"
  auth:
    # Use an existing Kubernetes secret for the MEILI_MASTER_KEY
    existingMasterKeySecret: "librechat-credentials-env"
  persistence:
    enabled: true
    size: "{{ pvc_meilisearch }}"
    accessMode: ReadWriteOnce
    storageClass: "{{ storage_class }}"

# Redis Parameters
redis:
  enabled: false
  architecture: standalone
  auth:
    enabled: false



Kubernetes Resources
#

# List default resources
kubectl -n librechat get all 

# Shell output:
NAME                                       READY   STATUS    RESTARTS   AGE
pod/librechat-librechat-789f5fdd4b-cqnf6   1/1     Running   0          107s
pod/librechat-meilisearch-0                1/1     Running   0          107s
pod/librechat-mongodb-7cdc596fd-twvd5      1/1     Running   0          107s

NAME                            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)     AGE
service/librechat-librechat     ClusterIP   10.201.149.252   <none>        3080/TCP    107s
service/librechat-meilisearch   ClusterIP   10.201.21.224    <none>        7700/TCP    107s
service/librechat-mongodb       ClusterIP   10.201.235.29    <none>        27017/TCP   107s

NAME                                  READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/librechat-librechat   1/1     1            1           107s
deployment.apps/librechat-mongodb     1/1     1            1           107s

NAME                                             DESIRED   CURRENT   READY   AGE
replicaset.apps/librechat-librechat-789f5fdd4b   1         1         1       107s
replicaset.apps/librechat-mongodb-7cdc596fd      1         1         1       107s

NAME                                     READY   AGE
statefulset.apps/librechat-meilisearch   1/1     107s
# List PVC
kubectl -n librechat get pvc

# Shell output:
NAME                         STATUS   VOLUME                                     CAPACITY   ACCESS MODES   STORAGECLASS   VOLUMEATTRIBUTESCLASS   AGE
librechat-librechat-images   Bound    pvc-70955ed0-1d14-4ace-b6df-45c9f4e562d6   10Gi       RWO            local-path     <unset>                 31m
librechat-meilisearch        Bound    pvc-65c37e15-8e64-49c8-ad65-6ecc54fc76a3   5Gi        RWO            local-path     <unset>                 31m
librechat-mongodb            Bound    pvc-7eee2266-6e65-46b8-b65a-4ac436b17242   8Gi        RWO            local-path     <unset>                 31m
# List HTTPRoute
kubectl -n librechat get httproute

# Shell output:
NAME        HOSTNAMES                  AGE
librechat   ["librechat.jklug.work"]   4m44s



LibreChat Webinterface
#

Make sure to the select the “qwen3.5:9b” model at the top of the menu: