Install Now

helm/helm

Helm - Kubernetes Package Manager

Last updated on Dec 17, 2025 (Commit: 4d1150d)

Overview

Relevant Files
  • README.md
  • AGENTS.md
  • go.mod
  • cmd/helm/helm.go
  • pkg/action/
  • pkg/chart/
  • pkg/engine/
  • pkg/storage/

Helm is a package manager for Kubernetes written in Go. It streamlines installing and managing Kubernetes applications by rendering templates and communicating with the Kubernetes API. Think of it like apt/yum/homebrew for Kubernetes.

What Helm Does

Helm packages pre-configured Kubernetes resources into Charts—reusable, versioned packages containing:

  • A Chart.yaml metadata file describing the package
  • One or more templates with Kubernetes manifest files
  • Optional dependencies and configuration files

Charts can be stored locally or fetched from remote repositories, making it easy to share and deploy applications consistently across environments.

Project Status

The repository maintains two active development branches:

  • main branch: Helm v4 (unstable, under active development)
  • dev-v3 branch: Helm v3 (stable, current production version)

Changes are made to main first, then backported to dev-v3 to ensure all improvements reach the stable release.

Architecture Overview

Loading diagram...

Core Components

Command Layer (pkg/cmd/): Cobra-based CLI interface handling user input and routing to appropriate actions.

Action Layer (pkg/action/): Core operations including install, upgrade, rollback, uninstall, and release management. All actions share a common Configuration object.

Chart Engine (pkg/engine/): Template rendering using Go templates with Sprig functions for advanced templating capabilities.

Storage Layer (pkg/storage/): Pluggable backends for storing release metadata—supports Kubernetes Secrets, ConfigMaps, and SQL databases.

Kubernetes Integration (pkg/kube/): Abstracts Kubernetes client interactions, resource management, and status tracking.

Registry Support (pkg/registry/): OCI-compliant chart repository support for modern chart distribution.

Key Design Patterns

  • Shared Configuration: All actions use a common Configuration object for consistent state management
  • Dual Chart Support: Stable v2 charts in /pkg/chart/, next-gen v3 charts in /internal/chart/v3/
  • Pluggable Storage: Release storage abstraction allows different backends without changing core logic
  • Plugin System: Extensible architecture supporting custom plugins via subprocess or Extism runtimes

Architecture & Data Flow

Relevant Files
  • pkg/action/action.go - Configuration and shared dependencies
  • pkg/cmd/root.go - CLI entry point and command setup
  • pkg/chart/interfaces.go - Chart abstraction layer
  • pkg/engine/engine.go - Template rendering engine
  • pkg/storage/storage.go - Release storage abstraction
  • pkg/release/interfaces.go - Release data structures

High-Level Architecture

Helm follows a layered architecture with clear separation of concerns:

  1. CLI Layer (pkg/cmd/) - Cobra-based command handlers that parse user input and delegate to actions
  2. Action Layer (pkg/action/) - Core business logic for operations (install, upgrade, rollback, etc.)
  3. Chart Layer (pkg/chart/) - Chart loading, parsing, and dependency resolution
  4. Rendering Layer (pkg/engine/) - Go template rendering with Sprig functions
  5. Storage Layer (pkg/storage/) - Pluggable release persistence backends
  6. Kubernetes Layer (pkg/kube/) - Kubernetes API interaction

Data Flow: Install Operation

Loading diagram...

Configuration & Dependency Injection

The Configuration struct in pkg/action/action.go serves as the central dependency container:

  • RESTClientGetter - Loads Kubernetes clients from kubeconfig
  • Releases - Storage interface for persisting release metadata
  • KubeClient - Kubernetes API client for resource operations
  • RegistryClient - OCI registry client for chart distribution
  • Capabilities - Kubernetes cluster capabilities (API versions, resources)
  • CustomTemplateFuncs - User-defined template functions

All action types (Install, Upgrade, Rollback, etc.) embed this configuration, ensuring consistent access to shared dependencies.

Template Rendering Pipeline

The Engine in pkg/engine/engine.go processes charts through these stages:

  1. Chart Traversal - Recursively processes chart and subchart templates
  2. Value Scoping - Isolates values per chart to prevent cross-chart pollution
  3. Template Compilation - Compiles Go templates with Sprig functions
  4. Rendering - Executes templates with merged values
  5. Output Mapping - Returns map[string]string of filename to rendered content

Storage Abstraction

The Storage struct wraps pluggable drivers supporting multiple backends:

  • ConfigMap Driver - Stores releases as Kubernetes ConfigMaps
  • Secret Driver - Stores releases as Kubernetes Secrets
  • Memory Driver - In-memory storage for testing
  • SQL Driver - Relational database backend

Each driver implements the Driver interface with Create, Get, Update, Delete, and List operations. The Storage layer adds release history management and MaxHistory enforcement.

Release Lifecycle

Releases flow through these states:

  • Pending - Initial state during install/upgrade
  • Deployed - Successfully applied to cluster
  • Superseded - Replaced by newer release version
  • Failed - Operation failed, release rolled back
  • Uninstalling - Marked for deletion
  • Uninstalled - Deleted from cluster

Each release version is immutable once stored, enabling safe rollback operations.

CLI & Commands

Relevant Files
  • pkg/cmd/root.go
  • pkg/cmd/install.go
  • pkg/cmd/upgrade.go
  • pkg/cmd/uninstall.go
  • pkg/cmd/list.go
  • pkg/cmd/get.go
  • pkg/action/
  • cmd/helm/helm.go

Architecture Overview

Helm's CLI is built on Cobra, a Go framework for building command-line applications. The architecture separates concerns into two layers:

  • Command Layer (pkg/cmd/): Handles CLI parsing, flags, and user interaction
  • Action Layer (pkg/action/): Contains the business logic for each operation

This separation allows the action layer to be used programmatically without the CLI.

Loading diagram...

Command Structure

The root command is initialized in cmd/helm/helm.go and creates a Configuration object that is shared across all actions. Commands are organized into two categories:

Release Commands (manage deployed releases):

  • install – Deploy a chart to create a new release
  • upgrade – Update an existing release to a new chart version
  • uninstall – Remove a release and its resources
  • list – Display all releases in a namespace
  • get – Retrieve detailed information about a release (with subcommands: all, values, manifest, hooks, notes, metadata)
  • rollback – Revert to a previous release version
  • status – Show the status of a named release
  • history – Display release version history

Chart Commands (manage chart repositories and definitions):

  • repo – Manage chart repositories (add, remove, list, update, index)
  • search – Search for charts in repositories
  • pull – Download a chart from a repository
  • show – Display chart information (values, README, CRDs)
  • create – Generate a new chart scaffold
  • dependency – Manage chart dependencies
  • lint – Validate chart syntax
  • package – Bundle a chart into a versioned archive
  • verify – Verify chart provenance and integrity

Core Action Classes

Each command delegates to an action class in pkg/action/:

  • Install: Deploys a chart, creates release objects, applies Kubernetes manifests
  • Upgrade: Updates existing releases, handles install-or-upgrade logic
  • Uninstall: Removes releases, optionally preserves history
  • List: Queries releases with filtering by status, namespace, and custom patterns
  • Get: Retrieves release metadata, manifests, values, and hooks

All actions share a Configuration object that provides:

  • Kubernetes client access
  • Release storage backend (Secrets, ConfigMaps, or SQL)
  • Registry client for OCI chart support
  • Template rendering engine
  • Logging and capability detection

Flags and Options

Commands support common flags for:

  • Namespace selection: --namespace, --all-namespaces
  • Output formatting: --output (table, json, yaml)
  • Dry-run modes: --dry-run (client-side or server-side)
  • Value overrides: --values, --set, --set-string
  • Chart sources: --repo, --version, --devel
  • Kubernetes interaction: --kubeconfig, --context, --timeout

Flags are registered using Cobra's flag API and bound to action struct fields for easy access during execution.

Plugin System

Helm supports client-side plugins via the plugin command:

  • plugin install – Install a plugin from a path or URL
  • plugin list – Show installed plugins
  • plugin uninstall – Remove a plugin
  • plugin update – Update a plugin

Plugins are discovered and executed as external binaries, allowing users to extend Helm's functionality without modifying core code.

Chart Loading & Processing

