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: