Bahriya Tersane

Documentation / valkey

ValKey

An enterprise-grade Helm chart for deploying ValKey on Kubernetes. ValKey is the open-source, BSD-licensed key-value store that succeeded Redis OSS — wire-compatible with the Redis protocol and clients.

The chart supports three deployment topologies behind a single mode: switch, and ships TLS, ACL bootstrap, persistence, S3 backups, an exporter sidecar, and a NetworkPolicy. It mirrors the conventions of the mysql chart.

Mode Topology Use it for
standalone 1 Pod dev, ephemeral caches, single-node fits-in-memory
sentinel 1 primary + N replicas, Sentinel sidecar on every Pod HA caches, session stores; data fits one node
cluster M shards × (1 primary + N replicas), gossip-based failover horizontal scale, > one node of data

Prerequisites

  • Kubernetes 1.27+
  • Helm 3.x
  • A StorageClass that supports ReadWriteOnce (Longhorn by default)

Installation

# Standalone
helm install vk charts/valkey

# Sentinel (HA), recommended 3-Pod topology
helm install vk charts/valkey \
  --set mode=sentinel \
  --set sentinel.preset=standard

# Cluster (sharded), recommended 3-shard topology
helm install vk charts/valkey \
  --set mode=cluster \
  --set cluster.preset=standard

See docs/topologies.md for the topology decision matrix.

Topology presets

Sentinel and Cluster modes both expose pre-built preset: shortcuts so you can pick a topology without sizing it yourself.

Sentinel presets

Preset Pods Quorum Notes
minimal 2 2 1 primary + 1 replica. Sentinel quorum is fragile — survives Pod loss but a network partition can prevent failover.
standard 3 2 Recommended. 1 primary + 2 replicas.
hardened 5 3 1 primary + 4 replicas. Higher read fan-out, stricter quorum.
custom sentinel.replicas sentinel.quorum Set both values yourself.

Cluster presets

Preset Pods Shards × Replicas Notes
minimal 3 3 × 0 Cache-only — losing any shard takes its slots offline.
standard 6 3 × 1 Recommended. Survives any single Pod loss.
hardened 9 3 × 2 Higher durability + more read replicas.
custom computed cluster.shards × (1 + cluster.replicasPerShard) Set both values yourself.

Connecting to ValKey

After helm install, the NOTES.txt printout includes connection examples for your chosen mode. Quick reference:

# Standalone / Sentinel best-effort / Cluster any-Pod
HOST=vk-valkey.<namespace>.svc.cluster.local   # svc.<clusterDomain> — set clusterDomain if your cluster does not use cluster.local

# Sentinel discovery — sentinel-aware clients use this to find the primary
SENTINEL_HOST=vk-valkey-sentinel.<namespace>.svc.cluster.local
SENTINEL_PORT=26379
MASTER_NAME=mymaster

# Read-only Service (sentinel + cluster modes)
READ_HOST=vk-valkey-readonly.<namespace>.svc.cluster.local

# Port-forward for local testing
kubectl port-forward -n <namespace> svc/vk-valkey 6379:6379

The chart also creates a headless service (vk-valkey-headless) so every Pod has a stable DNS name like vk-valkey-0.vk-valkey-headless.<namespace>.svc.cluster.local. Cluster-mode Pods follow the pattern vk-valkey-shard-<shard>-<ordinal>.

Failover + role tracking

In sentinel and cluster modes, every ValKey Pod labels itself: the image’s post-start monitor (/opt/bahriya/post-start.sh) checks INFO replication every 5 seconds and patches valkey.bahriya.io/role={primary,replica} on its own Pod through the Kubernetes API (RBAC: pods: get,patch — nothing more). The primary <release> Service selects role=primary and the <release>-readonly Service selects role=replica — so the load-balanced Services always track reality, even after a Sentinel-driven or cluster-gossip-driven failover.

Self-labelling is on by default for sentinel and cluster modes and a no-op in standalone. Disable it explicitly:

roleLabels:
  enabled: false

What this gives you in practice:

  • Non-Sentinel clients can use the primary Service safely. Writes routed through vk-valkey.<ns> always reach the live primary, including after a failover. Sentinel SDKs are still the gold standard, but the Service is no longer a foot-gun.
  • Reads spread across replicas. vk-valkey-readonly.<ns> excludes the current primary, so read traffic doesn’t pile up there.
  • Convergence within ~10s of a failover. Each Pod re-checks its role every 5 seconds; allow up to ~10 seconds for both sides of a promotion to settle.

In addition, sentinel-mode Pods consult the Sentinel Service on startup to determine their role — so a previous primary returning to a topology where a replica was promoted boots correctly as a replica instead of fighting for primary.

Slot rebalancing + scale-up (cluster mode)

The cluster-bootstrap Job is fully reconcile-aware:

  • Fresh install → valkey-cli --cluster create.
  • Restored data on primaries → manual CLUSTER ADDSLOTSRANGE + MEET + REPLICATE.
  • helm upgrade --set cluster.shards=N+1 → the bootstrap Job detects the new Pods, runs valkey-cli --cluster add-node for each, and --cluster rebalance to migrate slots onto the new shards. No manual intervention.

For ongoing maintenance, enable a periodic rebalance CronJob:

cluster:
  rebalanceCronJob:
    enabled: true
    schedule: "0 4 * * 0"   # Sunday 04:00
    threshold: 2            # skip if imbalance < 2%

The CronJob runs valkey-cli --cluster rebalance --cluster-yes --cluster-threshold N — idempotent, a no-op when slots are already evenly spread.

Authentication

Auth is disabled by default; the chart relies on NetworkPolicy for isolation. Enable it when ValKey is exposed beyond the trusted namespace.

auth:
  enabled: true
  ## Default-user password. Auto-generated if empty.
  password: ""
  ## Additional ACL users. Each is rendered into the ACL file.
  users:
    - name: app
      password: ""            # auto-generated and preserved across upgrades
      rules: "on ~* &* +@all"
    - name: readonly
      password: ""
      rules: "on ~* +@read"

Passwords are preserved across helm upgrade via a Secret lookup, so re-running the chart never rotates them. To use an externally-managed Secret instead, set auth.existingSecret — it must contain password and user-<name>-password keys.

Retrieve a password:

kubectl get secret vk-valkey -n <namespace> \
  -o jsonpath='{.data.password}' | base64 -d

TLS

tls:
  enabled: true
  requireSecureTransport: true   # reject plaintext on port 6379; tls-port only
  authClients: false             # set true to require client certificates (mTLS)

When TLS is enabled, the chart:

  1. Generates a self-signed CA + server certificate inline via Helm’s genCA / genSignedCert helpers (preserved across upgrades by lookup).
  2. Renders a kubernetes.io/tls Secret holding ca.crt, tls.crt, tls.key.
  3. Publishes the CA as a ConfigMap (<release>-tls-ca) so client Pods can mount it and verify the server.
  4. Wires TLS into replication and (in cluster mode) the cluster bus.

Mount the CA on a client Pod:

volumes:
  - name: valkey-ca
    configMap:
      name: vk-valkey-tls-ca
volumeMounts:
  - name: valkey-ca
    mountPath: /etc/valkey-tls
    readOnly: true

To use an external CA (cert-manager, your own PKI), set tls.existingSecret — the Secret must contain ca.crt, tls.crt, tls.key.

Persistence

RDB snapshots and AOF are both enabled by default. Each Pod gets its own PVC via volumeClaimTemplates; PVCs are retained on uninstall.

persistence:
  enabled: true
  rdb:
    enabled: true
    save:
      - "900 1"     # snapshot every 15min if >=1 key changed
      - "300 10"    # every 5min if >=10 changed
      - "60 1000"   # every 60s if >=1000 changed
    stopWritesOnBgsaveError: true
  aof:
    enabled: true
    fsync: "everysec"   # always | everysec | no
    autoRewritePercentage: 100
    autoRewriteMinSize: "64mb"

storage:
  storageClass: "longhorn"
  size: 10Gi

Memory limits and eviction

valkey:
  maxmemory: "1gb"                  # empty = no limit (uses all available)
  maxmemoryPolicy: "allkeys-lru"    # or noeviction, allkeys-lfu, volatile-lru, ...

maxmemory-policy is fully configurable — set it to noeviction when ValKey is used as a durable store rather than a cache.

Backups

S3-compatible backups via a daily CronJob. In sentinel/cluster mode the backup runs against a replica (configurable ordinal) to avoid taking BGSAVE load on the primary.

backup:
  enabled: true
  schedule: "0 2 * * *"
  fromReplicaOrdinal: 1        # which Pod ordinal to back up from
  allShards: true              # cluster mode: back up every shard
  s3:
    accessKey: ""
    secretKey: ""
    host: "s3.eu-central-1.amazonaws.com"
    bucket: "my-backups"
    region: "eu-central-1"
    prefix: "valkey"
    # existingSecret: my-backup-creds   # alternative

The CronJob:

  1. Runs BGSAVE on the chosen Pod.
  2. Polls LASTSAVE until the snapshot completes.
  3. Streams the RDB to local disk via valkey-cli --rdb.
  4. Uploads to s3://<bucket>/<prefix>/<release>/<shard>/<UTC-timestamp>.rdb.

See docs/backups-restore.md for restore recipes, S3 lifecycle, and disaster-recovery patterns.

Restoring from backup

# Standalone / Sentinel — literal key
restore:
  enabled: true
  s3Key: "valkey/vk-valkey/vk-valkey/20260521T020000Z.rdb"

# Cluster — use a `{shard}` placeholder, substituted at runtime per shard
restore:
  enabled: true
  s3Key: "valkey/vk-valkey/{shard}/20260521T020000Z.rdb"

When restore.enabled: true, each primary Pod gets an init container that downloads the RDB into its PVC on first boot, then drops a .restore-complete marker so subsequent restarts skip the download.

Mode What restores
standalone The single Pod.
sentinel Ordinal 0 only; replicas resync from the primary via replication.
cluster Ordinal 0 of every shard, each with its own per-shard RDB. The bootstrap Job detects the loaded data and switches from --cluster create to a manual join (CLUSTER ADDSLOTSRANGE + MEET + REPLICATE) so non-empty nodes are tolerated. Replicas resync via cluster replication.

To force a re-restore: delete /data/.restore-complete from the relevant PVC and restart the Pod.

Observability

A redis_exporter sidecar runs on every Pod when enabled:

metrics:
  enabled: true
  port: 9121

Prometheus scrape annotations are added to every Pod automatically. ValKey is wire-compatible with Redis, so the redis_exporter image works without modification. The chart does not ship a ServiceMonitor — wire scraping however you scrape elsewhere.

NetworkPolicy and external exposure

The chart renders a Calico projectcalico.org/v3 NetworkPolicy, so it requires Calico, which is the network fabric the Bahriya Kubernetes Engine installs. The policy adds Allow rules on top of any baseline policy the namespace already has; it never subtracts. The chart handles only the receiving side.

networkPolicy:
  enabled: true
  openByDefault: true                       # ingress from any Pod in the SAME namespace

  # Cross-namespace clients — pass any matchLabels block (recommended:
  # the auto-managed kubernetes.io/metadata.name label).
  allowFromNamespaces:
    - kubernetes.io/metadata.name: app-prod
    - kubernetes.io/metadata.name: app-staging

  # Pre-allowlist Kong even without external exposure (e.g. for a
  # manually-managed TCPIngress).
  allowFromIngressController:
    enabled: false
    namespace: kong
    podSelector:
      matchLabels:
        app.kubernetes.io/name: kong

  # Raw NetworkPolicyPeer escape hatch — full Kubernetes API surface.
  allowFrom:
    - ipBlock:
        cidr: 10.42.0.0/16

For external clients via Kong:

tls:
  enabled: true                              # REQUIRED — Kong does SSL passthrough

exposure:
  external:
    enabled: true
    host: "valkey.example.com"
    ingressClassName: kong
    ipAllowlist:                             # REQUIRED — empty list refused at render time
      - "203.0.113.0/24"
      - "198.51.100.42/32"

The chart renders a Kong TCPIngress + KongPlugin (ip-restriction), adds the Kong proxy to the chart’s NP, and bakes the external hostname into the TLS cert’s SAN list. Kong must be configured with a stream listener in ssl_preread mode — see docs/network-and-exposure.md for the full prerequisites and debugging walkthrough.

High availability

  • PodDisruptionBudget is enabled by default (maxUnavailable: 1) for sentinel and cluster modes. Disable for standalone (it’s a single Pod).
  • Pod anti-affinity preference (ScheduleAnyway) spreads ValKey Pods across nodes by default.
  • terminationGracePeriodSeconds: 60 gives ValKey time to SHUTDOWN SAVE on SIGTERM.
  • StatefulSet persistentVolumeClaimRetentionPolicy: Retain protects against accidental data loss on uninstall.

