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, runsvalkey-cli --cluster add-nodefor each, and--cluster rebalanceto 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:
- Generates a self-signed CA + server certificate inline via Helm’s
genCA/genSignedCerthelpers (preserved across upgrades by lookup). - Renders a
kubernetes.io/tlsSecret holdingca.crt,tls.crt,tls.key. - Publishes the CA as a ConfigMap (
<release>-tls-ca) so client Pods can mount it and verify the server. - 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:
- Runs
BGSAVEon the chosen Pod. - Polls
LASTSAVEuntil the snapshot completes. - Streams the RDB to local disk via
valkey-cli --rdb. - 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
PodDisruptionBudgetis 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: 60gives ValKey time toSHUTDOWN SAVEon SIGTERM.- StatefulSet
persistentVolumeClaimRetentionPolicy: Retainprotects 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 playbooksdocs/backups-restore.md— S3 lifecycle, restore recipes, disaster recoverydocs/tls-and-auth.md— TLS handshake details, ACL bootstrap recipes, mTLSdocs/network-and-exposure.md— how a namespace baseline policy interacts with the chart’s, cross-namespace allowlist, Kong external exposure