---
# === IDENTITY ===
id: software/devops/k8s-helm-chart/2026
canonical_question: "Kubernetes reference: Helm Chart structure"
aliases:
  - "Helm chart directory layout"
  - "Chart.yaml fields explained"
  - "Helm templates values.yaml structure"
  - "Helm chart best practices"
  - "Helm hooks pre-install post-install"
  - "Helm OCI registry chart push pull"
  - "Helm chart dependencies subcharts"
  - "Helm 3 vs Helm 4 chart differences"
entity_type: software_reference
domain: software > devops > Kubernetes Helm Chart Structure
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-27
confidence: 0.94
version: 1.0
first_published: 2026-02-27

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "Helm 4.0 (Nov 2025) -- server-side apply, WASM plugins, post-renderers as plugins; Chart.yaml apiVersion v2 unchanged"
  next_review: 2026-08-26
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Chart.yaml MUST contain apiVersion (v2), name, and version fields -- omitting any causes helm package/install to fail"
  - "Chart names MUST use lowercase letters, numbers, and dashes only -- no uppercase, underscores, or dots"
  - "Chart version MUST follow SemVer 2.0.0 -- non-SemVer versions are rejected by Helm"
  - "CRD files in crds/ CANNOT use Go templates -- they are installed as plain YAML before template rendering"
  - "spec.selector in Deployment templates is immutable after creation -- changing it requires delete + recreate"
  - "Never hardcode namespace in templates -- use Release.Namespace or let users set via --namespace flag"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "Need to debug Kubernetes Deployment, Service, or Ingress YAML (not Helm-specific)"
    use_instead: "software/devops/k8s-deployment-service-ingress/2026"
  - condition: "Need Kustomize overlays instead of Helm templating"
    use_instead: "software/devops/kustomize-overlays/2026"
  - condition: "Need to debug CrashLoopBackOff or pod failures (runtime, not packaging)"
    use_instead: "software/debugging/kubernetes-crashloopbackoff/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: helm_version
    question: "Which Helm version are you using?"
    type: choice
    options: ["Helm 3.x", "Helm 4.x", "Not sure"]
  - key: chart_type
    question: "What type of chart are you creating?"
    type: choice
    options: ["Application (installable)", "Library (reusable helpers)", "Subchart (dependency)"]
  - key: registry_type
    question: "Where will you host the chart?"
    type: choice
    options: ["OCI registry (recommended)", "Traditional Helm repo", "Local filesystem", "Not sure"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/k8s-helm-chart/2026"
suggested_citation: "Source: knowledgelib.io -- AI Knowledge Library (verified 2026-02-27)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: "software/devops/k8s-deployment-service-ingress/2026"
      label: "Kubernetes Deployment + Service + Ingress Reference"
    - id: "software/devops/k8s-statefulset-databases/2026"
      label: "Kubernetes StatefulSet for Databases"
    - id: "software/devops/k8s-hpa/2026"
      label: "Kubernetes HPA Autoscaling"
  solves: []
  alternative_to:
    - id: "software/devops/kustomize-overlays/2026"
      label: "Kustomize Overlays Reference"
  often_confused_with:
    - id: "software/migrations/docker-compose-to-kubernetes/2026"
      label: "Docker Compose to Kubernetes Migration"

# === SOURCES (7 authoritative sources) ===
sources:
  - id: src1
    title: "Charts"
    author: Helm Project
    url: https://helm.sh/docs/topics/charts/
    type: official_docs
    published: 2025-11-12
    reliability: authoritative
  - id: src2
    title: "Chart Template Guide"
    author: Helm Project
    url: https://helm.sh/docs/chart_template_guide/
    type: official_docs
    published: 2025-11-12
    reliability: authoritative
  - id: src3
    title: "Chart Hooks"
    author: Helm Project
    url: https://helm.sh/docs/topics/charts_hooks/
    type: official_docs
    published: 2025-11-12
    reliability: authoritative
  - id: src4
    title: "Best Practices - General Conventions"
    author: Helm Project
    url: https://helm.sh/docs/chart_best_practices/conventions/
    type: official_docs
    published: 2025-11-12
    reliability: authoritative
  - id: src5
    title: "Use OCI-based registries"
    author: Helm Project
    url: https://helm.sh/docs/topics/registries/
    type: official_docs
    published: 2025-11-12
    reliability: authoritative
  - id: src6
    title: "Helm 4 Released"
    author: Helm Project
    url: https://helm.sh/blog/helm-4-released/
    type: official_docs
    published: 2025-11-12
    reliability: authoritative
  - id: src7
    title: "Best Practices - Values"
    author: Helm Project
    url: https://helm.sh/docs/chart_best_practices/values/
    type: official_docs
    published: 2025-11-12
    reliability: authoritative
---

# Kubernetes Reference: Helm Chart Structure

## TL;DR

- **Bottom line**: A Helm chart is a versioned package of pre-configured Kubernetes resources defined by a required `Chart.yaml`, a `values.yaml` for defaults, and Go-templated manifests in `templates/`. [src1]
- **Key tool/command**: `helm create mychart` scaffolds the full directory structure with best-practice defaults.
- **Watch out for**: CRD files in `crds/` cannot be templated -- they are installed as plain YAML before any templates render. [src1]
- **Works with**: Helm 3.x and 4.x, Kubernetes 1.19+, OCI registries (GA since Helm 3.8). [src5] [src6]

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Chart.yaml MUST contain `apiVersion: v2`, `name`, and `version` -- all three are required fields [src1]
- Chart names MUST be lowercase letters, numbers, and dashes only -- no uppercase, underscores, or dots [src4]
- Chart `version` MUST follow SemVer 2.0.0 -- Helm rejects non-compliant versions [src1]
- CRDs in `crds/` are plain YAML -- Go template directives are not processed [src1]
- Never hardcode `namespace:` in template metadata -- use `{{ .Release.Namespace }}` or let users pass `--namespace` [src4]
- Hook-created resources are NOT tracked as part of the release -- they require explicit deletion policies [src3]

## Quick Reference

| File / Directory | Required | Purpose | Templated |
|---|---|---|---|
| `Chart.yaml` | Yes | Chart metadata: name, version, apiVersion, dependencies | No |
| `values.yaml` | No (recommended) | Default configuration values for templates | No |
| `values.schema.json` | No | JSON Schema validation for values | No |
| `templates/` | No | Go-templated Kubernetes manifests | Yes |
| `templates/_helpers.tpl` | No | Named template definitions (partials) | Yes |
| `templates/NOTES.txt` | No | Post-install usage instructions shown to user | Yes |
| `templates/tests/` | No | Test pod definitions run by `helm test` | Yes |
| `charts/` | No | Dependency chart archives (.tgz) or unpacked subdirectories | N/A |
| `crds/` | No | Custom Resource Definitions (installed before templates) | No |
| `Chart.lock` | No | Locked dependency versions (auto-generated) | No |
| `.helmignore` | No | File patterns to exclude from packaging | No |
| `LICENSE` | No | Chart license file | No |
| `README.md` | No | Chart documentation | No |

### Chart.yaml Required and Common Fields [src1]

| Field | Required | Type | Description |
|---|---|---|---|
| `apiVersion` | Yes | string | `v2` for Helm 3/4 (`v1` was Helm 2) |
| `name` | Yes | string | Chart name (lowercase, dashes allowed) |
| `version` | Yes | string | Chart version (SemVer 2.0.0) |
| `type` | No | string | `application` (default, installable) or `library` (helpers only) |
| `appVersion` | No | string | App version (informational, not used by Helm) |
| `kubeVersion` | No | string | SemVer constraint for compatible K8s versions |
| `description` | No | string | Single-sentence chart description |
| `dependencies` | No | list | Subchart dependencies with name, version, repository |
| `keywords` | No | list | Search keywords |
| `maintainers` | No | list | Maintainer name, email, url |
| `icon` | No | string | URL to SVG/PNG icon |
| `deprecated` | No | bool | Mark chart as deprecated |

## Decision Tree

```
START: What do you need?
├── Create a new chart from scratch?
│   ├── YES → Run `helm create mychart` (see Step 1)
│   └── NO ↓
├── Add dependencies/subcharts?
│   ├── YES → Add to Chart.yaml dependencies (see Step 3)
│   │   ├── OCI registry? → Use `repository: "oci://registry/path"`
│   │   └── Traditional repo? → Use `repository: "https://charts.example.com"`
│   └── NO ↓
├── Run pre/post-install jobs (DB migration, etc.)?
│   ├── YES → Use Helm hooks with annotations (see Hooks section)
│   └── NO ↓
├── Share reusable template helpers across charts?
│   ├── YES → Create a library chart (type: library)
│   └── NO ↓
├── Publish chart to a registry?
│   ├── OCI registry (recommended) → `helm push mychart-1.0.0.tgz oci://registry/repo`
│   └── Traditional repo → `helm repo index` + host index.yaml
└── DEFAULT → Use application chart (type: application)
```

## Step-by-Step Guide

### 1. Scaffold a new chart

`helm create` generates a complete chart directory with best-practice defaults including a Deployment, Service, Ingress, HPA, and ServiceAccount template. [src1]

```bash
# Create a new chart named "myapp"
helm create myapp

# Directory structure created:
# myapp/
# ├── Chart.yaml
# ├── values.yaml
# ├── charts/
# ├── templates/
# │   ├── _helpers.tpl
# │   ├── deployment.yaml
# │   ├── hpa.yaml
# │   ├── ingress.yaml
# │   ├── service.yaml
# │   ├── serviceaccount.yaml
# │   ├── NOTES.txt
# │   └── tests/
# │       └── test-connection.yaml
# └── .helmignore
```

**Verify**: `ls myapp/` → should show `Chart.yaml  charts  templates  values.yaml`

### 2. Define chart metadata in Chart.yaml

Every chart must declare its identity and version. The `appVersion` field is purely informational and does not affect Helm's behavior. [src1]

```yaml
# myapp/Chart.yaml
apiVersion: v2
name: myapp
description: A production-ready web application chart
type: application
version: 0.1.0
appVersion: "1.16.0"
kubeVersion: ">=1.22.0-0"
maintainers:
  - name: Platform Team
    email: platform@example.com
keywords:
  - web
  - api
  - microservice
```

**Verify**: `helm lint myapp/` → should report `1 chart(s) linted, 0 chart(s) failed`

### 3. Add dependencies

Dependencies are declared in Chart.yaml and resolved with `helm dependency update`. Both OCI and traditional repositories are supported. [src1] [src5]

```yaml
# myapp/Chart.yaml (append to existing)
dependencies:
  - name: postgresql
    version: "15.5.x"
    repository: "oci://registry-1.docker.io/bitnamicharts"
    condition: postgresql.enabled
  - name: redis
    version: "19.x.x"
    repository: "oci://registry-1.docker.io/bitnamicharts"
    condition: redis.enabled
    alias: cache
```

```bash
# Fetch and lock dependencies
helm dependency update myapp/

# This creates:
# myapp/Chart.lock        (locked versions)
# myapp/charts/postgresql-15.5.x.tgz
# myapp/charts/redis-19.x.x.tgz
```

**Verify**: `helm dependency list myapp/` → shows STATUS `ok` for all dependencies

### 4. Configure default values

Structure values with camelCase naming. Prefer flat over nested for simple configs. Quote all strings explicitly to avoid YAML type coercion. [src7]

```yaml
# myapp/values.yaml
replicaCount: 3

image:
  repository: myapp
  pullPolicy: IfNotPresent
  # tag: overrides appVersion from Chart.yaml
  tag: ""

# serviceAccount controls whether a ServiceAccount is created
serviceAccount:
  create: true
  annotations: {}
  name: ""

service:
  type: ClusterIP
  port: 80

ingress:
  enabled: false
  className: nginx
  annotations: {}
  hosts:
    - host: myapp.example.com
      paths:
        - path: /
          pathType: Prefix
  tls: []

resources:
  limits:
    cpu: 500m
    memory: 256Mi
  requests:
    cpu: 100m
    memory: 128Mi

# postgresql is a subchart dependency
postgresql:
  enabled: true
  auth:
    postgresPassword: "changeme"
    database: myapp

redis:
  enabled: false
```

**Verify**: `helm template myapp myapp/` → renders all YAML with default values substituted

### 5. Write templates with built-in objects

Templates use Go template syntax with Sprig functions. The key built-in objects are `.Values`, `.Release`, `.Chart`, `.Files`, `.Capabilities`, and `.Template`. [src2]

```yaml
# myapp/templates/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: {{ include "myapp.fullname" . }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
spec:
  replicas: {{ .Values.replicaCount }}
  selector:
    matchLabels:
      {{- include "myapp.selectorLabels" . | nindent 6 }}
  template:
    metadata:
      labels:
        {{- include "myapp.selectorLabels" . | nindent 8 }}
    spec:
      serviceAccountName: {{ include "myapp.serviceAccountName" . }}
      containers:
        - name: {{ .Chart.Name }}
          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
          imagePullPolicy: {{ .Values.image.pullPolicy }}
          ports:
            - name: http
              containerPort: 80
              protocol: TCP
          livenessProbe:
            httpGet:
              path: /healthz
              port: http
          readinessProbe:
            httpGet:
              path: /readyz
              port: http
          resources:
            {{- toYaml .Values.resources | nindent 12 }}
```

**Verify**: `helm template myapp myapp/ --debug` → shows rendered manifest with no errors

### 6. Define reusable named templates

The `_helpers.tpl` file contains named templates (partials) that are shared across all templates. The underscore prefix tells Helm not to render it as a manifest. [src2]

```yaml
# myapp/templates/_helpers.tpl

{{/*
Chart name, truncated to 63 chars (K8s name limit).
*/}}
{{- define "myapp.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
Fully qualified app name with release prefix.
Truncated to 63 chars because some K8s name fields are limited.
*/}}
{{- define "myapp.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- if contains $name .Release.Name }}
{{- .Release.Name | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{- end }}

{{/*
Common labels following Helm best practices.
*/}}
{{- define "myapp.labels" -}}
helm.sh/chart: {{ include "myapp.chart" . }}
{{ include "myapp.selectorLabels" . }}
{{- if .Chart.AppVersion }}
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
{{- end }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}

{{/*
Selector labels -- must match between Deployment and Service.
*/}}
{{- define "myapp.selectorLabels" -}}
app.kubernetes.io/name: {{ include "myapp.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}

{{/*
Chart label value: name-version.
*/}}
{{- define "myapp.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}

{{/*
ServiceAccount name -- use override or generate from fullname.
*/}}
{{- define "myapp.serviceAccountName" -}}
{{- if .Values.serviceAccount.create }}
{{- default (include "myapp.fullname" .) .Values.serviceAccount.name }}
{{- else }}
{{- default "default" .Values.serviceAccount.name }}
{{- end }}
{{- end }}
```

**Verify**: `helm template test myapp/ | grep "app.kubernetes.io/name"` → shows label on all resources

### 7. Package and push to OCI registry

Helm 3.8+ and 4.x support OCI registries natively. The chart name and version are read from Chart.yaml automatically. [src5]

```bash
# Package the chart into a .tgz archive
helm package myapp/
# Output: Successfully packaged chart and saved it to: myapp-0.1.0.tgz

# Login to OCI registry
helm registry login ghcr.io -u USERNAME

# Push to OCI registry (name and tag inferred from Chart.yaml)
helm push myapp-0.1.0.tgz oci://ghcr.io/myorg/charts
# Output: Pushed: ghcr.io/myorg/charts/myapp:0.1.0

# Install directly from OCI
helm install myapp oci://ghcr.io/myorg/charts/myapp --version 0.1.0
```

**Verify**: `helm show chart oci://ghcr.io/myorg/charts/myapp --version 0.1.0` → displays Chart.yaml contents

## Code Examples
<!-- Keep inline examples <=15 lines. For longer scripts, extract to scripts/ subdirectory
     and link: "Full script: [name.ext](scripts/name.ext) (N lines)" -->

### Helm Hook: Pre-install database migration Job

> Full script: [helm-hook-pre-install-database-migration-job.yml](scripts/helm-hook-pre-install-database-migration-job.yml) (26 lines)

```yaml
# templates/db-migrate-job.yaml
# Input:  .Values.image, .Values.dbMigration
# Output: Job that runs DB migrations before main app starts
apiVersion: batch/v1
kind: Job
# ... (see full script)
```

### Conditional Ingress with TLS

> Full script: [conditional-ingress-with-tls.yml](scripts/conditional-ingress-with-tls.yml) (42 lines)

```yaml
# templates/ingress.yaml
# Input:  .Values.ingress (enabled, className, hosts, tls)
# Output: Ingress resource with optional TLS
{{- if .Values.ingress.enabled -}}
apiVersion: networking.k8s.io/v1
# ... (see full script)
```

### values.schema.json for validation

> Full script: [values-schema-json-for-validation.json](scripts/values-schema-json-for-validation.json) (30 lines)

```json
{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["replicaCount", "image"],
  "properties": {
# ... (see full script)
```

## Anti-Patterns

### Wrong: Hardcoding namespace in templates

```yaml
# BAD -- prevents users from deploying to their chosen namespace
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
  namespace: production
```

### Correct: Let Helm manage namespace

```yaml
# GOOD -- namespace set via helm install --namespace or Release.Namespace
apiVersion: v1
kind: ConfigMap
metadata:
  name: {{ include "myapp.fullname" . }}-config
  namespace: {{ .Release.Namespace }}
  labels:
    {{- include "myapp.labels" . | nindent 4 }}
```

### Wrong: Using :latest tag in values.yaml

```yaml
# BAD -- :latest is mutable, breaks rollbacks and reproducibility
image:
  repository: myapp
  tag: latest
```

### Correct: Pin to specific version or use Chart.appVersion

```yaml
# GOOD -- pinned version, defaults to Chart.appVersion if empty
image:
  repository: myapp
  tag: ""  # defaults to .Chart.AppVersion via template logic
```

### Wrong: Using arrays in values for user-configurable resources

```yaml
# BAD -- arrays are hard to override with --set (index-based)
servers:
  - name: foo
    port: 80
  - name: bar
    port: 443
```

### Correct: Use maps for merge-friendly overrides

```yaml
# GOOD -- maps are easy to override: --set servers.foo.port=8080
servers:
  foo:
    port: 80
  bar:
    port: 443
```

### Wrong: Not quoting string values in YAML

```yaml
# BAD -- YAML type coercion: "yes" becomes boolean true, "3.0" becomes float
enabled: yes
version: 3.0
```

### Correct: Always quote strings explicitly

```yaml
# GOOD -- explicit quoting prevents type coercion surprises
enabled: "yes"
version: "3.0"
```

### Wrong: No resource limits in chart defaults

```yaml
# BAD -- omitting resources risks OOMKills and noisy-neighbor issues
containers:
  - name: myapp
    image: myapp:1.0.0
```

### Correct: Always set resource requests and limits

```yaml
# GOOD -- set sane defaults that users can override via values.yaml
containers:
  - name: {{ .Chart.Name }}
    image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
    resources:
      {{- toYaml .Values.resources | nindent 6 }}
```

## Common Pitfalls

- **Missing `include` vs `template`**: `template` does not allow pipeline chaining (e.g., `| nindent`). Always use `include` for named templates that need indentation. Fix: replace `{{ template "foo" . }}` with `{{ include "foo" . | nindent N }}`. [src2]
- **Whitespace in templates**: Go templates insert whitespace literally. Fix: use `{{-` (trim left) and `-}}` (trim right) to control whitespace around control structures. [src2]
- **Chart.lock out of sync**: After editing dependencies in Chart.yaml, `Chart.lock` is stale. Fix: run `helm dependency update` to regenerate Chart.lock and fetch archives. [src1]
- **Hook resources not cleaned up**: Hook-created Jobs persist after completion, cluttering the namespace. Fix: add `"helm.sh/hook-delete-policy": hook-succeeded` annotation. [src3]
- **SemVer `+` in Kubernetes labels**: K8s labels prohibit the `+` character used in SemVer build metadata. Fix: replace `+` with `_` in label values using `| replace "+" "_"`. [src4]
- **appVersion treated as constraint**: `appVersion` is purely informational and does not affect dependency resolution. Fix: do not rely on appVersion for image tag logic unless explicitly wired in templates. [src1]
- **values.yaml type coercion**: Bare `yes`/`no`/`on`/`off` become booleans; bare `3.0` becomes a float. Fix: quote all string values and use `{{ int $value }}` for large integers. [src7]
- **Global values not propagated**: Subcharts only receive their own scoped values unless globals are used. Fix: place shared config under `global:` key in parent values.yaml -- this is passed to all subcharts. [src2]

## Diagnostic Commands

```bash
# Lint chart for errors and best-practice warnings
helm lint mychart/

# Render templates locally without installing (dry run)
helm template myrelease mychart/ --debug

# Show computed values (merged defaults + overrides)
helm get values myrelease -n mynamespace

# Show all manifest content of an installed release
helm get manifest myrelease -n mynamespace

# List installed releases across all namespaces
helm list --all-namespaces

# Debug dependency resolution
helm dependency list mychart/

# Validate values against schema
helm install myrelease mychart/ --dry-run --debug

# Show chart metadata from OCI registry
helm show chart oci://ghcr.io/myorg/charts/mychart --version 1.0.0

# Diff upcoming upgrade against running release (requires helm-diff plugin)
helm diff upgrade myrelease mychart/ -f custom-values.yaml

# Run chart test hooks
helm test myrelease -n mynamespace
```

## Version History & Compatibility

| Version | Status | Key Changes | Migration Notes |
|---|---|---|---|
| Helm 4.x (Nov 2025) | Current | Server-side apply, WASM plugins, post-renderers as plugins, kstatus-based watching, reproducible builds, slog logging | Chart.yaml `apiVersion: v2` unchanged; plugin configs may need updates [src6] |
| Helm 3.x (2019-2026) | Bug fixes until Jul 2026, security until Nov 2026 | Tiller removed, release info stored per-namespace, three-way merge, OCI support (GA 3.8+), JSON Schema validation | Standard path; most charts work on both 3.x and 4.x [src6] |
| Helm 2.x | EOL (Nov 2020) | Required Tiller server-side component with cluster-admin privileges | Use `helm 2to3` plugin to migrate; heritage labels are immutable on existing resources |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Packaging a multi-resource K8s app for distribution | Simple single-manifest deployment (1-2 YAML files) | Plain kubectl apply or Kustomize |
| Need parameterized config across environments (dev/staging/prod) | All environments are identical | kubectl apply with a single manifest |
| Publishing reusable charts to a team or community | One-off internal deployment that never changes | Kustomize overlays |
| Managing complex dependency trees (app + database + cache) | Already using a GitOps tool that handles deps (e.g., Crossplane compositions) | Native GitOps composition |
| Need lifecycle hooks for migrations, backups, or tests | Simple workloads with no pre/post-deploy actions | kubectl apply |
| Distributing charts via OCI registries alongside container images | Vendoring all manifests in a monorepo with no reuse | Directory of plain YAML |

## Important Caveats

- Helm 4.0 introduced server-side apply and WASM plugins but kept Chart.yaml `apiVersion: v2` -- existing charts work without changes, but post-renderer plugins may need migration [src6]
- CRDs in `crds/` are only installed on `helm install` -- they are NOT updated on `helm upgrade`. To update CRDs, apply them separately with `kubectl apply` or use a CRD management chart [src1]
- Library charts (type: library) cannot be installed directly -- they only provide helper templates consumed by application charts via dependencies [src1]
- OCI-based chart registries do not use `index.yaml` -- `helm repo add` does not work with OCI references; use `oci://` prefix directly in commands and Chart.yaml dependencies [src5]
- Hook resources are not tracked as part of the release -- `helm uninstall` will NOT delete hook-created resources unless they have `helm.sh/resource-policy: keep` removed and a delete-policy set [src3]

## Related Units

- [Kubernetes Deployment + Service + Ingress Reference](/software/devops/k8s-deployment-service-ingress/2026)
- [Kubernetes StatefulSet for Databases](/software/devops/k8s-statefulset-databases/2026)
- [Kubernetes HPA Autoscaling](/software/devops/k8s-hpa/2026)
- [Docker Compose to Kubernetes Migration](/software/migrations/docker-compose-to-kubernetes/2026)