Relevant Files
  • pkg/chart/loader/load.go - Main chart loading dispatcher
  • pkg/chart/v2/loader/load.go - v1/v2 chart loader implementation
  • internal/chart/v3/loader/load.go - v3 chart loader implementation
  • pkg/chart/loader/archive/archive.go - Archive extraction and security
  • pkg/downloader/manager.go - Dependency resolution and download
  • internal/resolver/resolver.go - Semantic version resolution
  • pkg/chart/v2/chart.go - Chart data structures
  • internal/chart/v3/chart.go - v3 Chart data structures

Helm charts are loaded through a unified dispatcher that automatically detects the chart format (v1, v2, or v3) and routes to the appropriate loader. The loading process handles both directory-based charts and compressed .tgz archives, with built-in security checks and support for chart dependencies.

Loading Flow

The entry point is pkg/chart/loader/load.go, which provides a format-agnostic Load() function. This function:

  1. Detects whether the input is a directory or file
  2. Reads Chart.yaml to determine the API version
  3. Routes to the appropriate version-specific loader (v2load or v3load)
// Detects chart version from Chart.yaml and routes accordingly
func LoadDir(dir string) (chart.Charter, error) {
    data, err := os.ReadFile(filepath.Join(dir, "Chart.yaml"))
    c := new(chartBase)
    yaml.Unmarshal(data, c)
    
    switch c.APIVersion {
    case c2.APIVersionV1, c2.APIVersionV2, "":
        return c2load.Load(dir)
    case c3.APIVersionV3:
        return c3load.Load(dir)
    }
}

Chart Structure

Both v2 and v3 charts share a common structure defined in their respective chart.go files:

  • Metadata - Chart.yaml contents (name, version, dependencies, etc.)
  • Templates - Kubernetes manifest templates in templates/ directory
  • Values - Default configuration from values.yaml
  • Schema - Optional JSON schema for value validation
  • Files - Miscellaneous files (README, LICENSE, etc.)
  • Raw - Original file contents (used for helm show values)
  • Lock - Dependency lock information from Chart.lock
  • Dependencies - Loaded subchart references

Archive Processing

Charts distributed as .tgz files are extracted via pkg/chart/loader/archive/archive.go with strict security controls:

  • Size limits - Max 100 MiB decompressed, 5 MiB per file
  • Path validation - Prevents directory traversal attacks
  • UTF-8 BOM handling - Strips byte order marks from text files
  • Tar extraction - Safely unpacks gzip-compressed tar archives

The LoadArchiveFiles() function reads the archive into memory as BufferedFile objects before processing, ensuring all security checks pass before any files are written.

Dependency Resolution

The pkg/downloader/manager.go handles chart dependencies through a multi-step process:

  1. Load - Read the chart directory and extract dependencies from metadata
  2. Resolve - Use semantic version constraints to find exact versions
  3. Download - Fetch resolved charts from repositories
  4. Lock - Write Chart.lock with resolved versions and digest
// Manager.Update() orchestrates the full dependency workflow
func (m *Manager) Update() error {
    c, err := m.loadChartDir()
    req := c.Metadata.Dependencies
    
    repoNames, err := m.resolveRepoNames(req)
    lock, err := m.resolve(req, repoNames)
    err = m.downloadAll(lock.Dependencies)
    
    return writeLock(m.ChartPath, lock, ...)
}

Version Resolution

The internal/resolver/resolver.go package resolves semantic version constraints to specific versions:

  • Local charts - Resolved from charts/ subdirectories
  • File URLs - Resolved from file:// paths on disk
  • Repository charts - Resolved from configured Helm repositories
  • Registry charts - Resolved from OCI registries

The resolver uses github.com/Masterminds/semver/v3 to evaluate version constraints and selects the best matching version from available options.

Format Differences

v1/v2 Charts (in pkg/chart/v2/):

  • Support deprecated requirements.yaml and requirements.lock
  • Default to APIVersion v1 if unspecified
  • Maintain backward compatibility with Helm 2

v3 Charts (in internal/chart/v3/):

  • Require explicit apiVersion: v3
  • Use only Chart.yaml and Chart.lock
  • Default to v3 if APIVersion is missing
  • Support library charts via type: library

Both versions support the same core features: templates, values, schemas, and dependencies. The main difference is metadata organization and deprecation handling.

Template Rendering Engine

Relevant Files
  • pkg/engine/engine.go - Core rendering engine and orchestration
  • pkg/engine/funcs.go - Template function registration and Sprig integration
  • pkg/engine/files.go - File access and manipulation in templates
  • pkg/engine/lookup_func.go - Kubernetes cluster lookups from templates

