Skip to content

Helm

The icoretech/codex-pooler Helm chart deploys Codex Pooler into Kubernetes from the iCoreTech Helm repository. It separates web traffic, background work, scheduled work, and migration work so each role can scale and restart on its own.

Use this page as the public shape of the chart. Keep environment-specific secrets, private image repository names, internal hostnames, and operational evidence out of values files and public docs.

Terminal window
helm repo add icoretech https://icoretech.github.io/helm
helm repo update
helm upgrade --install codex-pooler icoretech/codex-pooler \
--namespace codex-pooler \
--create-namespace \
--version 0.5.3 \
--values ./values.production.yaml

The same chart is also published as an OCI artifact:

Terminal window
helm upgrade --install codex-pooler oci://ghcr.io/icoretech/charts/codex-pooler \
--namespace codex-pooler \
--create-namespace \
--version 0.5.3 \
--values ./values.production.yaml

The chart has four release roles:

  • app, serves HTTP with OBAN_MODE=web and PHX_SERVER=true
  • oban.worker, runs background jobs with OBAN_MODE=worker
  • oban.scheduler, runs scheduled jobs with OBAN_MODE=scheduler
  • migrations, runs release migrations before app rollout when enabled

All roles use the same image repository and tag. Only the app role serves HTTP. Worker, scheduler, and migration pods don’t expose the HTTP endpoint.

image:
repository: ghcr.io/icoretech/codex-pooler
tag: ""
pullPolicy: IfNotPresent

The chart defaults image.tag to its appVersion, so a pinned chart version normally selects the matching Codex Pooler image. Set image.tag only when you intentionally run a custom image or a different immutable release tag.

By default, the chart expects an existing Kubernetes Secret:

secrets:
create: false
existingSecret: codex-pooler-secrets

That secret must provide the release values needed before Codex Pooler can read database-managed settings:

  • database-url
  • secret-key-base
  • totp-encryption-key
  • totp-key-version
  • upstream-secret-key
  • upstream-secret-key-version
  • release-cookie, when app clustering is enabled and you use the default clustering cookie key

Don’t put upstream access tokens, Pool API keys, MCP tokens, cookies, auth.json, SMTP passwords, or raw client payloads into chart values.

Set the public host through chart values:

config:
host: codex-pooler.example.com
ingress:
enabled: true
hosts:
- host: codex-pooler.example.com
paths:
- path: /
pathType: Prefix

Public client examples should use https://codex-pooler.example.com as the deployed base URL.

The default topology is conservative:

app:
enabled: true
replicaCount: 1
oban:
worker:
enabled: true
replicaCount: 1
scheduler:
enabled: true
replicaCount: 1
migrations:
enabled: true

The migration hook runs database migrations and imports the vendored OpenAI pricing feed. The scheduler keeps scheduled background work separate from request-serving pods.

The app role exposes Prometheus metrics on /metrics through the same HTTP service as /healthz and /readyz. Worker, scheduler, and migration pods don’t expose the HTTP endpoint, so app-level metrics such as BEAM memory, request rate, route latency, gateway admission, stream-buffer pressure, and Ecto query pressure are scraped from app pods only. Worker and scheduler roles still run the memory sampler and VM telemetry poller, but they don’t start the Prometheus reporter because unswept histogram samples are drained only by a /metrics scrape.

If you run Prometheus Operator, enable the chart ServiceMonitor:

monitoring:
serviceMonitor:
enabled: true
labels:
release: kube-prometheus-stack
interval: 10s
scrapeTimeout: 5s
path: /metrics

Set labels to match your Prometheus Operator selector. The common release: kube-prometheus-stack label is only an example; use the label your Prometheus installation selects.

If you configure a metrics bearer token from /admin/system, create a Kubernetes Secret containing the same raw token and point the ServiceMonitor at it:

monitoring:
serviceMonitor:
enabled: true
bearerTokenSecret:
name: codex-pooler-metrics
key: token

Don’t put the raw metrics token in Helm values. The app stores only a keyed digest, so the token must be saved separately at creation time if Prometheus will authenticate with it.

Render the monitoring resource before installing:

Terminal window
helm template codex-pooler icoretech/codex-pooler \
--namespace codex-pooler \
--version 0.5.3 \
--set monitoring.serviceMonitor.enabled=true \
--set monitoring.serviceMonitor.labels.release=kube-prometheus-stack \
--set monitoring.serviceMonitor.interval=10s \
--set monitoring.serviceMonitor.scrapeTimeout=5s \
--show-only templates/servicemonitor.yaml

After installation, verify that Kubernetes rendered the ServiceMonitor and that Prometheus has active targets:

Terminal window
kubectl -n codex-pooler get servicemonitor codex-pooler-app
kubectl -n codex-pooler get endpoints codex-pooler-app

When you can reach the Prometheus HTTP API, check scrape health:

