litellm/helm/litellm-helm
devin-ai-integration[bot] d98522b6f6
feat(proxy): share database connections across workers with an in-container pgbouncer (#39683)
* feat(proxy): share database connections across workers with an in-container pgbouncer

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): parse pgbouncer options iteratively to satisfy the recursion gate

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): refuse pgbouncer with token db auth and retry failed pooler restarts

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): build pgbouncer 1.25.2 from a pinned source archive and verify pooler replacements

The public Wolfi repository only carries pgbouncer 1.24.1-r3, which the image
scan rejects (CVE-2026-6664, CVE-2026-6665, CVE-2026-6666, CVE-2025-12819).
All three images now compile the checksummed 1.25.2 release in a builder stage.

The supervisor now waits for a replacement pooler to listen before treating it
as recovered, ends and retries one that never does, and takes the same lock for
stop() and spawn so no replacement can be started after shutdown began.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): refuse to start pgbouncer on a loopback port another process already owns

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): count pgbouncer ready only once its own unix socket answers, not any listener on the port

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): refuse pgbouncer older than 1.19, whose unix socket cannot vouch for the tcp port

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(helm,terraform): expose the in-container pgbouncer pool for the componentized gateway

Add database.connectionPool to helm/litellm and gateway_connection_pool_* to
terraform/litellm/aws so the componentized gateway can receive the
LITELLM_PGBOUNCER_* env the classic image already honours. Both reject the
pool under IAM or Entra token auth at render/plan time: the pooler holds one
static database password for the life of the pod or task.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(gateway): launch the componentized gateway image through a pgbouncer-aware supervisor (#40592)

The componentized gateway image started uvicorn directly, so the in-container
PgBouncer never ran for it: every worker opened its own Prisma pool to the
database. It also passed no keep-alive timeout, so behind a load balancer with
a 60s idle timeout uvicorn's 5s default closed idle connections first and the
balancer returned 502s on scale-out

gateway.launch assembles DATABASE_URL, starts PgBouncer once per pod when
LITELLM_PGBOUNCER_ENABLED is set, hands the workers the loopback URL and then
runs uvicorn on gateway.main:app with KEEPALIVE_TIMEOUT as --timeout-keep-alive.
The image builds PgBouncer 1.25.2 from a checksummed tarball, copies the
compiled Rust extension into the /app source tree it imports from (it was only
in site-packages, which PYTHONPATH=/app shadows) and asserts the native bridge
loads. The app user is added to stats_users so operators can read the PgBouncer
console with the application credentials

The supervisor returns the pooled URL instead of writing into the mapping it
was handed, a database user whose name PgBouncer would split into several
stats_users entries is refused before the config is written, and the launcher
tests drive main() with an injected serve callable

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* docs(terraform): describe the gateway.launch pooler entrypoint in the aws module README

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(proxy): run pgbouncer exit hooks only in the parent and copy the CA into the runtime dir

Gunicorn workers inherit the parent's atexit table, so a recycled worker (max_requests) stopped the shared pooler and removed its runtime dir, then hung in the inherited Popen lock. The hooks now no-op unless os.getpid() is the process that started PgBouncer

A verified TLS upstream named the operator's CA bundle directly, which is often a 0600 root-owned file that nobody (the user PgBouncer drops to) cannot read, so every server connection failed with "failed to load CA". The bundle is copied into the runtime dir next to the ini and chowned with it

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* feat(terraform): run the gateway through gateway.launch and add the gcp connection-pool variables

Cloud Run and ECS overrode the image command with uvicorn gateway.main:app, which skips the supervisor that starts the in-container PgBouncer, so LITELLM_PGBOUNCER_ENABLED was inert on both stacks. Both now exec python -m gateway.launch (under ddtrace-run when USE_DDTRACE is set), and the gcp module gains gateway_connection_pool_enabled / gateway_pool_max_db_connections / gateway_pool_max_client_conn wired to the gateway service only

The test_launch password_env fixture now restores DATABASE_URL even when it was unset: monkeypatch.delenv records nothing for an absent var, so main() left postgresql://...@db.internal in the xdist worker's environ and the key-rotation e2e test in the same proxy-infra shard stopped skipping and tried to reach db.internal

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

* fix(pgbouncer): keep channel_binding and gssencmode off the loopback URL

Prisma would demand TLS channel binding from a pooler that only speaks
plain TCP on 127.0.0.1. Also pass the request the marketplace test
started needing after #40518 landed on top of #40496

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

---------

Co-authored-by: yassin <yassin@berri.ai>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-09-10 22:14:37 +00:00
..
charts refactor(helm): move litellm-helm chart to helm/ and drop deploy folder (#32234) 2026-07-07 15:18:33 +03:00
ci refactor(helm): move litellm-helm chart to helm/ and drop deploy folder (#32234) 2026-07-07 15:18:33 +03:00
templates feat(proxy): share database connections across workers with an in-container pgbouncer (#39683) 2026-09-10 22:14:37 +00:00
tests feat(proxy): share database connections across workers with an in-container pgbouncer (#39683) 2026-09-10 22:14:37 +00:00
.helmignore refactor(helm): move litellm-helm chart to helm/ and drop deploy folder (#32234) 2026-07-07 15:18:33 +03:00
Chart.lock fix(helm): pin bundled postgres and redis to the bitnamilegacy images (#34963) 2026-07-28 16:19:20 -07:00
Chart.yaml fix(helm): scale the classic chart's HPA out at the documented 60 percent CPU (#35975) 2026-09-02 10:19:03 -07:00
README.md fix(helm): reuse the generated master key Secret on helm upgrade (#39219) 2026-09-01 17:45:59 -07:00
values.yaml feat(proxy): share database connections across workers with an in-container pgbouncer (#39683) 2026-09-10 22:14:37 +00:00

Helm Chart for LiteLLM

Important

This is community maintained, Please make an issue if you run into a bug We recommend using Docker or Kubernetes for production deployments

Prerequisites

  • Kubernetes 1.21+
  • Helm 3.8.0+

If db.deployStandalone is used:

  • PV provisioner support in the underlying infrastructure

If db.useStackgresOperator is used (not yet implemented):

  • The Stackgres Operator must already be installed in the Kubernetes Cluster. This chart will not install the operator if it is missing.

Parameters

LiteLLM Proxy Deployment Settings

Name Description Value
replicaCount The number of LiteLLM Proxy pods to be deployed 1
masterkeySecretName The name of the Kubernetes Secret that contains the Master API Key for LiteLLM. If not specified, use the generated secret name. N/A
masterkeySecretKey The key within the Kubernetes Secret that contains the Master API Key for LiteLLM. If not specified, use masterkey as the key. N/A
masterkey The Master API Key for LiteLLM. If not specified, a random key in the sk-... format is generated on first install and reused on upgrades. N/A
environmentSecrets An optional array of Secret object names. The keys and values in these secrets will be presented to the LiteLLM proxy pod as environment variables. See below for an example Secret object. []
environmentConfigMaps An optional array of ConfigMap object names. The keys and values in these configmaps will be presented to the LiteLLM proxy pod as environment variables. See below for an example Secret object. []
image.repository LiteLLM Proxy image repository ghcr.io/berriai/litellm
image.pullPolicy LiteLLM Proxy image pull policy IfNotPresent
image.tag Overrides the image tag whose default the latest version of LiteLLM at the time this chart was published. ""
imagePullSecrets Registry credentials for the LiteLLM and initContainer images. []
serviceAccount.create Whether or not to create a Kubernetes Service Account for this deployment. The default is false because LiteLLM has no need to access the Kubernetes API. false
service.type Kubernetes Service type (e.g. LoadBalancer, ClusterIP, etc.) ClusterIP
service.port TCP port that the Kubernetes Service will listen on. Also the TCP port within the Pod that the proxy will listen on. 4000
livenessProbe.* Liveness probe settings for the LiteLLM container (path, periodSeconds, timeoutSeconds, thresholds, and initial delay). See values.yaml
readinessProbe.* Readiness probe settings for the LiteLLM container (path, periodSeconds, timeoutSeconds, thresholds, and initial delay). See values.yaml
startupProbe.* Startup probe settings for the LiteLLM container (path, periodSeconds, timeoutSeconds, thresholds, and initial delay). See values.yaml
resources.* CPU/memory requests and limits for the LiteLLM container. Unset by default; production deployments should set 1 CPU and 4Gi of memory per worker. {}
service.loadBalancerClass Optional LoadBalancer implementation class (only used when service.type is LoadBalancer) ""
ingress.labels Additional labels for the Ingress resource {}
ingress.* See values.yaml for example settings N/A
proxyConfigMap.create When true, render a ConfigMap from .Values.proxy_config and mount it. true
proxyConfigMap.name When create=false, name of the existing ConfigMap to mount. ""
proxyConfigMap.key Key in the ConfigMap that contains the proxy config file. "config.yaml"
proxy_config.* See values.yaml for default settings. Rendered into the ConfigMaps config.yaml only when proxyConfigMap.create=true. See example_config_yaml for configuration examples. N/A
extraContainers[] An array of additional containers to be deployed as sidecars alongside the LiteLLM Proxy.
pdb.enabled Enable a PodDisruptionBudget for the LiteLLM proxy Deployment false
pdb.minAvailable Minimum number/percentage of pods that must be available during voluntary disruptions (choose one of minAvailable/maxUnavailable) null
pdb.maxUnavailable Maximum number/percentage of pods that can be unavailable during voluntary disruptions (choose one of minAvailable/maxUnavailable) null
pdb.annotations Extra metadata annotations to add to the PDB {}
pdb.labels Extra metadata labels to add to the PDB {}

| billingMetrics.enabled | Enable enterprise billable-request metering. Requires an enterprise license. | false | | billingMetrics.endpoint | Collector that the billable-request counter is pushed to. | https://telemetry.litellm.ai | | billingMetrics.secretName | Name of an existing Secret holding the mTLS client certificate, under the keys tls.crt and tls.key. | litellm-billing-metrics-mtls | | billingMetrics.caSecretName | Name of an existing Secret holding a CA bundle under the key ca.crt. Only needed for a private or test collector whose server certificate is not on the public web PKI. | "" | | billingMetrics.exportIntervalMs | How often the counter is pushed, in milliseconds. The proxy defaults to 60000 when unset. | "" |

Example proxy_config ConfigMap from values (default):

proxyConfigMap:
  create: true
  key: "config.yaml"

proxy_config:
  general_settings:
    master_key: os.environ/PROXY_MASTER_KEY
  model_list:
    - model_name: gpt-3.5-turbo
      litellm_params:
        model: gpt-3.5-turbo
        api_key: eXaMpLeOnLy

Example using existing proxyConfigMap instead of creating it:

proxyConfigMap:
  create: false
  name: my-litellm-config
  key: config.yaml

# proxy_config is ignored in this mode

Example environmentSecrets Secret

apiVersion: v1
kind: Secret
metadata:
  name: litellm-envsecrets
data:
  AZURE_OPENAI_API_KEY: TXlTZWN1cmVLM3k=
type: Opaque

Enterprise billable-request metering

Enterprise licenses meter billable requests by pushing a counter to LiteLLM's collector over mutual TLS. The chart does not create the client certificate; it mounts one you already hold, read-only, so the private key is never exposed through the environment. Create the Secret under the name the chart expects, then turn the block on:

kubectl create secret tls litellm-billing-metrics-mtls --cert=client.crt --key=client.key
billingMetrics:
  enabled: true

Set billingMetrics.caSecretName only when the collector is a private or test one whose server certificate is not on the public web PKI; the production collector needs no CA override. The chart fails the render rather than deploying a proxy that silently never exports, so a missing secretName or an emptied endpoint surfaces at helm install time.

Database Settings

Name Description Value
db.useExisting Use an existing Postgres database. A Kubernetes Secret object must exist that contains credentials for connecting to the database. An example secret object definition is provided below. false
db.endpoint If db.useExisting is true, this is the IP, Hostname or Service Name of the Postgres server to connect to. localhost
db.database If db.useExisting is true, the name of the existing database to connect to. litellm
db.url If db.useExisting is true, the connection url of the existing database to connect to can be overwritten with this value. postgresql://$(DATABASE_USERNAME):$(DATABASE_PASSWORD)@$(DATABASE_HOST)/$(DATABASE_NAME)
db.secret.name If db.useExisting is true, the name of the Kubernetes Secret that contains credentials. postgres
db.secret.usernameKey If db.useExisting is true, the name of the key within the Kubernetes Secret that holds the username for authenticating with the Postgres instance. username
db.secret.passwordKey If db.useExisting is true, the name of the key within the Kubernetes Secret that holds the password associates with the above user. password
db.useStackgresOperator Not yet implemented. false
db.deployStandalone Deploy a standalone, single instance deployment of Postgres, using the Bitnami postgresql chart. This is useful for getting started but doesn't provide HA or (by default) data backups. true
postgresql.* If db.deployStandalone is true, configuration passed to the Bitnami postgresql chart. See the Bitnami Documentation for full configuration details. See values.yaml for the default configuration. See values.yaml
postgresql.auth.* If db.deployStandalone is true, care should be taken to ensure the default password and postgres-password values are NOT used. NoTaGrEaTpAsSwOrD
postgresql.image.* If db.deployStandalone is true, the image for the bundled Postgres. Pinned to a docker.io/bitnamilegacy build because Bitnami retired the versioned tags under docker.io/bitnami. bitnamilegacy/postgresql:16.2.0-debian-12-r6
redis.image.* If redis.enabled is true, the image for the bundled Redis. Pinned to a docker.io/bitnamilegacy build for the same reason. bitnamilegacy/redis:7.2.4-debian-12-r9

Bundled Postgres image

Bitnami removed the versioned tags from docker.io/bitnami and republished the archived builds under docker.io/bitnamilegacy, so the image defaults that ship inside the postgresql and redis subcharts no longer pull. The chart pins both to the bitnamilegacy copies of the exact builds those subchart versions were released with, which keeps the on-disk data directory layout unchanged for existing installs.

Keep postgresql.image.tag pinned. docker.io/bitnami/postgresql still publishes a floating latest, and pointing the bundled Postgres at a different major version starts the server against a data directory it cannot read (database files are incompatible with server). There is no in-place way back, so crossing a major version means dumping the database with the old image and restoring it into the new one. The chart refuses to render when the tag is empty or latest.

Those images no longer receive updates. For anything beyond getting started, run Postgres outside the chart and point at it with db.useExisting.

Example Postgres db.useExisting Secret

apiVersion: v1
kind: Secret
metadata:
  name: postgres
data:
  # Password for the "postgres" user
  postgres-password: <some secure password, base64 encoded>
  username: litellm
  password: <some secure password, base64 encoded>
type: Opaque

Examples for environmentSecrets and environemntConfigMaps

# Use config map for not-secret configuration data
apiVersion: v1
kind: ConfigMap
metadata:
  name: litellm-env-configmap
data:
  SOME_KEY: someValue
  ANOTHER_KEY: anotherValue
# Use secrets for things which are actually secret like API keys, credentials, etc
# Base64 encode the values stored in a Kubernetes Secret: $ pbpaste | base64 | pbcopy
# The --decode flag is convenient: $ pbpaste | base64 --decode

apiVersion: v1
kind: Secret
metadata:
  name: litellm-env-secret
type: Opaque
data:
  SOME_PASSWORD: cDZbUGVXeU5e0ZW # base64 encoded
  ANOTHER_PASSWORD: AAZbUGVXeU5e0ZB # base64 encoded

Source: GitHub Gist from troyharvey

Migration Job Settings

The migration job supports both ArgoCD and Helm hooks to ensure database migrations run at the appropriate time during deployments.

Name Description Value
migrationJob.enabled Enable or disable the schema migration Job true
migrationJob.backoffLimit Backoff limit for Job restarts 4
migrationJob.ttlSecondsAfterFinished TTL for completed migration jobs 120
migrationJob.annotations Additional annotations for the migration job pod {}
migrationJob.extraContainers Additional containers to run alongside the migration job []
migrationJob.hooks.argocd.enabled Enable ArgoCD hooks for the migration job (uses PreSync hook with BeforeHookCreation delete policy) true
migrationJob.hooks.helm.enabled Enable Helm hooks for the migration job (uses pre-install,pre-upgrade hooks with before-hook-creation delete policy) false
migrationJob.hooks.helm.weight Helm hook execution order (lower weights executed first). Optional - defaults to "1" if not specified. N/A

Accessing the Admin UI

When browsing to the URL published per the settings in ingress.*, you will be prompted for Admin Configuration. The Proxy Endpoint is the internal (from the litellm pod's perspective) URL published by the <RELEASE>-litellm Kubernetes Service. If the deployment uses the default settings for this service, the Proxy Endpoint should be set to http://<RELEASE>-litellm:4000.

The Proxy Key is the value specified for masterkey or, if a masterkey was not provided to the helm command line, the masterkey is a randomly generated string in the sk-... format stored in the <RELEASE>-litellm-masterkey Kubernetes Secret. The key is generated once on the first install; later helm upgrade runs reuse the value already in that Secret, so upgrading never rotates the master key.

kubectl -n litellm get secret <RELEASE>-litellm-masterkey -o jsonpath="{.data.masterkey}"

Admin UI Limitations

At the time of writing, the Admin UI is unable to add models. This is because it would need to update the config.yaml file which is a exposed ConfigMap, and therefore, read-only. This is a limitation of this helm chart, not the Admin UI itself.