Helm chart for a Next.js app
Use for charts/<app>/ and how Argo CD consumes it. The image it runs comes from docker-nextjs; the pipeline that publishes both is github-actions.
The template's charts/__APP__/ is the source of truth. All values with comments: references/values.md. Argo CD wiring: references/argocd.md.
Rules
-
One chart per app, at
charts/<app>/in the app's repo root. Why there: Argo CD can sync it straight from git (path: charts/<app>) for a dev environment and from the OCI registry for production, with the same files; it's outsideapps/, so it's neither in the Docker build context nor a pnpm workspace package; and the chart changes in the same PR as the code that needs it. The chart is single-app on purpose: no umbrella, no subcharts. Generic, values-driven templates (like the backend umbrella chart) only where a single app needs the switch: ingress, HPA, PDB, topology spread. -
Never hand-edit
versionorappVersion. Both stay0.0.0in git.release.yamlrunshelm package --version X.Y.Z --app-version vX.Y.Zfrom the tagvX.Y.Z, so the published chart and the image tag always match. -
image.tagdefaults toappVersion; set it only for git-synced environments. From OCI,appVersionis the release tag and the image isghcr.io/nuvraxis/<app>:vX.Y.Z. Synced from git,appVersionis0.0.0, so the Argo CD Application setsimage.tag: sha-<short-sha>(ormain).image.repositoryis lowercase, always. -
Probes: startup, liveness and readiness, all on
/api/health. The startup probe (2s × 30) covers boot; liveness and readiness only start after it. The route never touches a dependency, so a database outage doesn't restart pods. Don't point a probe at/: it renders a page. -
Security context matches the image: uid/gid 1001,
runAsNonRoot,readOnlyRootFilesystem, no privilege escalation, all capabilities dropped,RuntimeDefaultseccomp.automountServiceAccountToken: falseon both the ServiceAccount and the pod: the app doesn't call the Kubernetes API. -
.next/cacheis anemptyDirwith asizeLimit. It's the only writable path (/app/apps/<app>/.next/cache). The limit keeps a runaway image cache from filling the node. -
Configuration through
envFrom: a ConfigMap fromvalues.config, and a Secret that already exists.config: API_BASE_URL: https://api.nuvraxis.com existingSecret: portal-secretsThe chart never templates secret values: no
kind: Secretwithdatafrom values, no--set secret=…. The Secret is created by External Secrets, Sealed Secrets or an operator, and only its name is in values. A ConfigMap change rolls the pods through thechecksum/configannotation. -
No
NEXT_PUBLIC_*in the chart. They're baked in at image build (seedocker-nextjs); setting them at runtime does nothing. -
Resources: requests for CPU and memory, a memory limit, no CPU limit. CPU limits throttle a Node server during bursts and inflate tail latency; the memory limit stays because Node's heap grows to what it's given.
-
High availability by default: 2 replicas, PDB
maxUnavailable: 1, rolling update withmaxUnavailable: 0, topology spread across zones and nodes (ScheduleAnyway). Withautoscaling.enabledthe Deployment omitsreplicas, so Argo CD and the HPA don't fight over it. -
Ingress is opt-in, TLS through cert-manager annotations.
ingress.enabled,className, hosts,tlswith asecretName, andcert-manager.io/cluster-issuerin annotations. For streaming responses on ingress-nginx addnginx.ingress.kubernetes.io/proxy-buffering: "off"; for rate limiting,limit-rps(seenextjs-security). -
No Helm hooks, no
lookup, no random values. Argo CD renders the chart withhelm templateand applies the result: hooks become Argo hooks with different semantics,lookupreturns nothing, andrandAlphaNumchanges on every sync and shows the app permanently out of sync. Everything rendered must be a pure function of the values. -
Private GHCR packages need pull credentials. GHCR packages are private on first push. For images: a
docker-registrySecret with a token that hasread:packages, referenced inimagePullSecrets(production values referenceghcr-pull). For the chart in Argo CD: a repository Secret withenableOCI: "true"(seereferences/argocd.md). Or make the packages public in GitHub if the code is public. -
values-production.yamlholds only what differs in production. Layer it:-f values.yaml -f values-production.yaml. Don't copy the defaults into it. -
Validate with
helm lint --strictandhelm template | kubeconform -strict. Why kubeconform over helm-unittest: the bugs a single-app chart actually has are wrong field names, types or API versions in the rendered output, which kubeconform catches against the real Kubernetes schemas with zero test code. helm-unittest asserts template logic, which this chart barely has. Lint and validate both values files, aspr.yamldoes.
Adding things
| Need | Change |
|---|---|
| A runtime env var | config: in values (non-secret) or a key in the existing Secret |
| A secret | add it to the external Secret; nothing in the chart changes |
| Another port or sidecar | justify it first; a Next app needs one port |
| A cron job | a separate chart or app, not this Deployment |
| NetworkPolicy | add templates/networkpolicy.yaml, toggled by networkPolicy.enabled |
Verify
helm lint --strict charts/<app>
helm lint --strict charts/<app> -f charts/<app>/values-production.yaml
helm template <app> charts/<app> -f charts/<app>/values-production.yaml \
| docker run --rm -i ghcr.io/yannh/kubeconform:v0.8.0 -strict -summary -kubernetes-version 1.34.0 -
Sources
- https://helm.sh/docs/topics/charts/
- https://helm.sh/docs/chart_best_practices/
- https://helm.sh/docs/topics/registries/
- https://helm.sh/docs/overview/
- https://github.com/yannh/kubeconform
- https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/
- https://kubernetes.io/docs/concepts/security/pod-security-standards/
- https://kubernetes.io/docs/concepts/scheduling-eviction/topology-spread-constraints/
- https://kubernetes.io/docs/concepts/workloads/pods/disruptions/
- https://argo-cd.readthedocs.io/en/stable/user-guide/helm/
- https://cert-manager.io/docs/usage/ingress/
- https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry