Writing a Helm Values Overlay for a GeoNode Staging Environment

This guide walks through building a layered values.staging.yaml overlay that adapts a shared GeoNode Helm chart to a staging cluster without forking the chart or leaking secrets into Git.

It is the hands-on companion to Helm vs Kustomize for GeoNode Deployments and sits within the wider Infrastructure Orchestration & Configuration Management practice; read the parent guide first if you need the reasoning behind choosing Helm’s templating model in the first place. Here the scope is narrow: how to split a base values file from a per-environment overlay, layer them at upgrade time, inject secrets by reference, and prove the rendered manifests are correct before anything reaches the Kubernetes cluster.

Prerequisites

Each item below is a common cause of a broken first render or a leaked credential.

  • Helm 3.13+ installed, with the helm-diff plugin (helm plugin install https://github.com/databus23/helm-diff).
  • kubectl access to the staging namespace — examples use -n geoportal-staging.
  • A GeoNode chart available locally at ./chart or from a pinned repository, exposing geonode, postgis, celery, and geoserver value keys.
  • kubeconform 0.6+ on the pipeline runner for offline manifest schema validation.
  • A pre-provisioned Secret named geonode-db-credentials in the target namespace (created by External Secrets, Sealed Secrets, or a sealed manifest — never committed plaintext).
  • Write access to a Git repository where values.yaml and values.staging.yaml live under version control; secret values must not.

The layering below is the whole idea: a base values file carries defaults, the staging overlay carries only what differs, Helm merges them right-to-left, and secrets enter by reference rather than by value.

How a base values file and a staging overlay merge into rendered GeoNode manifests Two input files sit at the top: base values.yaml holding defaults for replicas, resources and hostnames, and values.staging.yaml holding only the staging overrides. An arrow labelled last file wins carries both into a merge step that produces one resolved values tree. Helm combines that tree with the chart templates to render final Kubernetes manifests. Separately, an existingSecret reference in the values points at a pre-provisioned cluster Secret, so the database password never appears in the values files or the release history. The rendered manifests flow to helm upgrade. base values.yaml defaults: replicas, resources, hostnames values.staging.yaml overrides only: what differs -f base -f staging · last file wins Merge resolved values tree Helm render tree + chart templates Rendered manifests helm upgrade to cluster Cluster Secret geonode-db-credentials pre-provisioned, sealed existingSecret ref

Step-by-step implementation

1. Structure a base values.yaml with production-safe defaults

Keep the base file free of any environment-specific hostnames or credentials. It holds the defaults every environment inherits — conservative replica counts, resource requests, and feature flags biased toward safety. Treat the base as the contract the chart promises to honor.

# values.yaml — shared defaults inherited by every environment
geonode:
  replicas: 1
  image:
    repository: geonode/geonode
    tag: "4.4.2"
  resources:
    requests: { cpu: "500m", memory: "1Gi" }
    limits:   { cpu: "2",    memory: "4Gi" }
  featureFlags:
    monitoringEnabled: false
    debug: false
celery:
  workers: 2
  beat: true
postgis:
  host: postgis.geoportal.svc
  existingSecret: geonode-db-credentials

2. Create values.staging.yaml overriding only what differs

The overlay is deliberately small. It names the staging hostnames, trims resources to fit a cheaper cluster, and flips the feature flags that only make sense outside production. Anything not mentioned here falls through to the base — that fall-through is what keeps the overlay reviewable.

# values.staging.yaml — only the deltas for the staging cluster
geonode:
  replicas: 2
  resources:
    requests: { cpu: "250m", memory: "512Mi" }
    limits:   { cpu: "1",    memory: "2Gi" }
  featureFlags:
    monitoringEnabled: true
    debug: true
  ingress:
    host: staging.geoportal.example.gov
celery:
  workers: 4
postgis:
  host: postgis-staging.geoportal-staging.svc

3. Layer the files at upgrade time

Helm merges values files left to right, so the last -f flag wins on any conflicting key. Order the base first and the environment overlay last; deep keys merge, scalars replace. Use --atomic so a failed staging upgrade rolls itself back rather than leaving a half-applied release.

helm upgrade --install geonode ./chart \
  -n geoportal-staging --create-namespace \
  -f values.yaml \
  -f values.staging.yaml \
  --atomic --timeout 5m

4. Inject secrets by reference, never by value

The database password must never appear in either values file. Instead the overlay references a Secret that already exists in the namespace, and the chart’s template reads the credential through valueFrom.secretKeyRef at pod start. The values you commit only ever name the Secret and its key:

# values.staging.yaml fragment — reference, do not inline
postgis:
  existingSecret: geonode-db-credentials
  secretKeys:
    passwordKey: postgres-password

The Secret itself is provisioned out of band. A sealed manifest that is safe to commit looks like this — the ciphertext, not the password, lives in Git:

# sealed-db-credentials.yaml — encrypted at rest, safe for version control
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
  name: geonode-db-credentials
  namespace: geoportal-staging
spec:
  encryptedData:
    postgres-password: AgBv7a8xN2p...ciphertext-truncated...9Qk=

5. Render and diff before applying

Never let an upgrade be the first time you see the merged output. Render locally with both files layered, then diff the proposed release against what is live so the exact change set is reviewable in the pipeline.

# render the merged manifests without touching the cluster
helm template geonode ./chart \
  -f values.yaml -f values.staging.yaml \
  -n geoportal-staging > /tmp/rendered-staging.yaml

# show precisely what would change against the running release
helm diff upgrade geonode ./chart \
  -f values.yaml -f values.staging.yaml \
  -n geoportal-staging

6. Gate the overlay in CI

Wire rendering and validation into the pipeline so a malformed overlay fails the build instead of the live cluster. The stage renders the layered values, validates every manifest against the Kubernetes schemas offline, and confirms no plaintext password slipped into the output.

# .gitlab-ci.yml — validate the staging overlay before promotion
validate-staging-values:
  stage: test
  script:
    - helm template geonode ./chart -f values.yaml -f values.staging.yaml
        -n geoportal-staging > rendered.yaml
    - kubeconform -strict -summary -kubernetes-version 1.29.0 rendered.yaml
    # fail loudly if a real secret value ever reaches rendered output
    - "! grep -iE 'password:\\s*[A-Za-z0-9]{8,}' rendered.yaml"

This is the same gate-before-promote discipline used across Environment Parity in Geospatial CI Pipelines, applied to the values layer specifically.

Verification

Confirm the overlay resolved as intended, both before and after the upgrade.

# 1. The merged values Helm actually used — staging overrides must be present
helm get values geonode -n geoportal-staging
#   geonode.replicas: 2
#   geonode.ingress.host: staging.geoportal.example.gov
#   celery.workers: 4

# 2. The rendered manifests pass schema validation offline
helm template geonode ./chart -f values.yaml -f values.staging.yaml \
  -n geoportal-staging | kubeconform -strict -summary
#   Summary: 0 errors, 0 skipped

# 3. The running Deployment reflects the staging replica count
kubectl get deploy geonode -n geoportal-staging -o jsonpath='{.spec.replicas}'
#   2

# 4. The DB password is sourced from the Secret, not baked into the pod spec
kubectl get deploy geonode -n geoportal-staging \
  -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="POSTGRES_PASSWORD")].valueFrom.secretKeyRef.name}'
