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-diffplugin (helm plugin install https://github.com/databus23/helm-diff). kubectlaccess to the staging namespace — examples use-n geoportal-staging.- A GeoNode chart available locally at
./chartor from a pinned repository, exposinggeonode,postgis,celery, andgeoservervalue keys. kubeconform0.6+ on the pipeline runner for offline manifest schema validation.- A pre-provisioned Secret named
geonode-db-credentialsin the target namespace (created by External Secrets, Sealed Secrets, or a sealed manifest — never committed plaintext). - Write access to a Git repository where
values.yamlandvalues.staging.yamllive 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.
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 |
Related
- Helm vs Kustomize for GeoNode Deployments — the decision this overlay technique implements, with the full trade-off analysis.
- Environment Parity in Geospatial CI Pipelines — gating rendered manifests so staging and production never diverge.
- Syncing GeoNode Environments with Terraform — reconciling the infrastructure beneath these values to code.
Up one level: Helm vs Kustomize for GeoNode Deployments.