Skip to content

How to deploy Redis in Kubernetes with Helm chart + Redis Commander (2026)

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-cli and every Redis client library talk to it unchanged. One primary, three replicas, password from a Secret, persistent volumes. Tested with chart valkey/valkey 0.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/valkey

Expected 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: 128Mi

Two 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/valkey

Expected result: partitioned roll out complete: 4 new pods have been updated.

5. Check pods, services and volumes

kubectl -n valkey get pods,svc,pvc

You 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 replication

Expected 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: 10Gi

REDISCLI_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-cluster

4. 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-yes

Expected 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:42

Expected 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: http
kubectl apply -f redis-commander.yaml
kubectl -n valkey port-forward svc/redis-commander 8081:8081

Open 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 --version and read helm show values valkey/valkey --version 0.12.0 before copying examples from the internet.
  • No more manual hostPath volumes. Hand-made PVs with chown break 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 create again on nodes that already hold slots fails. To grow, add pods and use redis-cli --cluster add-node and --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 create is all it takes.
  • Passwords live in Secrets, UIs stay behind port-forward, volumes come from a StorageClass.

Links

Published inCI/CD & AutomationKubernetesLinux