Self-hosting
Helm chart reference
A field-by-field reference for the official golden-reports Helm chart - components, routing, dependencies, security, and operations.
The golden-reports Helm chart is the deployment artifact for self-hosted installs. This page walks through every block in values.yaml and what it controls.
For the higher-level "what you're deploying", read Deployment overview first. This page is a reference - use it when you're writing your own values overrides.
Chart shape
- Type: application chart.
- Name:
golden-reports. - No subchart dependencies. Postgres comes from CNPG (which you install separately as the operator); Redis is templated directly.
- Naming: every resource flows through
golden-reports.fullnameandgolden-reports.labelsin_helpers.tpl. Selector labels are intentionally a strict subset of full labels (the Deployment selector is immutable, so cosmetic labels can't go inselectorLabels).
Top-level structure
global: {...} # apply across every workload
backendVersion: "" # api, query-engine, data-provider, migrations share this
frontendVersion: "" # manager versions independently
commonDefaults: {...} # security context, resources, scheduling defaults
components: # the four application workloads
api: {...}
queryEngine: {...}
dataProvider: {...}
manager: {...}
migrations: {...} # EF migrations Job
postgresql: {...} # bundled CNPG mode
externalDatabase: {...} # used when postgresql.enabled=false
redis: {...} # bundled Redis Deployment
externalRedis: {...} # used when redis.enabled=false
serviceAccount: {...}
networkPolicy: {...}
podAnnotations: {}
podLabels: {}
The chart never mixes "render this and that" with "render this OR that". For example, postgresql.enabled and externalDatabase are mutually exclusive - set the toggle and one block governs.
The global block
Applied to every Pod the chart creates.
global:
imageRegistry: "" # mirror images through a private registry
imagePullSecrets: [] # pull secrets attached to every Pod
storageClass: "" # propagates to CNPG and Redis PVC when blank
When imageRegistry is set, image references resolve as <imageRegistry>/<repository>:<tag>. This is the air-gapped deployment switch.
Version trains
backendVersion: "" # defaults to .Chart.AppVersion
frontendVersion: "" # REQUIRED - must be set on install/upgrade
The four backend workloads (api, queryEngine, dataProvider, plus the migrations Job) read this train when their per-component image.tag is empty. Override per-component when you want a one-off pin.
Why split: UI cycles are faster than backend cycles. Decoupling lets you ship a CSS fix to the manager without redeploying the API.
The commonDefaults block
Defaults applied to every component's Pod spec. Override per-component as needed.
commonDefaults:
imagePullPolicy: IfNotPresent
podSecurityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
resources: {}
nodeSelector: {}
tolerations: []
affinity: {}
podAnnotations: {}
podLabels: {}
These are restrictive-by-default - non-root, all capabilities dropped, read-only root filesystem. If a component genuinely needs to write to disk it must override these.
The components.* blocks
The four application workloads share a similar shape:
components:
api:
enabled: true
replicaCount: 2
image:
repository: wolfware/golden-reports-api
tag: "" # defaults to backendVersion
service:
type: ClusterIP
port: 80
ingress:
enabled: false
className: ""
annotations: {}
hosts:
- host: api.golden-reports.local
paths:
- path: /
pathType: Prefix
tls: []
certManager:
enabled: false
clusterIssuer: "" # exactly one of these
issuer: ""
secretName: "" # auto-derived when blank
extraAnnotations: {}
httpRoute:
enabled: false
annotations: {}
parentRefs:
- name: gateway
sectionName: http
hostnames: []
rules: [...]
cors:
allowedOrigins: [] # auto-derived from manager.apiUrl when blank
podDisruptionBudget:
enabled: false
maxUnavailable: 1 # XOR with minAvailable
minAvailable: ""
autoscaling:
enabled: false
minReplicas: 2
maxReplicas: 10
targetCPUUtilizationPercentage: 80
targetMemoryUtilizationPercentage: 0 # 0 = disabled
livenessProbe:
httpGet: { path: /health/live, port: http }
readinessProbe:
httpGet: { path: /health/ready, port: http }
env: []
envFrom: []
Per-component differences
apihas the full block above, pluscors.allowedOrigins(auto-derived frommanager.apiUrlwhen blank).queryEngineanddataProviderare internal - they don't expose Ingress or HTTPRoute. They have the sameservice,podDisruptionBudget,autoscaling, and probe blocks.managerhas the same Ingress/HTTPRoute shape as the API, plusapiUrl- empty for relative routing (browser hits/graphqlon the same host) or an absolute URL for cross-origin.
Routing: pick Ingress or HTTPRoute
The two routing primitives are gated independently and must not both be enabled. The chart's NOTES.txt URL hint prioritizes HTTPRoute over Ingress. If both are enabled at once you'll get two parallel routes to the same Service. Pick one.
PodDisruptionBudgets
A PDB only makes sense when the workload has at least 2 replicas. With a single replica, an enabled PDB block is treated as a no-op.
Choose exactly one of maxUnavailable and minAvailable. The default maxUnavailable: 1 is safe at any replica count ≥ 2.
Autoscaling
When autoscaling.enabled: true, the Deployment's replicas field is omitted so the HPA owns the count. targetMemoryUtilizationPercentage: 0 disables the memory metric - keep it disabled unless you have a real memory-bound workload.
CORS (api only)
api.cors.allowedOrigins:
- Empty AND
manager.apiUrlis empty → CORS not configured (relative routing). - Empty AND
manager.apiUrlis set → chart auto-derives frommanager.apiUrl. - Non-empty → exact override.
The migrations block
Database schema migrations run as a Job before the API rolls.
migrations:
enabled: true
image:
repository: "" # reuses components.api.image.repository
tag: "" # reuses components.api.image.tag → backendVersion
command: []
args: ["--migrate"]
backoffLimit: 1
ttlSecondsAfterFinished: 600
activeDeadlineSeconds: 600
resources: {}
waitImage:
repository: postgres
tag: "" # chart default; override to pin
waitImage is a small psql client image used by init containers that pg_isready-loop and poll the migrations-history table before the API starts.
postgresql (bundled mode)
postgresql:
enabled: true
cnpg:
instances: 1 # 3 for HA
storage:
size: 20Gi
storageClass: "" # inherits global.storageClass
database: goldenreports
owner: goldenreports
existingSecret: "" # CNPG generates one when blank
Bundled mode renders a CloudNativePG Cluster resource. The CNPG operator must already be installed in the cluster. For HA, set instances: 3; CNPG handles failover.
externalDatabase (when bundled is off)
externalDatabase:
host: ""
port: 5432
database: goldenreports
username: goldenreports
existingSecret: "" # Secret with key for password
existingSecretKey: password
sslmode: prefer
connectionStringExtraParams: ""
Used only when postgresql.enabled: false. Point at any managed Postgres (RDS, Cloud SQL, Aiven, internal cluster).
redis (bundled mode)
redis:
enabled: true
image:
repository: redis
tag: "" # chart default; override to pin
persistence:
enabled: true
size: 8Gi
storageClass: ""
auth:
enabled: true
existingSecret: "" # chart generates one when blank, preserves on upgrade via `lookup`
existingSecretKey: password
resources: {}
The chart's generated Redis Secret is preserved across upgrades via the lookup pattern - but GitOps workflows that render templates offline must set existingSecret so the rendered manifest doesn't generate a new random password each plan.
The externalRedis block
externalRedis:
host: ""
port: 6379
auth:
enabled: false
existingSecret: ""
existingSecretKey: password
database: 0
Used only when redis.enabled: false.
The serviceAccount block
serviceAccount:
create: true
name: ""
annotations: {}
automount: true
When create: true and name: "", the chart names the SA after the release. annotations is where you'd attach a Workload Identity / IRSA / pod-identity binding.
The networkPolicy block
networkPolicy:
enabled: false
ingressController:
namespaceSelector: {}
podSelector: {}
allowDNS: true
extraEgress: []
When enabled, the chart renders per-workload NetworkPolicies. The ingressController block identifies legitimate external traffic for the API and Manager Pods. Empty selectors match everything - tighten in production by selecting your ingress controller's namespace and pod labels.
extraEgress is the catch-all for external dependencies - if you use externalDatabase over the network, add an ipBlock egress rule here.
TLS
There are two ways to terminate TLS:
- Provide cert-manager. Set
certManager.enabled: trueplus exactly one ofclusterIssuerorissuer. The chart adds the right annotations to the Ingress/HTTPRoute and points TLS at a Secret cert-manager populates. - Bring your own. Populate
ingress.tls:directly with the secret name. The chart trusts you.
Probes
All four application components have livenessProbe and readinessProbe blocks. The defaults are:
livenessProbe:
httpGet: { path: /health/live, port: http }
readinessProbe:
httpGet: { path: /health/ready, port: http }
(Manager has path: / because it's a static-asset workload without dedicated health endpoints.)
If you change service.port, the probe target moves with it (port: http is the named container port). If your app doesn't serve a 2xx/3xx on the configured path, override the probes.
Common operations
Install or upgrade
helm upgrade --install golden-reports . -n golden-reports --create-namespace \
--set frontendVersion=<frontend-version> \
-f values.yaml
Render templates locally (debug)
helm template golden-reports . --debug -f values.yaml
Lint the chart
helm lint .
Test the deployed release
helm test golden-reports -n golden-reports
The chart's only test is templates/tests/test-connection.yaml, a Pod that does a connection probe against the API.
Next
- High-level concept overview: Deployment overview.
- Wire up monitoring: Observability.
- Understand the runtime: System overview.