Helm's template rendering engine transforms chart templates into Kubernetes manifests using Go's text/template package enhanced with Sprig functions and custom Helm-specific capabilities.

Architecture Overview

Loading diagram...

Core Rendering Flow

The Engine struct in pkg/engine/engine.go orchestrates rendering through these stages:

  1. Template Collection - allTemplates() recursively gathers templates from the chart and all dependencies, applying value scoping so each chart only sees its own values
  2. Template Parsing - Templates are parsed in predictable order (shallow paths first) to enable template sharing and includes
  3. Function Registration - Sprig functions are loaded, custom functions added, and late-bound functions (include, tpl, lookup) are injected
  4. Execution - Each non-partial template is executed with its scoped values, producing rendered manifests
  5. Output Mapping - Results returned as map[string]string where keys are template paths and values are rendered content

Template Functions

The engine provides three categories of functions:

Sprig Functions - 100+ utility functions from the Sprig library (string manipulation, math, date formatting, etc.). Environment variable functions (env, expandenv) are explicitly removed for security.

Format Conversion - Custom functions for data transformation:

  • toYaml / fromYaml - YAML serialization
  • toJson / fromJson - JSON serialization
  • toToml / fromToml - TOML serialization
  • mustToYaml / mustToJson - Panic on error variants

Helm-Specific Functions:

  • include - Include another template by name
  • tpl - Render a string as a template
  • required - Enforce required values
  • lookup - Query Kubernetes cluster for existing resources

File Access

The .Files object provides template access to non-template chart files:

{{.Files.Get "config/app.conf"}}           # Get file as string
{{.Files.GetBytes "data/binary"}}          # Get raw bytes
{{.Files.Glob "config/**"}}                # Match files by pattern
{{.Files.Glob "config/**" | .AsConfig}}    # Convert to ConfigMap data
{{.Files.Glob "secrets/*" | .AsSecrets}}   # Convert to Secret data (base64)
{{.Files.Lines "config/list.txt"}}         # Split file into lines

Kubernetes Lookups

The lookup function queries the cluster for existing resources, enabling dynamic template generation:

{{lookup "v1" "Pod" "default" "my-pod"}}           # Get single resource
{{lookup "v1" "Pod" "default" ""}}                 # List all pods in namespace
{{lookup "apps/v1" "Deployment" "" "my-deploy"}}  # Cluster-scoped resource

Returns empty map if resource not found (allowing if not (lookup ...) patterns). Requires Kubernetes API access via RenderWithClient() or RenderWithClientProvider().

Rendering Modes

  • Strict Mode - Template rendering fails if a template references undefined values
  • Lint Mode - Allows missing required values for chart validation
  • Custom Functions - Users can inject custom template functions via Engine.CustomTemplateFuncs

Error Handling

