Last updated on October 26, 2026
Updated October 2026. Rewritten from scratch around the official Valkey Helm chart and a Redis 8 cluster on a plain StatefulSet. Every command below was tested on Kubernetes 1.36 with Helm 3.21.
There are now two sane ways to run Redis in Kubernetes, and this post covers both:
- Option 1: Valkey with its official Helm chart. Valkey is the Linux Foundation fork of Redis 7.2, BSD licensed and protocol compatible, so
redis-cliand every Redis client library talk to it unchanged. One primary, three replicas, password from a Secret, persistent volumes. Tested with chartvalkey/valkey0.12.0 (Valkey 9.1.2). - Option 2: a Redis 8 Cluster (3 primaries, 3 replicas) on a plain StatefulSet, for when you need Redis itself and sharding. Tested with
redis:8.4-alpine(Redis 8.4.7).
At the end there is Redis Commander, a small web UI, wired to the password Secret instead of an open LoadBalancer.
Option 1: Valkey with the official Helm chart
1. Add the Helm repository
helm repo add valkey https://valkey.io/valkey-helm/
helm repo update
helm search repo valkey/valkeyExpected result: valkey/valkey with chart version 0.12.0 and app version 9.1.2 (or newer).
2. Create a namespace and the password Secret
The chart reads user passwords from a Secret, one key per ACL user. The key name is the user name, so for the default user the key is default:
kubectl create namespace valkey
kubectl -n valkey create secret generic valkey-users \
--from-literal=default="$(openssl rand -hex 16)"3. Write values.yaml
auth:
enabled: true
usersExistingSecret: valkey-users
aclUsers:
default:
permissions: "~* &* +@all"
replica:
enabled: true
replicas: 3
persistence:
size: 8Gi
# storageClass: longhorn # leave empty to use the default StorageClass
valkeyConfig: |
appendonly yes
resources:
requests:
cpu: 100m
memory: 128MiTwo things here are easy to miss. With replica.enabled: true the chart refuses to install without replica.persistence.size. And if auth.enabled is true, the default user must be listed in aclUsers, otherwise anyone can connect without a password.
4. Install the chart
helm install valkey valkey/valkey --version 0.12.0 -n valkey -f values.yaml
kubectl -n valkey rollout status statefulset/valkeyExpected result: partitioned roll out complete: 4 new pods have been updated.
5. Check pods, services and volumes
kubectl -n valkey get pods,svc,pvcYou should see valkey-0 to valkey-3 Running, four PVCs Bound, and three services:
valkey: read-write, always points at the primary (valkey-0).valkey-read: read-only, load-balanced across all pods.valkey-headless: for the StatefulSet itself.
If the PVCs stay Pending, the cluster has no default StorageClass. This is the situation the old version of this post fixed with manual hostPath volumes. Do not do that any more; on a bare-metal or lab cluster install local-path-provisioner and mark it default instead:
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/v0.0.37/deploy/local-path-storage.yaml
kubectl patch storageclass local-path \
-p '{"metadata":{"annotations":{"storageclass.kubernetes.io/is-default-class":"true"}}}'6. Test it
export VALKEY_PASSWORD=$(kubectl -n valkey get secret valkey-users -o jsonpath='{.data.default}' | base64 -d)
# write through the primary
kubectl -n valkey exec valkey-0 -c valkey -- valkey-cli --no-auth-warning -a "$VALKEY_PASSWORD" -h valkey SET greeting hello
# read through the replicas
kubectl -n valkey exec valkey-2 -c valkey -- valkey-cli --no-auth-warning -a "$VALKEY_PASSWORD" -h valkey-read GET greeting
# replication status
kubectl -n valkey exec valkey-0 -c valkey -- valkey-cli --no-auth-warning -a "$VALKEY_PASSWORD" INFO replicationExpected result: OK, then hello, then role:master with connected_slaves:3 and every replica state=online. Without the password every command returns NOAUTH Authentication required., and a write sent to valkey-read fails with READONLY You can't write against a read only replica.
Applications connect to valkey.valkey.svc.cluster.local:6379 for writes and valkey-read.valkey.svc.cluster.local:6379 for reads, with any Redis client and the same password.
Option 2: Redis 8 Cluster on a StatefulSet
If you need Redis itself (Redis 8 is available under AGPLv3 again since May 2025) and want sharding across several primaries, a StatefulSet with cluster mode is enough. The earlier version of this section had two separate StatefulSets labelled master and replica, no password and no step that actually creates the cluster. In Redis Cluster the roles are assigned by redis-cli --cluster create, not by the StatefulSet name, so here it is one StatefulSet of six pods.
1. Password Secret
kubectl create namespace redis
kubectl -n redis create secret generic redis-cluster-auth \
--from-literal=password="$(openssl rand -hex 16)"2. Manifest
Save as redis-cluster.yaml. Storage uses the default StorageClass; add storageClassName under volumeClaimTemplates if you need another one.
apiVersion: v1
kind: ConfigMap
metadata:
name: redis-cluster-config
data:
redis.conf: |
cluster-enabled yes
cluster-config-file /data/nodes.conf
cluster-node-timeout 15000
cluster-require-full-coverage no
appendonly yes
appendfsync everysec
maxmemory 1gb
maxmemory-policy allkeys-lru
---
apiVersion: v1
kind: Service
metadata:
name: redis-cluster-headless
spec:
clusterIP: None
publishNotReadyAddresses: true
selector:
app: redis-cluster
ports:
- name: redis
port: 6379
- name: bus
port: 16379
---
apiVersion: v1
kind: Service
metadata:
name: redis-cluster
spec:
selector:
app: redis-cluster
ports:
- name: redis
port: 6379
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: redis-cluster
spec:
serviceName: redis-cluster-headless
replicas: 6
podManagementPolicy: Parallel
selector:
matchLabels:
app: redis-cluster
template:
metadata:
labels:
app: redis-cluster
spec:
securityContext:
runAsUser: 999
runAsGroup: 999
fsGroup: 999
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
labelSelector:
matchLabels:
app: redis-cluster
topologyKey: kubernetes.io/hostname
containers:
- name: redis
image: redis:8.4-alpine
command:
- sh
- -c
- >-
exec redis-server /etc/redis/redis.conf
--requirepass "$REDIS_PASSWORD"
--masterauth "$REDIS_PASSWORD"
--cluster-announce-ip "$POD_IP"
env:
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: redis-cluster-auth
key: password
- name: REDISCLI_AUTH
valueFrom:
secretKeyRef:
name: redis-cluster-auth
key: password
ports:
- name: redis
containerPort: 6379
- name: bus
containerPort: 16379
readinessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 5
periodSeconds: 5
livenessProbe:
exec:
command: ["redis-cli", "ping"]
initialDelaySeconds: 30
periodSeconds: 10
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 1536Mi
volumeMounts:
- name: data
mountPath: /data
- name: config
mountPath: /etc/redis
volumes:
- name: config
configMap:
name: redis-cluster-config
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes: ["ReadWriteOnce"]
resources:
requests:
storage: 10GiREDISCLI_AUTH lets redis-cli inside the pod authenticate without putting the password on the command line, which is also what the probes rely on. cluster-config-file lives on the volume, so a restarted pod keeps its node ID and rejoins the cluster with its new IP.
3. Apply and wait
kubectl -n redis apply -f redis-cluster.yaml
kubectl -n redis rollout status statefulset/redis-cluster4. Create the cluster (once)
IPS=$(kubectl -n redis get pods -l app=redis-cluster \
-o jsonpath='{range .items[*]}{.status.podIP}:6379 {end}')
kubectl -n redis exec redis-cluster-0 -- \
redis-cli --cluster create $IPS --cluster-replicas 1 --cluster-yesExpected result: the output ends with [OK] All 16384 slots covered.
5. Verify
kubectl -n redis exec redis-cluster-0 -- redis-cli cluster info | grep -E 'cluster_state|cluster_size|known_nodes'
kubectl -n redis exec redis-cluster-0 -- redis-cli -c -h redis-cluster SET user:42 alice
kubectl -n redis exec redis-cluster-3 -- redis-cli -c -h redis-cluster GET user:42Expected result: cluster_state:ok, cluster_size:3, cluster_known_nodes:6, then OK and alice. The -c flag matters: without it redis-cli does not follow MOVED redirects to the primary that owns the key slot. Your application needs a cluster-aware client for the same reason; the redis-cluster Service is only the first address it connects to.
Bonus: Redis Commander
Redis Commander is a lightweight web UI. Compared with the 2022 manifest, the password now comes from the Secret, the deprecated AppArmor annotations are replaced by securityContext.appArmorProfile, and the Service is ClusterIP: the UI has full access to your data, so it should not sit on a public LoadBalancer.
apiVersion: apps/v1
kind: Deployment
metadata:
name: redis-commander
namespace: valkey
spec:
replicas: 1
selector:
matchLabels:
app: redis-commander
template:
metadata:
labels:
app: redis-commander
spec:
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
appArmorProfile:
type: RuntimeDefault
containers:
- name: redis-commander
image: ghcr.io/joeferner/redis-commander:latest
env:
- name: REDIS_HOST
value: valkey
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef:
name: valkey-users
key: default
- name: K8S_SIGTERM
value: "1"
ports:
- name: http
containerPort: 8081
livenessProbe:
httpGet:
path: /favicon.png
port: http
initialDelaySeconds: 10
timeoutSeconds: 5
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
---
apiVersion: v1
kind: Service
metadata:
name: redis-commander
namespace: valkey
spec:
selector:
app: redis-commander
ports:
- name: http
port: 8081
targetPort: httpkubectl apply -f redis-commander.yaml
kubectl -n valkey port-forward svc/redis-commander 8081:8081Open http://localhost:8081. The server info page should show Valkey version 9.1.2. For the Redis 8 cluster from Option 2, put the Deployment in the redis namespace, set REDIS_HOST to redis-cluster and take the password from redis-cluster-auth.
Gotchas
- Pin the chart version. The Valkey chart moves fast: its README on the main branch already documents keys that 0.12.0 does not have. Install with
--versionand readhelm show values valkey/valkey --version 0.12.0before copying examples from the internet. - No more manual hostPath volumes. Hand-made PVs with
chownbreak as soon as a pod lands on another node or the image runs under a different UID (the Valkey chart runs as 1000, the official Redis image as 999). A provisioner gets this right. - Create the Redis Cluster once. Running
--cluster createagain on nodes that already hold slots fails. To grow, add pods and useredis-cli --cluster add-nodeand--cluster rebalance. - Never disable auth outside a lab. The 2022 post showed
auth.enabled=false. Anything that can reach the Service can then read and flush your data.
Takeaways
- For replication with failover reads, the official Valkey chart is the shortest path in 2026: one values file, a Secret, one
helm install. - For sharded Redis 8, a single StatefulSet plus one
redis-cli --cluster createis all it takes. - Passwords live in Secrets, UIs stay behind
port-forward, volumes come from a StorageClass.
