A Helm-based deployment framework for running a microservice stack (frontend + API + Postgres) consistently across dev, staging, and production without copy-pasting Kubernetes manifests per environment.
Most teams end up with one of two anti-patterns as they grow: either a single set of Kubernetes manifests hand-edited per environment (drift, forgotten changes, works in dev surprises), or a full separate chart per environment (duplicated templates that rot independently). This project uses one chart, parameterized values files, and real subchart dependencies to avoid both.
┌─────────────┐
│ Ingress │ (not yet wired — see Roadmap)
└──────┬──────┘
│
┌────────────┴────────────┐
│ │
┌─────▼─────┐ ┌──────▼──────┐
│ frontend │ │ backend │
│ (Nginx) │───calls───▶│ (Express) │
└───────────┘ └──────┬──────┘
│
┌──────▼──────┐
│ PostgreSQL │
│ (Bitnami │
│ subchart) │
└─────────────┘
charts/app-stack is the umbrella chart. frontend and backend are local subcharts; postgresql is a pinned Bitnami dependency. Environment differences (replica counts, resource limits, autoscaling, persistence) live entirely in values-dev.yaml / values-staging.yaml / values-prod.yaml the templates themselves never change per environment.
| dev | staging | prod | |
|---|---|---|---|
| Frontend/backend replicas | 1 | 2 | 3 |
| Postgres persistence | disabled | 1Gi | 5Gi |
| Resource limits | minimal | moderate | production-sized |
Two approaches are demonstrated here, deliberately:
secrets-<env>.yaml(gitignored,secrets-dev.yaml.examplecommitted as a template) .The baseline pattern: real values never touch git, layered in at deploy time with-f.- External Secrets Operator: a more realistic setup for teams already running a secrets backend.
charts/app-stack/eso/sets up aSecretStore(kubernetes provider), RBAC scoped to a single namespace, and anExternalSecretthat syncs a password into the cluster and feeds it to the Postgres chart viaauth.existingSecret. Thevault-mocknamespace stands in for an external secrets manager (AWS Secrets Manager, Vault, etc.) swapping the provider block insecretstore.yamlis the only change needed to point this at a real one.
Requires helm, kubectl, kind (or k3d), docker.
kind create cluster --name helm-demo
kind load docker-image <your-dockerhub-username>/helm-demo-frontend:v1 --name helm-demo
kind load docker-image <your-dockerhub-username>/helm-demo-backend:v1 --name helm-demo
helm repo add bitnami https://charts.bitnami.com/bitnami
helm dependency update charts/app-stack
helm install app-stack charts/app-stack -f charts/app-stack/values-dev.yaml
kubectl get pods -wSwitch environments with helm upgrade:
helm upgrade app-stack charts/app-stack -f charts/app-stack/values-prod.yamlGitHub Actions runs helm lint and helm template against all three environments on every push/PR a broken values-prod.yaml fails CI even if dev renders fine. See .github/workflows/helm-ci.yml.
- Bitnami discontinued free unauthenticated images at
docker.io/bitnami/*in August 2025. This chart pinspostgresql.image.repositoryandvolumePermissions.image.repositorytobitnamilegacy/*instead. Those images are frozen and unpatched fine for local/demo use, not for anything you'd actually run in production long-term. file://chart dependencies get packaged, not linked. Editing a subchart'stemplates/orvalues.yamldoes nothing until you rerunhelm dependency updateit repackages the.tgzsnapshot Helm actually reads from.- Liveness/readiness probes need
initialDelaySeconds. Without it, Kubernetes starts health-checking before the app finishes booting, kills it, and you get a restart loop that looks like an application bug but isn't.
- Ingress + TLS
- HPA re-enabled per environment
- ArgoCD-based GitOps deployment instead of manual
helm upgrade
MIT