Skip to content
Install the CLI

How-to guide Package task guides

Deploy Valkey / Redis

Use the valkey package to deploy a Redis-compatible data store on the official Valkey image: a StatefulSet with a volume per pod, a Service for clients, password authentication, and, on clusters that span several nodes, Sentinel failover.

Add the package, and a secret holding its password, in the namespace where the applications that use it run:

how-to/package-stacks/valkey-cluster.nix
instances.cache = {
valkey = {
package = packages.valkey;
config.storage.size = "2Gi";
};
# Outside local development, give the secret a SOPS `source` with
# keys = [ "password" ], or use a secret-ref to an existing Secret.
valkey-password = {
package = packages.secret;
aliases = [ "valkeyPassword" ];
config.stringData.password = "development-only-password";
};
};

View source on GitHub ↗

The valkeyPassword dependency is required and auth is always on. It resolves to a secret or secret-ref instance in Valkey’s namespace that declares the key password. The inline password in the example is for local development. For a deployed environment, give the secret a SOPS source with keys = [ "password" ], or point a secret-ref instance at a Secret managed outside Kix. When the password of a secret-ref changes, the pods do not restart on their own; use reloader’s withReloadOn to roll them. The password is used byte for byte, so a trailing newline in the Secret is part of it, and clients must send it too. An empty password stops the pod from starting.

Each pod gets a claim of storage.size (8 GiB unless set). The claim’s class is storage.storageClassName when set, else the class of the cluster’s storageClasses provider, else the cluster default.

The availability level decides the topology. Below node, Valkey runs one standalone pod. From node up, sentinel.enabled defaults on and the instance runs three pods: one primary, two replicas, and a Sentinel sidecar in each pod that promotes a replica when the primary is lost. Set sentinel.enabled and replicas on the instance to choose explicitly. Evaluation refuses more than one pod without Sentinel, fewer than three with it, and a sentinel.quorum that the remaining Sentinels could not reach once the primary’s pod is gone.

Change Valkey’s configuration through settings, which renders valkey.conf directives, and Sentinel’s through sentinel.primarySettings:

cluster.nix
instances.cache.valkey.config = {
settings.maxmemory-policy = "allkeys-lru";
sentinel.primarySettings.down-after-milliseconds = 5000;
};

maxmemory defaults to 70% of resources.limits.memory, leaving room for replication buffers and the copy-on-write pages of an AOF rewrite. With the default noeviction policy, writes fail once the dataset is full. Directives the package sets itself or relies on, such as port, bind, the password, replication, and rename-command, are refused, and so is a value with a line break.

When the cluster has a prometheus provider, each pod also runs a redis_exporter sidecar, and the instance gets a ServiceMonitor and alerts. The sidecar is part of the pod template, so adding the first provider, or setting metrics.enabled = false, restarts every pod.

Valkey’s out follows the Redis contract, kix.contracts.redis. Applications take it as their valkey dependency and read these fields:

KeyValue
out.host, out.portThe Service that selects every Valkey pod, port "6379"
out.credentialsThe password secret: valkey.out.credentials.out.keyRef "password"
out.tlsfalse: Valkey serves no TLS
out.sentinelnull, or with Sentinel { port = "26379"; primarySet = "primary"; }

A package checks the dependency with deps.valkey.assertions = dep: kix.contracts.redis.assertions who dep;.

A consumer must run in Valkey’s namespace, because Kubernetes resolves a secretKeyRef only in the pod’s own namespace. Dependency injection does not offer a Valkey instance to a consumer in another namespace, and an explicit ref to one fails evaluation.

With Sentinel on, the Service also routes to replicas, which refuse writes. A client that writes asks Sentinel at out.host and out.sentinel.port for the primary of out.sentinel.primarySet. It sends Sentinel no password: Sentinel’s default user takes none, and an AUTH with a password is an error. That user may read Sentinel’s state, which covers the discovery calls of common clients (SENTINEL GET-PRIMARY-ADDR-BY-NAME, SENTINEL MASTERS, SENTINEL REPLICAS, SENTINEL SENTINELS), and may not change it. Only the sentinel-admin user, with the instance password, can reconfigure Sentinel.