#   geonode-db-credentials

A helm get values output that shows the staging deltas, a clean kubeconform summary, and a secretKeyRef (rather than a literal value) on the password env confirm the overlay layered, validated, and kept the credential out of the manifest. Wiring these same checks into a promotion gate keeps every environment honest, the pattern extended in Syncing GeoNode Environments with Terraform.

Troubleshooting matrix

Symptom Likely cause Fix
Staging override ignored, base value used Overlay passed before base, or key path misspelled Put -f values.staging.yaml last; confirm the key path with helm get values
Nested map replaced wholesale, losing base keys A list or scalar overrode a map instead of merging Helm deep-merges maps but replaces lists; re-express the change as a map key, not a list
helm upgrade hangs then rolls back --atomic reverted on a failed readiness probe Check pod events; raise --timeout or fix the probe before retrying
Plaintext password appears in helm get manifest Credential inlined in a values file Move to existingSecret; rotate the exposed credential immediately
kubeconform fails on a valid-looking manifest Wrong --kubernetes-version or missing CRD schema Match the Kubernetes cluster version; add -schema-location for custom resources
Ingress host still points at production Overlay omitted ingress.host or typo in the key Add the key under the exact path the chart reads; re-render and diff
Diff shows churn on every run A generated hash suffix or timestamp in the chart Pin the generator or exclude the field from the diff comparison

Up one level: Helm vs Kustomize for GeoNode Deployments.