Parameters

Image and naming

Parameter Description Default
image.repository Container image repository valkey/valkey
image.tag Image tag (defaults to Chart appVersion) Set in each release
image.pullPolicy Pull policy IfNotPresent
imagePullSecrets Image pull secrets [{name: bhr-1x-ax-image-pull-creds}]
nameOverride Override the chart name ""
fullnameOverride Override the full release name ""

Mode

Parameter Description Default
mode standalone | sentinel | cluster standalone
clusterDomain Kubernetes cluster DNS domain used in composed FQDNs cluster.local
clusterRegion Region identifier of the target cluster (set per install; only used for external announce names) ""

ValKey core

Parameter Description Default
valkey.port Client port 6379
valkey.maxmemoryPolicy Eviction policy when maxmemory is reached allkeys-lru
valkey.maxmemory Memory limit (empty = no limit) ""
valkey.config Map of valkey.conf settings see values.yaml
valkey.configOverride Raw extra lines appended to valkey.conf ""

Persistence

Parameter Description Default
persistence.enabled Mount a PVC for /data true
persistence.rdb.enabled Write RDB snapshots true
persistence.rdb.save List of save <sec> <changes> lines ["900 1", "300 10", "60 1000"]
persistence.rdb.stopWritesOnBgsaveError Stop writes on snapshot failure true
persistence.aof.enabled Enable AOF true
persistence.aof.fsync always | everysec | no everysec
persistence.aof.autoRewritePercentage Auto-rewrite threshold (%) 100
persistence.aof.autoRewriteMinSize Auto-rewrite minimum size 64mb

Auth

Parameter Description Default
auth.enabled Enable AUTH + ACLs false
auth.password Default-user password (auto-generated if empty) ""
auth.users[] Additional ACL users (name, password, rules) []
auth.existingSecret Use an existing Secret instead ""

Sentinel mode

Parameter Description Default
sentinel.preset minimal | standard | hardened | custom standard
sentinel.replicas Custom topology Pod count 3
sentinel.quorum Custom topology quorum 2
sentinel.masterName Sentinel master name mymaster
sentinel.port Sentinel port 26379
sentinel.downAfterMilliseconds down-after timer 5000
sentinel.failoverTimeoutMs failover timeout 180000
sentinel.parallelSyncs parallel-syncs 1

Cluster mode

Parameter Description Default
cluster.preset minimal | standard | hardened | custom standard
cluster.shards Custom topology shard count 3
cluster.replicasPerShard Custom topology replicas per shard 1
cluster.busPort Cluster gossip port 16379
cluster.bootstrap Run the bootstrap Job on install/upgrade true
cluster.requireFullCoverage Refuse writes when any slot is unassigned true
cluster.rebalanceCronJob.enabled Render a periodic slot-rebalance CronJob false
cluster.rebalanceCronJob.schedule Cron schedule 0 4 * * 0
cluster.rebalanceCronJob.threshold Min imbalance % before migration 2

Storage

Parameter Description Default
storage.storageClass StorageClass for PVCs longhorn
storage.size PVC size 10Gi
storage.accessModes PVC access modes [ReadWriteOnce]
persistentVolumeClaimRetentionPolicy.whenDeleted PVC retention on uninstall Retain
persistentVolumeClaimRetentionPolicy.whenScaled PVC retention on scale-down Retain

TLS

Parameter Description Default
tls.enabled Enable TLS for client + intra-cluster traffic false
tls.requireSecureTransport Reject plaintext connections true
tls.authClients Require client certificates (mTLS) false
tls.existingSecret Use an existing TLS Secret ""
tls.existingSecretKeys Key names inside the existing Secret, projected onto /tls/{ca.crt,tls.crt,tls.key} {ca: ca.crt, cert: tls.crt, key: tls.key}
tls.selfSigned.validity Self-signed cert validity in days 3650

Backup + restore