A package whose client cannot use Sentinel asks for a standalone Valkey and refuses one that runs Sentinel anyway, such as an import:

package default.nix
deps.valkey = {
config.sentinel.enabled = false;
assertions =
dep:
kix.contracts.redis.assertions who dep
++ lib.optional (kix.contracts.redis.usesSentinel dep) {
assertion = false;
message = "this package has no Sentinel client; set sentinel.enabled = false on the valkey instance.";
};
};

To use a server Kix does not run, such as ElastiCache, replace the valkey instance with a kix.mkImport that sets the contract’s fields. Put the import in the application’s namespace, beside the secret that holds the password:

cluster definition
{ ref, ... }:
{
instances.app = {
redis-login = {
package = packages.secret;
config.source = ./secrets/redis-login.sops.yaml;
config.keys = [ "password" ];
};
valkey.package = kix.mkImport {
scope = "namespace";
out = {
host = "app-cache.abc123.euw1.cache.amazonaws.com";
port = "6379";
tls = true;
credentials = ref.app.redis-login;
};
};
};
}

Leave out credentials for a server that takes no password. An import without a password can keep the default scope = "cluster" and serve every namespace. Not every application can connect over TLS: Harbor and Immich refuse a server with tls = true, and n8n turns TLS on for it.

Evaluate the cluster:

kix-examples/
❱ kix check how-to-package-valkey
 TOOL       RESULT  DETAILS                      
 eval       pass    14 manifests evaluated       
 scorecard  pass    0 errors, 1 warnings, 0 info

Inspect the StatefulSet and client Service:

kix-examples/ output excerpt
❱ kix build how-to-package-valkey --output json
[
  {
    "apiVersion": "apps/v1",
    "kind": "StatefulSet",
    "metadata": {
      "name": "valkey",
      "namespace": "cache"
    },
    "spec": {
      "replicas": 1,
      "serviceName": "valkey-headless",
      "containers": [
        {
          "name": "valkey",
          "image": "docker.io/valkey/valkey:8.1.10@sha256:640c5e62cea04b6d6f2084232651d0cc70362d31f4f805e7be94dbed6855e8f2",
          "ports": [
            {
              "containerPort": 6379,
              "name": "valkey",
              "protocol": "TCP"
            }
          ]
        }
      ],
      "volumeClaimTemplates": [
        {
          "metadata": {
            "name": "data"
          },
          "spec": {
            "accessModes": [
              "ReadWriteOnce"
            ],
            "resources": {
              "requests": {
                "storage": "2Gi"
              }
            },
            "storageClassName": "standard"
          }
        }
      ]
    }
  },
  {
    "apiVersion": "v1",
    "kind": "Service",
    "metadata": {
      "name": "valkey",
      "namespace": "cache"
    },
    "spec": {
      "ports": [
        {
          "name": "valkey",
          "port": 6379,
          "protocol": "TCP",
          "targetPort": 6379
        }
      ]
    }
  }
]

The StatefulSet requests the configured 2 GiB volume and uses the headless Service for stable pod names. Applications connect to the valkey Service on port 6379.

Deploy the cluster and check the instance:

kix-examples/
❱ kix deploy how-to-package-valkey
❱ kix status how-to-package-valkey

To test it locally, forward the service port:

kix-examples/
❱ kix pf how-to-package-valkey valkey 6379:6379

While the forward is running, use a Redis-compatible client from another terminal:

❱ REDISCLI_AUTH=development-only-password valkey-cli ping

A successful connection returns PONG. Stop the port-forward with Ctrl-C.

If the pod remains pending, inspect its PVC and confirm that the requested StorageClass exists and can provision ReadWriteOnce volumes in the target cluster.