Replace bundled Bitnami subcharts with in-chart manifests on official images

The Bitnami catalog removed all versioned image tags from docker.io/bitnami in
Aug 2025 (old images frozen in bitnamilegacy, maintained catalog now behind a
Broadcom subscription), so the bundled postgresql/redis/minio subcharts no
longer pull. Replace them with plain in-chart manifests built on official
upstream images, keeping the internal/all-in-one path fully self-contained and
free of third-party chart packaging that can disappear:

- internal-postgres.yaml: pgvector/pgvector:pg17 — upstream Postgres plus the
  `vector` extension the migrations require. POSTGRES_USER=dograh is the initdb
  superuser, so CREATE EXTENSION vector succeeds.
- internal-redis.yaml: redis:7.4-alpine, password-protected, AOF persistence.
- internal-minio.yaml: minio/minio, root creds shared with the app via a single
  secret (can't drift); the app auto-creates its bucket.

Service/secret names are unchanged (<rel>-postgresql, <rel>-redisinternal-master,
<rel>-minio) so the app wiring is untouched. Dep passwords are generated once and
persisted across upgrades via lookup. Drop the Chart.yaml dependencies,
Chart.lock, and the `helm dependency` step; the internal manifests gate on the
mode toggles (database.mode=internal, etc.).

Also fixes surfaced by smoke-testing on a live EKS cluster:
- Dockerfile: ship the per-service run_*.sh entrypoints the chart invokes.
- migrate-job: run as a post-install/pre-upgrade hook (the bundled Postgres does
  not exist during pre-install) with a wait-for-postgres init container.
- backend env: declare POSTGRES_PASSWORD/REDIS_PASSWORD before the DATABASE_URL/
  REDIS_URL that interpolate them (Kubernetes only expands back-references).
- worker liveness probes: pgrep isn't in the slim runtime image; check
  /proc/1/cmdline instead (each worker execs its process as PID 1).
- UI: set HOSTNAME=0.0.0.0 so Next.js standalone doesn't bind to the k8s-injected
  pod name (which maps to the pod IP only, breaking port-forward/loopback).

Verified end-to-end on EKS 1.36: all pods Ready, migrations applied (pgvector
extension + 27 tables), UI login page and web API served via port-forward.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Abhishek Kumar 2026-07-02 22:10:15 +05:30
parent 89d1e5ee89
commit 00e290671f
20 changed files with 614 additions and 190 deletions

View file

@ -1,12 +0,0 @@
dependencies:
- name: postgresql
repository: oci://registry-1.docker.io/bitnamicharts
version: 16.7.27
- name: redis
repository: oci://registry-1.docker.io/bitnamicharts
version: 20.13.4
- name: minio
repository: oci://registry-1.docker.io/bitnamicharts
version: 16.0.10
digest: sha256:8a2d769b21e882239fc847a9638758131f388dbd12feabd387671e9a336a2615
generated: "2026-05-25T19:49:15.193741+05:30"

View file

@ -27,21 +27,11 @@ sources:
maintainers:
- name: Dograh
# Bundled stateful dependencies. Each is gated by its mode toggle in
# values.yaml so external-managed deployments do not pull these charts.
dependencies:
- name: postgresql
version: "16.x.x"
repository: oci://registry-1.docker.io/bitnamicharts
condition: postgresql.enabled
# Aliased to redisInternal so the dograh-level `redis.mode` toggle does not
# collide with the subchart's values namespace.
- name: redis
alias: redisInternal
version: "20.x.x"
repository: oci://registry-1.docker.io/bitnamicharts
condition: redisInternal.enabled
- name: minio
version: "16.x.x"
repository: oci://registry-1.docker.io/bitnamicharts
condition: minio.enabled
# The bundled stateful deps (PostgreSQL, Redis, MinIO) for the internal/all-in-one
# modes are plain in-chart manifests (templates/internal-*.yaml) built on official
# upstream images — NOT subcharts. This avoids depending on third-party chart
# packaging/registries that can change or disappear (as the Bitnami catalog did in
# Aug 2025). Production deployments should use the external/managed modes
# (database.mode=external, redis.mode=external, storage.mode=s3|externalMinio).
#
# dependencies: []

View file

@ -15,10 +15,9 @@ exercised against a live cluster.
```bash
cd deploy/helm/dograh
# Pull in Bitnami subcharts (network required)
helm dependency update
# Install with defaults (all internal deps, Gateway API exposure)
# Install with defaults (all internal deps, Gateway API exposure).
# The bundled Postgres/Redis/MinIO are in-chart manifests on official upstream
# images — no `helm dependency` / subchart pull step needed.
helm install dograh . \
--set secrets.ossJwtSecret="$(openssl rand -hex 32)" \
--set secrets.turnSecret="$(openssl rand -hex 32)" \

View file

@ -62,10 +62,5 @@ autoscaling:
minReplicas: 3
maxReplicas: 12
# Disable bundled deps.
postgresql:
enabled: false
redisInternal:
enabled: false
minio:
enabled: false
# No bundled deps: the in-chart Postgres/Redis/MinIO manifests are gated on the
# internal modes, so the external/S3 modes above already keep them from rendering.

View file

@ -62,10 +62,7 @@ autoscaling:
maxReplicas: 12
targetCPUUtilizationPercentage: 70
# Disable bundled deps when using external services.
postgresql:
enabled: false
redisInternal:
enabled: false
minio:
enabled: false
# No bundled deps here: the in-chart Postgres/Redis/MinIO manifests are gated on
# the internal modes (database.mode=internal, redis.mode=internal,
# storage.mode=internalMinio), so the external/S3 modes set above already keep
# them from rendering.

View file

@ -49,13 +49,11 @@ autoscaling:
enabled: false
postgresql:
primary:
persistence:
size: 2Gi
redisInternal:
master:
persistence:
size: 1Gi
persistence:
size: 2Gi
redisinternal:
persistence:
size: 1Gi
minio:
persistence:
size: 5Gi

View file

@ -52,7 +52,7 @@ coturn is disabled. Set coturn.enabled=true to deploy media relay.
=== Migrations ===
{{- if .Values.migrate.enabled }}
Alembic migrations run as a pre-install / pre-upgrade hook. Inspect with:
Alembic migrations run as a post-install / pre-upgrade hook. Inspect with:
kubectl logs job/{{ include "dograh.migrate.fullname" . }} -n {{ .Release.Namespace }}
{{- end }}

View file

@ -98,12 +98,60 @@ In-cluster service references for internal deps.
{{- define "dograh.redisHost" -}}{{ .Release.Name }}-redisinternal-master{{- end }}
{{- define "dograh.minioHost" -}}{{ .Release.Name }}-minio{{- end }}
{{/*
Resolved passwords for the bundled internal deps.
Precedence: explicit value in values.yaml wins; else reuse the value already
stored in the Secret (so `helm upgrade` does NOT rotate the password and desync a
running datastore); else generate a fresh one. `lookup` returns empty during
`helm template` (no cluster), so dry renders get a throwaway random value — fine.
Each is materialized in exactly one place (the dep's Secret); every other
reference is a secretKeyRef, so the generated value is stable within a render.
*/}}
{{- define "dograh.postgresPassword" -}}
{{- if .Values.postgresql.auth.password -}}
{{- .Values.postgresql.auth.password -}}
{{- else -}}
{{- $s := lookup "v1" "Secret" .Release.Namespace (printf "%s-postgresql" .Release.Name) -}}
{{- if and $s $s.data (index $s.data "password") -}}
{{- index $s.data "password" | b64dec -}}
{{- else -}}
{{- randAlphaNum 24 -}}
{{- end -}}
{{- end -}}
{{- end }}
{{- define "dograh.redisPassword" -}}
{{- if .Values.redisinternal.auth.password -}}
{{- .Values.redisinternal.auth.password -}}
{{- else -}}
{{- $s := lookup "v1" "Secret" .Release.Namespace (printf "%s-redisinternal" .Release.Name) -}}
{{- if and $s $s.data (index $s.data "redis-password") -}}
{{- index $s.data "redis-password" | b64dec -}}
{{- else -}}
{{- randAlphaNum 24 -}}
{{- end -}}
{{- end -}}
{{- end }}
{{- define "dograh.minioRootPassword" -}}
{{- if .Values.minio.auth.rootPassword -}}
{{- .Values.minio.auth.rootPassword -}}
{{- else -}}
{{- $s := lookup "v1" "Secret" .Release.Namespace (printf "%s-minio" .Release.Name) -}}
{{- if and $s $s.data (index $s.data "root-password") -}}
{{- index $s.data "root-password" | b64dec -}}
{{- else -}}
{{- randAlphaNum 24 -}}
{{- end -}}
{{- end -}}
{{- end }}
{{/*
Default DATABASE_URL when database.mode=internal.
Bitnami Postgres exposes the password as <release>-postgresql secret key
`postgres-password` or `password`. The chart pulls it via envFrom on the
generated secret; for clarity we still need the URL string. Auth username
defaults to `dograh` (see values.postgresql.auth.username).
The bundled Postgres (templates/internal-postgres.yaml) stores the app-user
password in the <release>-postgresql Secret under key `password`; dograh.dbEnv
projects it into $(POSTGRES_PASSWORD), which this URL interpolates at runtime.
Auth username/database default to `dograh` (see values.postgresql.auth).
*/}}
{{- define "dograh.databaseUrl" -}}
{{- if eq .Values.database.mode "internal" -}}
@ -121,6 +169,53 @@ $(REDIS_URL)
{{- end -}}
{{- end }}
{{/*
Database / Redis connection env for backend workloads (web, arq, singletons,
migrate).
ORDER IS LOAD-BEARING. POSTGRES_PASSWORD / REDIS_PASSWORD are declared BEFORE
DATABASE_URL / REDIS_URL because Kubernetes only expands a $(VAR) reference to
an env var defined *earlier* in the same container's env list — a forward
reference is left as the literal string "$(VAR)". DATABASE_URL / REDIS_URL
embed $(POSTGRES_PASSWORD) / $(REDIS_PASSWORD) (see dograh.databaseUrl /
dograh.redisUrl), so the password vars must come first or the composed URLs
ship with a literal "$(POSTGRES_PASSWORD)" as the password.
*/}}
{{- define "dograh.dbEnv" -}}
{{- if eq .Values.database.mode "internal" }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgresql
key: password
{{- end }}
{{- if eq .Values.redis.mode "internal" }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-redisinternal
key: redis-password
{{- end }}
- name: DATABASE_URL
value: {{ include "dograh.databaseUrl" . | quote }}
- name: REDIS_URL
value: {{ include "dograh.redisUrl" . | quote }}
{{- if eq .Values.storage.mode "internalMinio" }}
{{- /* Internal MinIO creds come from the <release>-minio secret, the same
source the MinIO server uses (no ordering constraint — no composition). */}}
- name: MINIO_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-user
- name: MINIO_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-password
{{- end }}
{{- end }}
{{/*
Common env block for backend workloads (web, arq, singletons, migrate).
References the ConfigMap + Secret via envFrom. DATABASE_URL and REDIS_URL
@ -131,16 +226,6 @@ are added inline because they may need composition from subchart secrets.
name: {{ include "dograh.configMapName" . }}
- secretRef:
name: {{ include "dograh.secretName" . }}
{{- if eq .Values.database.mode "internal" }}
- secretRef:
name: {{ .Release.Name }}-postgresql
optional: true
{{- end }}
{{- if eq .Values.redis.mode "internal" }}
- secretRef:
name: {{ .Release.Name }}-redisinternal
optional: true
{{- end }}
{{- end }}
{{/*

View file

@ -43,24 +43,7 @@ spec:
envFrom:
{{- include "dograh.backendEnvFrom" . | nindent 12 }}
env:
- name: DATABASE_URL
value: {{ include "dograh.databaseUrl" . | quote }}
- name: REDIS_URL
value: {{ include "dograh.redisUrl" . | quote }}
{{- if eq .Values.database.mode "internal" }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgresql
key: password
{{- end }}
{{- if eq .Values.redis.mode "internal" }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-redisinternal
key: redis-password
{{- end }}
{{- include "dograh.dbEnv" . | nindent 12 }}
# exec probe — no HTTP endpoint exists on ari-manager.
livenessProbe:
{{- toYaml .Values.ariManager.livenessProbe | nindent 12 }}

View file

@ -40,24 +40,7 @@ spec:
envFrom:
{{- include "dograh.backendEnvFrom" . | nindent 12 }}
env:
- name: DATABASE_URL
value: {{ include "dograh.databaseUrl" . | quote }}
- name: REDIS_URL
value: {{ include "dograh.redisUrl" . | quote }}
{{- if eq .Values.database.mode "internal" }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgresql
key: password
{{- end }}
{{- if eq .Values.redis.mode "internal" }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-redisinternal
key: redis-password
{{- end }}
{{- include "dograh.dbEnv" . | nindent 12 }}
livenessProbe:
{{- toYaml .Values.workers.livenessProbe | nindent 12 }}
resources:

View file

@ -43,24 +43,7 @@ spec:
envFrom:
{{- include "dograh.backendEnvFrom" . | nindent 12 }}
env:
- name: DATABASE_URL
value: {{ include "dograh.databaseUrl" . | quote }}
- name: REDIS_URL
value: {{ include "dograh.redisUrl" . | quote }}
{{- if eq .Values.database.mode "internal" }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgresql
key: password
{{- end }}
{{- if eq .Values.redis.mode "internal" }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-redisinternal
key: redis-password
{{- end }}
{{- include "dograh.dbEnv" . | nindent 12 }}
# exec probe — no HTTP endpoint exists on campaign-orchestrator.
livenessProbe:
{{- toYaml .Values.campaignOrchestrator.livenessProbe | nindent 12 }}

View file

@ -0,0 +1,137 @@
{{- if eq .Values.storage.mode "internalMinio" }}
{{- /*
Bundled MinIO for storage.mode=internalMinio, official upstream image. Single
replica (Recreate strategy — the RWO volume can't be shared). The app creates
its bucket on first use (bucket_exists → make_bucket), so no provisioning Job.
The root credentials live in <release>-minio and are consumed BOTH by the MinIO
server (MINIO_ROOT_USER / MINIO_ROOT_PASSWORD) and by the app (MINIO_ACCESS_KEY /
MINIO_SECRET_KEY, wired in dograh.dbEnv) — one source of truth, so they can't
drift. For production use storage.mode=s3 or externalMinio.
*/}}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-minio
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: minio
type: Opaque
stringData:
root-user: {{ .Values.minio.auth.rootUser | default "minioadmin" | quote }}
root-password: {{ include "dograh.minioRootPassword" . | quote }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-minio
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: minio
spec:
type: ClusterIP
ports:
- name: api
port: 9000
targetPort: api
- name: console
port: 9001
targetPort: console
selector:
{{- include "dograh.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: minio
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: {{ .Release.Name }}-minio
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: minio
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.minio.persistence.storageClass }}
storageClassName: {{ .Values.minio.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.minio.persistence.size | quote }}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: {{ .Release.Name }}-minio
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: minio
spec:
replicas: 1
strategy:
type: Recreate
selector:
matchLabels:
{{- include "dograh.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: minio
template:
metadata:
labels:
{{- include "dograh.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: minio
spec:
securityContext:
fsGroup: 1000
containers:
- name: minio
image: {{ printf "%s/%s:%s" (.Values.minio.image.registry | default "docker.io") .Values.minio.image.repository (.Values.minio.image.tag | toString) | quote }}
imagePullPolicy: {{ .Values.minio.image.pullPolicy | default "IfNotPresent" }}
args:
- server
- /data
- --console-address
- ":9001"
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-user
- name: MINIO_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-minio
key: root-password
ports:
- name: api
containerPort: 9000
- name: console
containerPort: 9001
readinessProbe:
httpGet:
path: /minio/health/ready
port: api
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
httpGet:
path: /minio/health/live
port: api
initialDelaySeconds: 20
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 6
resources:
{{- toYaml .Values.minio.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: {{ .Release.Name }}-minio
{{- end }}

View file

@ -0,0 +1,118 @@
{{- if eq .Values.database.mode "internal" }}
{{- /*
Bundled PostgreSQL for database.mode=internal.
Image is the official pgvector build — upstream postgres plus the `vector`
extension the app's migrations require (CREATE EXTENSION vector). POSTGRES_USER
is the initdb superuser, so the migration can create the (untrusted) extension.
This is the convenience/all-in-one path. For production, set database.mode=external
and point secrets.databaseUrl at a managed Postgres.
*/}}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-postgresql
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: postgresql
type: Opaque
stringData:
password: {{ include "dograh.postgresPassword" . | quote }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-postgresql
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: postgresql
spec:
type: ClusterIP
ports:
- name: postgresql
port: 5432
targetPort: postgresql
selector:
{{- include "dograh.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: postgresql
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-postgresql
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: postgresql
spec:
serviceName: {{ .Release.Name }}-postgresql
replicas: 1
selector:
matchLabels:
{{- include "dograh.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: postgresql
template:
metadata:
labels:
{{- include "dograh.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: postgresql
spec:
securityContext:
# Official image starts as root, chowns PGDATA, then drops to the
# `postgres` uid (999). fsGroup makes the mounted volume group-writable.
fsGroup: 999
containers:
- name: postgresql
image: {{ printf "%s/%s:%s" (.Values.postgresql.image.registry | default "docker.io") .Values.postgresql.image.repository (.Values.postgresql.image.tag | toString) | quote }}
imagePullPolicy: {{ .Values.postgresql.image.pullPolicy | default "IfNotPresent" }}
ports:
- name: postgresql
containerPort: 5432
env:
- name: POSTGRES_USER
value: {{ .Values.postgresql.auth.username | quote }}
- name: POSTGRES_DB
value: {{ .Values.postgresql.auth.database | quote }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgresql
key: password
# Init into a subdirectory so a mounted volume's lost+found does not
# collide with initdb's empty-dir check.
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
readinessProbe:
exec:
command: ["pg_isready", "-U", {{ .Values.postgresql.auth.username | quote }}, "-d", {{ .Values.postgresql.auth.database | quote }}]
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
exec:
command: ["pg_isready", "-U", {{ .Values.postgresql.auth.username | quote }}, "-d", {{ .Values.postgresql.auth.database | quote }}]
initialDelaySeconds: 30
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 6
resources:
{{- toYaml .Values.postgresql.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.postgresql.persistence.storageClass }}
storageClassName: {{ .Values.postgresql.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.postgresql.persistence.size | quote }}
{{- end }}

View file

@ -0,0 +1,115 @@
{{- if eq .Values.redis.mode "internal" }}
{{- /*
Bundled Redis for redis.mode=internal, official upstream image. Single-node,
password-protected, AOF persistence. For production set redis.mode=external and
point secrets.redisUrl at a managed Redis/Valkey.
Service name is <release>-redisinternal-master (kept from the previous packaging)
so dograh.redisHost / dograh.redisUrl need no change.
*/}}
apiVersion: v1
kind: Secret
metadata:
name: {{ .Release.Name }}-redisinternal
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: redis
type: Opaque
stringData:
redis-password: {{ include "dograh.redisPassword" . | quote }}
---
apiVersion: v1
kind: Service
metadata:
name: {{ .Release.Name }}-redisinternal-master
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: redis
spec:
type: ClusterIP
ports:
- name: redis
port: 6379
targetPort: redis
selector:
{{- include "dograh.selectorLabels" . | nindent 4 }}
app.kubernetes.io/component: redis
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: {{ .Release.Name }}-redisinternal-master
namespace: {{ .Release.Namespace }}
labels:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: redis
spec:
serviceName: {{ .Release.Name }}-redisinternal-master
replicas: 1
selector:
matchLabels:
{{- include "dograh.selectorLabels" . | nindent 6 }}
app.kubernetes.io/component: redis
template:
metadata:
labels:
{{- include "dograh.selectorLabels" . | nindent 8 }}
app.kubernetes.io/component: redis
spec:
securityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
containers:
- name: redis
image: {{ printf "%s/%s:%s" (.Values.redisinternal.image.registry | default "docker.io") .Values.redisinternal.image.repository (.Values.redisinternal.image.tag | toString) | quote }}
imagePullPolicy: {{ .Values.redisinternal.image.pullPolicy | default "IfNotPresent" }}
command: ["redis-server"]
# $(REDIS_PASSWORD) is expanded by Kubernetes from the env var below.
args:
- "--requirepass"
- "$(REDIS_PASSWORD)"
- "--appendonly"
- "yes"
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-redisinternal
key: redis-password
ports:
- name: redis
containerPort: 6379
readinessProbe:
exec:
command: ["sh", "-c", 'redis-cli -a "$REDIS_PASSWORD" ping | grep -q PONG']
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 6
livenessProbe:
exec:
command: ["sh", "-c", 'redis-cli -a "$REDIS_PASSWORD" ping | grep -q PONG']
initialDelaySeconds: 20
periodSeconds: 15
timeoutSeconds: 5
failureThreshold: 6
resources:
{{- toYaml .Values.redisinternal.resources | nindent 12 }}
volumeMounts:
- name: data
mountPath: /data
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
{{- if .Values.redisinternal.persistence.storageClass }}
storageClassName: {{ .Values.redisinternal.persistence.storageClass | quote }}
{{- end }}
resources:
requests:
storage: {{ .Values.redisinternal.persistence.size | quote }}
{{- end }}

View file

@ -8,7 +8,14 @@ metadata:
{{- include "dograh.labels" . | nindent 4 }}
app.kubernetes.io/component: migrate
annotations:
"helm.sh/hook": pre-install,pre-upgrade
# post-install (not pre-install): when the bundled Postgres subchart is in
# use it is a normal resource, so on a fresh install it does not exist until
# after pre-install hooks finish — a pre-install migration would deadlock
# waiting for a database Helm has not created yet. post-install runs after
# all resources (incl. Postgres) are applied. pre-upgrade is kept so
# migrations land before new app code rolls out on upgrades (where the DB
# already exists).
"helm.sh/hook": post-install,pre-upgrade
"helm.sh/hook-weight": "-5"
"helm.sh/hook-delete-policy": before-hook-creation,hook-succeeded
spec:
@ -27,6 +34,26 @@ spec:
imagePullSecrets:
{{- toYaml . | nindent 8 }}
{{- end }}
{{- if eq .Values.database.mode "internal" }}
# post-install fires right after resources are applied — the bundled
# Postgres pod may still be starting. Block until its Service routes to a
# ready endpoint (endpoints only publish once the pod passes readiness)
# before alembic attempts to connect. Reuses the app image (already
# pulled for the migrate container) so no extra image is needed.
initContainers:
- name: wait-for-postgres
image: {{ include "dograh.image" . }}
imagePullPolicy: {{ .Values.image.pullPolicy }}
command:
- bash
- -c
- |
until (exec 3<>/dev/tcp/{{ include "dograh.postgresHost" . }}/5432) 2>/dev/null; do
echo "waiting for postgres at {{ include "dograh.postgresHost" . }}:5432 ..."
sleep 2
done
echo "postgres is reachable"
{{- end }}
containers:
- name: migrate
image: {{ include "dograh.image" . }}
@ -35,15 +62,7 @@ spec:
envFrom:
{{- include "dograh.backendEnvFrom" . | nindent 12 }}
env:
- name: DATABASE_URL
value: {{ include "dograh.databaseUrl" . | quote }}
{{- if eq .Values.database.mode "internal" }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgresql
key: password
{{- end }}
{{- include "dograh.dbEnv" . | nindent 12 }}
resources:
{{- toYaml .Values.migrate.resources | nindent 12 }}
{{- end }}

View file

@ -16,7 +16,9 @@ stringData:
{{- if eq .Values.redis.mode "external" }}
REDIS_URL: {{ required "secrets.redisUrl is required when redis.mode=external" .Values.secrets.redisUrl | quote }}
{{- end }}
{{- if or (eq .Values.storage.mode "internalMinio") (eq .Values.storage.mode "externalMinio") }}
{{- /* internalMinio creds are sourced from the <release>-minio secret via
dograh.dbEnv; only externalMinio pulls creds from here. */}}
{{- if eq .Values.storage.mode "externalMinio" }}
MINIO_ACCESS_KEY: {{ .Values.secrets.minioAccessKey | quote }}
MINIO_SECRET_KEY: {{ .Values.secrets.minioSecretKey | quote }}
{{- end }}

View file

@ -42,6 +42,13 @@ spec:
containerPort: {{ .Values.ui.port }}
protocol: TCP
env:
# Next.js standalone binds to $HOSTNAME (falls back to 0.0.0.0).
# Kubernetes injects HOSTNAME=<pod-name>, which /etc/hosts maps to the
# pod IP — so without this, Next listens on the pod IP ONLY. In-cluster
# Service traffic still works, but loopback (kubectl port-forward, local
# health checks) is refused. Pin to 0.0.0.0 to listen on all interfaces.
- name: HOSTNAME
value: "0.0.0.0"
- name: NODE_ENV
value: "oss"
- name: BACKEND_URL

View file

@ -52,24 +52,7 @@ spec:
env:
- name: WEB_PORT
value: {{ .Values.web.port | quote }}
- name: DATABASE_URL
value: {{ include "dograh.databaseUrl" . | quote }}
- name: REDIS_URL
value: {{ include "dograh.redisUrl" . | quote }}
{{- if eq .Values.database.mode "internal" }}
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-postgresql
key: password
{{- end }}
{{- if eq .Values.redis.mode "internal" }}
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: {{ .Release.Name }}-redisinternal
key: redis-password
{{- end }}
{{- include "dograh.dbEnv" . | nindent 12 }}
# Distinct probes: readiness flips fast (drain), liveness is
# slower (process aliveness).
livenessProbe:

View file

@ -32,13 +32,13 @@ serviceAccount:
# Stateful dependency modes.
#
# database.mode:
# internal — bundled Bitnami PostgreSQL subchart (postgresql.enabled=true)
# internal — bundled Postgres manifest (pgvector image; see `postgresql` below)
# external — operator supplies DATABASE_URL via secrets.databaseUrl
# redis.mode:
# internal — bundled Bitnami Redis subchart (redis.enabled=true)
# internal — bundled Redis manifest (see `redisinternal` below)
# external — operator supplies REDIS_URL via secrets.redisUrl
# storage.mode:
# internalMinio — bundled MinIO subchart (minio.enabled=true)
# internalMinio — bundled MinIO manifest (see `minio` below)
# externalMinio — operator supplies a MinIO-compatible endpoint + creds
# s3 — sets ENABLE_AWS_S3=true; uses AWS S3
# exposure.mode:
@ -253,7 +253,10 @@ workers:
# exec probe — workers have no HTTP endpoint.
livenessProbe:
exec:
command: ["sh", "-c", "pgrep -f 'arq api.tasks.arq.WorkerSettings' > /dev/null"]
# The entrypoint exec's the worker, so it runs as PID 1; grep its cmdline.
# Avoids pgrep/procps, which isn't in the slim runtime image. Matching a
# single argv token (no spaces) — argv is NUL-separated in /proc.
command: ["sh", "-c", "grep -qa api.tasks.arq.WorkerSettings /proc/1/cmdline"]
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
@ -285,7 +288,8 @@ ariManager:
livenessProbe:
exec:
command: ["sh", "-c", "pgrep -f api.services.telephony.ari_manager > /dev/null"]
# PID 1 cmdline check (procps-free; see workers.livenessProbe).
command: ["sh", "-c", "grep -qa api.services.telephony.ari_manager /proc/1/cmdline"]
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
@ -316,7 +320,8 @@ campaignOrchestrator:
livenessProbe:
exec:
command: ["sh", "-c", "pgrep -f api.services.campaign.campaign_orchestrator > /dev/null"]
# PID 1 cmdline check (procps-free; see workers.livenessProbe).
command: ["sh", "-c", "grep -qa api.services.campaign.campaign_orchestrator /proc/1/cmdline"]
initialDelaySeconds: 30
periodSeconds: 30
timeoutSeconds: 5
@ -452,7 +457,8 @@ coturn:
# Migration Job
# -----------------------------------------------------------------------------
migrate:
# Run alembic upgrade head as a pre-install / pre-upgrade Helm hook.
# Run alembic upgrade head as a Helm hook: post-install (so the bundled
# Postgres exists first) and pre-upgrade (so migrations land before new code).
enabled: true
# Hard cap on how long a migration may run. A failed/exceeded migration
@ -484,44 +490,65 @@ autoscaling:
targetMemoryUtilizationPercentage: 80
# -----------------------------------------------------------------------------
# Bitnami subcharts. Each is gated by its own `enabled` flag set by mode
# resolution in templates/_helpers.tpl. Override the subchart's own values
# under these keys.
# Bundled stateful deps for the internal/all-in-one modes. These are plain
# in-chart manifests (templates/internal-*.yaml) on official upstream images —
# not subcharts. Each renders only in its internal mode:
# postgresql -> database.mode == internal
# redisinternal-> redis.mode == internal
# minio -> storage.mode == internalMinio
# For production, use the external/managed modes instead (see database/redis/
# storage above).
# -----------------------------------------------------------------------------
postgresql:
# enabled is set automatically based on database.mode in _helpers.tpl;
# this key is read by the subchart's `condition`.
enabled: true
# Official pgvector image = upstream Postgres + the `vector` extension the app
# migrations require. Keep the tag on a supported Postgres major.
image:
registry: docker.io
repository: pgvector/pgvector
tag: pg17
pullPolicy: IfNotPresent
auth:
username: dograh
password: "" # auto-generated if empty
username: dograh # also the initdb superuser (needed for CREATE EXTENSION)
password: "" # auto-generated and persisted across upgrades if empty
database: dograh
primary:
persistence:
enabled: true
size: 8Gi
persistence:
size: 8Gi
storageClass: "" # "" = cluster default StorageClass
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: "1", memory: 1Gi }
redisInternal:
# Bitnami Redis subchart values, aliased to redisInternal to avoid
# colliding with `redis.mode` above. `redisInternal.enabled` is the gating
# flag for whether the subchart deploys.
enabled: true
redisinternal:
# Official upstream Redis. Single node, password-protected, AOF persistence.
# (Key kept as `redisinternal` so it doesn't collide with `redis.mode` above.)
image:
registry: docker.io
repository: redis
tag: 7.4-alpine
pullPolicy: IfNotPresent
auth:
enabled: true
password: "" # auto-generated if empty
master:
persistence:
enabled: true
size: 8Gi
replica:
replicaCount: 0 # standalone primary by default
password: "" # auto-generated and persisted across upgrades if empty
persistence:
size: 8Gi
storageClass: ""
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { cpu: 500m, memory: 256Mi }
minio:
enabled: true
# Official upstream MinIO. Root creds are shared with the app (single source of
# truth) so they can't drift; the app creates its bucket on first use.
image:
registry: docker.io
repository: minio/minio
tag: RELEASE.2025-04-22T22-12-26Z
pullPolicy: IfNotPresent
auth:
rootUser: minioadmin
rootPassword: "" # auto-generated if empty
defaultBuckets: "voice-audio"
rootPassword: "" # auto-generated and persisted across upgrades if empty
persistence:
enabled: true
size: 20Gi
storageClass: ""
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { cpu: "1", memory: 1Gi }