Parameter Description Default
backup.enabled Render the backup CronJob false
backup.schedule Cron schedule 0 2 * * *
backup.image Container image (must have valkey-cli + s3cmd) Set in each release
backup.fromReplicaOrdinal Pod ordinal to back up from (sentinel/cluster) 1
backup.allShards Back up every shard (cluster mode) true
backup.s3.* S3 credentials + bucket + region + prefix ""
backup.s3.existingSecret Use an existing S3 credentials Secret ""
restore.enabled Render the restore init container false
restore.s3Key S3 key of the RDB to restore ""
restore.s3.* Override the restore S3 credentials (defaults to backup) ""

Role labels

Parameter Description Default
roleLabels.enabled true / false / null (auto-on for sentinel+cluster) null

Observability

Parameter Description Default
metrics.enabled Run a redis_exporter sidecar false
metrics.image.repository Exporter image oliver006/redis_exporter
metrics.image.tag Exporter tag Set in each release
metrics.port Exporter port 9121

NetworkPolicy

Parameter Description Default
networkPolicy.enabled Render the NetworkPolicy true
networkPolicy.openByDefault Allow ingress from any Pod in the same namespace true
networkPolicy.allowFromNamespaces List of matchLabels blocks — each becomes a namespaceSelector ingress rule []
networkPolicy.allowFromIngressController.enabled Allow Kong proxy Pods to reach ValKey false (auto-on when exposure.external.enabled)
networkPolicy.allowFromIngressController.namespace Kong’s namespace kong
networkPolicy.allowFromIngressController.podSelector Kong Pod selector {app.kubernetes.io/name: kong}
networkPolicy.allowFrom Raw NetworkPolicyPeer entries (ipBlock, custom selectors) []
networkPolicy.platformEgress.enabled Pod-scoped egress allows (intra-namespace, DNS, 443/6443) so replication, role labels and backups survive a default-deny egress policy true
networkPolicy.platformEgress.extraPorts Extra TCP ports to allow egress to anywhere []

External exposure (Kong TCPIngress)

Parameter Description Default
exposure.external.enabled Render TCPIngress + KongPlugin (requires tls.enabled: true) false
exposure.external.host SNI hostname for the TCPIngress (required when enabled and hosts is empty) ""
exposure.external.hosts Full SNI host list — per-pod node-<N>.… names route to their per-pod Service, everything else to the primary []
exposure.external.defaultHostname Instance’s default hostname label — with domain and clusterRegion, enables per-pod announce names + per-pod Services ""
exposure.external.domain Base domain of the announce names ""
exposure.external.ingressClassName IngressClass routed by the Kong proxy kong
exposure.external.ipAllowlist CIDRs / IPs allowed (REQUIRED non-empty when enabled) []
exposure.external.annotations Additional annotations on the TCPIngress {}

Services

Parameter Description Default
service.type Primary Service type ClusterIP
service.port Primary Service port 6379
readonlyService.enabled Render a <release>-readonly Service true
readonlyService.type Read-only Service type ClusterIP
sentinelService.type Sentinel Service type (sentinel mode only) ClusterIP

Pods + scheduling

Parameter Description Default
resources.limits.cpu CPU limit 1
resources.limits.memory Memory limit 1Gi
resources.requests.cpu CPU request 100m
resources.requests.memory Memory request 256Mi
terminationGracePeriodSeconds Grace period for clean shutdown 60
podDisruptionBudget.enabled Render a PDB (sentinel + cluster) true
podDisruptionBudget.maxUnavailable PDB target 1
podAnnotations Additional pod annotations {}
podLabels Additional pod labels {}
podSecurityContext Pod-level security context {fsGroup: 1000}
securityContext Container-level security context non-root, read-only rootfs
topologySpreadConstraints Override the default soft spread []
affinity Override the default soft anti-affinity {}
nodeSelector Node selector {}
tolerations Tolerations []
priorityClassName PriorityClass name ""

Extras

Parameter Description Default
extraEnv Extra env vars on the ValKey container []
extraInitContainers Extra init containers []
extraVolumes Extra volumes []
extraVolumeMounts Extra volume mounts []

See also

  • docs/topologies.md — when to pick standalone vs sentinel vs cluster, scale-up/shrink playbooks
  • docs/backups-restore.md — S3 lifecycle, restore recipes, disaster recovery
  • docs/tls-and-auth.md — TLS handshake details, ACL bootstrap recipes, mTLS
  • docs/network-and-exposure.md — how a namespace baseline policy interacts with the chart’s, cross-namespace allowlist, Kong external exposure