Skip to content

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.fullname and golden-reports.labels in _helpers.tpl. Selector labels are intentionally a strict subset of full labels (the Deployment selector is immutable, so cosmetic labels can't go in selectorLabels).

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

  • api has the full block above, plus cors.allowedOrigins (auto-derived from manager.apiUrl when blank).
  • queryEngine and dataProvider are internal - they don't expose Ingress or HTTPRoute. They have the same service, podDisruptionBudget, autoscaling, and probe blocks.
  • manager has the same Ingress/HTTPRoute shape as the API, plus apiUrl - empty for relative routing (browser hits /graphql on 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.apiUrl is empty → CORS not configured (relative routing).
  • Empty AND manager.apiUrl is set → chart auto-derives from manager.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:

  1. Provide cert-manager. Set certManager.enabled: true plus exactly one of clusterIssuer or issuer. The chart adds the right annotations to the Ingress/HTTPRoute and points TLS at a Secret cert-manager populates.
  2. 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

Previous
Deployment overview