Parse errors include filename and line number. Execution errors are reformatted with template location and function context. Missing values in non-strict mode render as empty strings (Go's <no value> is stripped).

Release Management & Storage

Relevant Files
  • pkg/storage/storage.go
  • pkg/storage/driver/driver.go
  • pkg/storage/driver/secrets.go
  • pkg/storage/driver/cfgmaps.go
  • pkg/storage/driver/memory.go
  • pkg/storage/driver/sql.go
  • pkg/action/install.go
  • pkg/action/upgrade.go
  • pkg/action/rollback.go
  • pkg/action/action.go

Helm manages release state through a pluggable storage abstraction layer. Every deployment, upgrade, and rollback operation persists release metadata to a backend storage system, enabling history tracking, rollback capabilities, and concurrent operation safety.

Storage Architecture

The storage system is built on a driver-based abstraction that separates the storage interface from implementation details. The Storage struct wraps a Driver interface and provides high-level operations like Create(), Update(), Delete(), and Get(). Drivers implement the actual persistence logic for different backends.

Loading diagram...

Supported Storage Backends

Helm v4 supports four storage drivers, selectable via the HELM_DRIVER environment variable:

  1. Secrets (default): Stores releases as Kubernetes Secrets in the release namespace. Each secret is labeled with metadata (name, version, status, owner).

  2. ConfigMaps: Stores releases as Kubernetes ConfigMaps. Similar to Secrets but suitable for non-sensitive data and environments with Secret restrictions.

  3. Memory: In-memory storage for testing and dry-run operations. Namespace-aware and reused across multiple operations within the same configuration.

  4. SQL: Experimental support for SQL databases (PostgreSQL). Requires HELM_DRIVER_SQL_CONNECTION_STRING environment variable.

Release Storage Format

Releases are stored as base64-encoded, gzip-compressed protobuf messages. Each storage object includes:

  • Key: Format sh.helm.release.v1.{release-name}.v{version} (e.g., sh.helm.release.v1.my-app.v3)
  • Type: helm.sh/release.v1 (Kubernetes object type field)
  • Labels: Metadata for querying (name, version, status, owner, timestamps)
  • Body: Serialized release object containing chart, config, manifest, and hooks

Release Lifecycle & History Management

The Storage.Create() method enforces history limits via MaxHistory. When creating a new release, if the history exceeds the limit, the least recent release is automatically removed. This prevents unbounded storage growth while preserving rollback capability.

// Example: MaxHistory = 10 keeps the 10 most recent releases
if s.MaxHistory > 0 {
    s.removeLeastRecent(name, s.MaxHistory-1)
}

Integration with Actions

Install, Upgrade, and Rollback actions interact with storage as follows:

  • Install: Creates a new release (version 1) via Releases.Create()
  • Upgrade: Creates a new release version via Releases.Create(), incrementing the version number
  • Rollback: Retrieves a previous release via Releases.Get(), then creates a new release with the previous manifest
  • Uninstall: Updates the release status to UNINSTALLED without deleting the record

Query operations like Deployed(), History(), and Last() filter releases by status and version to support listing and retrieval workflows.

Concurrency & Safety

The storage layer includes safeguards against concurrent operations. The Deployed() method detects and resolves concurrent deployments by selecting the latest revision. Kubernetes-backed drivers (Secrets/ConfigMaps) leverage Kubernetes' optimistic concurrency control via resource versions.

Plugin System & Extensibility

Relevant Files
  • internal/plugin/plugin.go
  • internal/plugin/loader.go
  • internal/plugin/runtime.go
  • internal/plugin/runtime_subprocess.go
  • internal/plugin/runtime_extismv1.go
  • internal/plugin/metadata.go
  • pkg/cmd/plugin.go
  • pkg/getter/plugingetter.go
  • internal/plugin/schema/

Helm's plugin system enables extensibility through a flexible architecture supporting multiple plugin types and runtimes. Plugins are discovered, loaded, and executed through a well-defined interface that abstracts away runtime details.

Plugin Architecture

The core plugin system is built on three key abstractions:

  1. Plugin Interface - Defines the contract for plugin instances with methods to retrieve metadata and invoke the plugin
  2. Runtime - Handles plugin instantiation and execution (subprocess, WebAssembly/Extism)
  3. Metadata - Describes plugin configuration, type, and runtime requirements

Plugins are defined by a plugin.yaml manifest file in their directory. The loader supports both legacy and v1 API versions, automatically converting them to a unified in-memory representation.

Plugin Types

Helm supports three primary plugin types, each with specific input/output schemas:

  • CLI Plugins (cli/v1) - Extend Helm with custom commands, receiving extra arguments and returning output data
  • Getter Plugins (getter/v1) - Handle custom chart download protocols by implementing protocol-specific retrieval logic
  • Post-Renderer Plugins (postrenderer/v1) - Transform rendered Kubernetes manifests before deployment

Each type defines its own input/output message structure and configuration schema through the internal/plugin/schema package.

Runtime Execution Models

Loading diagram...

Subprocess Runtime - Launches plugins as external processes, communicating via JSON over stdin/stdout. Supports platform-specific commands and lifecycle hooks (install, upgrade, uninstall).

Extism/WASM Runtime - Executes WebAssembly plugins in a sandboxed environment with configurable memory limits, HTTP access controls, and temporary filesystem support. Provides host functions for plugin-to-Helm communication.

Plugin Discovery & Loading

The LoadDir() function loads a single plugin from a directory, while LoadAll() scans a base directory for all plugins. The FindPlugins() function filters plugins by type and name using a descriptor pattern.

Plugin loading involves:

  1. Reading and parsing plugin.yaml (supporting both legacy and v1 formats)
  2. Validating metadata (name, type, runtime, configuration)
  3. Creating a runtime-specific plugin instance via the appropriate Runtime implementation
  4. Detecting duplicate plugin names across directories

Plugin Management Commands

The CLI provides commands for plugin lifecycle management:

  • helm plugin install - Install plugins from local paths, HTTP URLs, or OCI registries
  • helm plugin list - Display installed plugins
  • helm plugin uninstall - Remove plugins
  • helm plugin update - Update existing plugins
  • helm plugin verify - Verify plugin signatures
  • helm plugin package - Package plugins for distribution

Installation supports optional signature verification and can extract plugins from various sources (local filesystem, HTTP, VCS, OCI).

Plugin Invocation

Plugins are invoked through the Invoke() method, which accepts a context, input message, and optional stdin/stdout/stderr streams. The input message is JSON-serializable and interpreted according to the plugin type. Plugins return output messages or execution errors.

Plugins can also implement the PluginHook interface to respond to lifecycle events (install, upgrade, uninstall) through the InvokeHook() method.

Registry & Chart Distribution

Relevant Files
  • pkg/registry/client.go - OCI registry client implementation
  • pkg/registry/chart.go - Chart-specific registry operations
  • pkg/getter/ocigetter.go - OCI chart retrieval handler
  • pkg/getter/httpgetter.go - HTTP repository handler
  • pkg/pusher/ocipusher.go - OCI chart push implementation
  • pkg/downloader/chart_downloader.go - Chart download orchestration
  • pkg/uploader/chart_uploader.go - Chart upload orchestration
  • pkg/repo/v1/chartrepo.go - Traditional chart repository support
  • pkg/repo/v1/index.go - Repository index file handling

Helm supports two primary distribution mechanisms for charts: traditional HTTP repositories and modern OCI registries. This dual approach enables both legacy compatibility and cloud-native deployment patterns.

Traditional HTTP Repositories

Traditional chart repositories use HTTP(S) to serve charts and an index.yaml metadata file. The ChartRepository type in pkg/repo/v1/ manages repository configuration and index files. Each repository entry stores connection details: URL, credentials, TLS certificates, and authentication settings.

The HTTPGetter retrieves charts and index files over HTTP(S), handling authentication via basic auth or custom headers. Repository indexes are cached locally and parsed to resolve chart versions and download URLs. The ChartDownloader orchestrates the full download flow, including verification against provenance files when requested.

OCI Registry Support

OCI-compliant registries (Docker registries, Artifactory, Harbor, etc.) provide a modern alternative using the OCI Image Spec. The Client in pkg/registry/ implements OCI operations with ORAS (OCI Repository As Storage) as the underlying library.

Key OCI Concepts:

  • References: Charts are addressed as registry.example.com/myapp/mychart:1.0.0
  • Layers: Charts are stored as OCI image layers with specific media types
  • Media Types: Helm defines custom types for chart content, config, and provenance data
  • Credentials: Uses Docker config.json or environment variables for authentication

Chart Push & Pull Operations

// Pull downloads a chart from an OCI registry
result, err := client.Pull("registry.example.com/myapp/mychart:1.0.0",
    registry.PullOptWithChart(true),
    registry.PullOptWithProv(true))

// Push uploads a chart to an OCI registry
result, err := client.Push(chartBytes, "registry.example.com/myapp/mychart:1.0.0",
    registry.PushOptProvData(provBytes))

The OCIGetter handles chart retrieval by delegating to the registry client. The OCIPusher handles uploads, validating chart metadata and constructing proper OCI references. Both support TLS configuration, plain HTTP mode, and credential management.

Unified Download & Upload Interface

The ChartDownloader and ChartUploader provide scheme-agnostic interfaces. They detect the URL scheme (http, https, or oci) and route to the appropriate handler. This abstraction allows commands like helm pull and helm push to work seamlessly across repository types without knowing implementation details.

Loading diagram...

Authentication & Security

Both mechanisms support TLS certificates, basic authentication, and credential stores. OCI registries leverage Docker's credential helpers for secure credential storage. HTTP repositories support per-repository credentials and optional credential pass-through to dependency charts. The registry client handles OAuth2 flows for cloud registries automatically.

Version Handling

OCI tags have restrictions (no + character), so Helm converts semantic version pre-release identifiers: 1.0.0+build123 becomes 1.0.0_build123 when pushing and converts back when pulling. This ensures compatibility with OCI tag specifications while preserving version semantics.