Terminal window
# If Prometheus is only reachable inside the cluster, port-forward your Prometheus service first.
kubectl -n monitoring port-forward svc/prometheus-operated 9090:9090
curl -fsS --get http://127.0.0.1:9090/api/v1/query \
--data-urlencode 'query=up{namespace="codex-pooler",job="codex-pooler-app"}'

Useful first queries:

vm_memory_total_bytes{namespace="codex-pooler", job="codex-pooler-app"}
sum by (pod) (rate(phoenix_endpoint_stop_count{namespace="codex-pooler", job="codex-pooler-app"}[5m]))
sum by (method, status_class) (rate(codex_pooler_http_request_count{namespace="codex-pooler", job="codex-pooler-app"}[5m]))
topk(10, sum by (source, command) (rate(codex_pooler_repo_query_count{namespace="codex-pooler", job="codex-pooler-app"}[5m])))

For memory incidents, also collect Kubernetes cgroup metrics such as container_memory_working_set_bytes, container_memory_rss, restart counters, and OOM counters from your cluster collectors. The app metrics explain BEAM and gateway behavior; cgroup and kube-state-metrics data explain container limits, restarts, and killed pods.

The monitoring and runtime triage guide includes a starter Grafana dashboard JSON and PromQL examples.

The chart defaults the web app to one replica. If app.replicaCount is 2 or higher, the chart treats websocket owner forwarding as required.

The current chart guard rejects multi-replica app rendering unless app clustering is enabled and app pods participate in that cluster. App participation defaults to enabled, so the minimal multi-replica values shape is:

clustering:
enabled: true
app:
replicaCount: 2

With that shape, the chart renders CODEX_POOLER_WEBSOCKET_OWNER_FORWARDING=true on app pods automatically. Render the chart and verify the topology before raising app.replicaCount.

This example shows a Flux HelmRepository and HelmRelease for two app replicas. It assumes:

  • a Kubernetes Secret named codex-pooler-secrets exists in that namespace
  • the Secret contains the required release keys plus release-cookie
  • Postgres is reachable through the Secret’s database-url
  • https://codex-pooler.example.com is the public app URL

The example leaves chart defaults out of values. In particular, it does not set image values, secrets.create, secrets.existingSecret, app probes, rollout strategy, termination grace, worker or scheduler replica counts, or explicit websocket owner-forwarding values.

apiVersion: v1
kind: Namespace
metadata:
name: codex-pooler
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
name: icoretech
namespace: flux-system
spec:
interval: 1h
url: https://icoretech.github.io/helm
---
apiVersion: helm.toolkit.fluxcd.io/v2
kind: HelmRelease
metadata:
name: codex-pooler
namespace: codex-pooler
spec:
releaseName: codex-pooler
chart:
spec:
chart: codex-pooler
version: "0.5.3"
sourceRef:
kind: HelmRepository
name: icoretech
namespace: flux-system
interval: 5m
timeout: 3m
install:
remediation:
retries: -1
upgrade:
remediation:
retries: -1
strategy: uninstall
remediateLastFailure: true
values:
config:
host: codex-pooler.example.com
ingress:
enabled: true
className: nginx
annotations:
cert-manager.io/cluster-issuer: letsencrypt
hosts:
- host: codex-pooler.example.com
paths:
- path: /
pathType: Prefix
tls:
- secretName: codex-pooler-tls
hosts:
- codex-pooler.example.com
monitoring:
serviceMonitor:
enabled: true
labels:
release: kube-prometheus-stack
interval: 10s
scrapeTimeout: 5s
clustering:
enabled: true
participants:
worker: false
scheduler: false
app:
replicaCount: 2
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app.kubernetes.io/name: codex-pooler
app.kubernetes.io/instance: codex-pooler
app.kubernetes.io/component: app
topologyKey: kubernetes.io/hostname

When app.replicaCount is 2 or higher, the chart enables websocket owner forwarding automatically. Keep clustering.participants.worker and clustering.participants.scheduler disabled unless those roles also need BEAM cluster membership in your environment.

Render manifests locally before applying them:

Terminal window
helm template codex-pooler icoretech/codex-pooler \
--version 0.5.3 \
--set config.host=codex-pooler.example.com \
--set ingress.enabled=true \
--set ingress.hosts[0].host=codex-pooler.example.com

Inspect the output for the expected app, worker, scheduler, migration, Secret reference, Service, and Ingress resources. The rendered app Deployment should serve HTTP. Worker, scheduler, and migration resources should not.

For install or upgrade, provide your production values file from a private path:

Terminal window
helm upgrade --install codex-pooler icoretech/codex-pooler \
--namespace codex-pooler \
--create-namespace \
--version 0.5.3 \
--values ./values.production.yaml

Keep values.production.yaml private if it contains secret names, internal DNS names, ingress details, or environment-specific settings.