GuidesDevelopers and hostingValkey key-value server

Run a Valkey server with rootless Podman

Valkey 9.1 on Debian 13, the open-source fork of Redis, in rootless Podman under a user of its own, with a login per app, sized for 8 GB, reachable only from the server and over WireGuard, and nightly snapshots you have restored.

Tested on Valkey 9.1.2 on Debian 13 (trixie) on a Melonslab server Updated October 14, 2026

Recommended server for this guide

SSD RAID 1 · ECC DDR4 · 10 Gbit

VC-P Alloy · 2 vCPU · 8 GB Memory · 60 GB Storage

Month to month, no lock-in 7-day money-back guarantee

€8.99/mo

Deploy now
On this page

What you will set up

Valkey is an in-memory key-value store: apps use it as a cache, for sessions, for job queues, counters and rate limits. It answers in well under a millisecond because it keeps everything in memory, and it saves to disk so the data survives a restart. Apps that support Redis work with Valkey without changes.

Valkey started in March 2024, when Redis moved away from an open-source licence, as a fork of Redis 7.2.4. It is a project of the Linux Foundation, developed in the open on GitHub with engineers from companies such as AWS, Google and Oracle, and it is open source under the BSD 3-clause licence. Valkey contacts no online service: in a capture of our test server's traffic while Valkey started and served an app, the only outgoing traffic was the server's own time sync.

Here Valkey runs from its official image, in Podman under a user of its own called valkey, with a login for each app and the built-in default user switched off. It listens only on the server itself and on its WireGuard address, never on the public address.

Every step below was run on a fresh Melonslab VC-P Alloy (2 vCPU, 8 GB) with Debian 13:

  • Valkey 9.1.2 started with its users from a Podman secret. A connection without a user name got NOAUTH Authentication required., and the default user was refused.
  • The app's login could read and write, but not run CONFIG, SHUTDOWN or ACL. A second app's login, limited to keys starting with shop:, got NOPERM on the first app's keys.
  • An app server on WireGuard connected with valkey-cli and a Python script, and our computer through an SSH tunnel. From the internet, port 6379 timed out on the public address.
  • With 1 GB of test data, we measured what each kind of persistence costs, and checked that maxmemory refuses writes, or evicts old keys, when it is full.
  • After a reboot, 10,000 test records were all back. A nightly snapshot was restored into an empty volume, and an update from 9.0 to 9.1 kept the data.

After a reboot, the whole server used about 470 MB of memory, and Valkey 9 MB of it with our 10,000 records. Valkey grows with your data, up to the 4 GB limit set in step 3.

Before you start

You need:

You do not need a DNS name: apps connect to an IP address that only they can reach.

The examples use 203.0.113.10 for the server's public address, 10.8.0.1 for its WireGuard address, 10.8.0.2 for an app server on WireGuard, and app for the app's login. Replace them throughout.

1. Prepare the server

As root, create /etc/sysctl.d/60-valkey.conf:

# Let Valkey save snapshots without failing when memory is tight
vm.overcommit_memory = 1
# Let Valkey listen on the WireGuard address before the tunnel is up
net.ipv4.ip_nonlocal_bind = 1
sysctl --system
  • To save to disk, Valkey copies itself in memory with fork. Linux's default overcommit setting can refuse that copy when memory looks tight, and the save fails. Valkey's documentation asks for 1, and without it Valkey logs WARNING Memory overcommit must be enabled! at every start.
  • At boot, nothing makes the container wait for WireGuard, and Podman cannot listen on 10.8.0.1 before that address exists. The second line lets it, and connections work as soon as the tunnel is up. Without it, Valkey fails to start when it is quicker than WireGuard (see Troubleshooting).

Then create the user:

useradd -m -s /bin/bash valkey
loginctl enable-linger valkey
machinectl shell valkey@

Everything up to step 6 runs as valkey.

2. Create the logins

Valkey's built-in default user has no password and may run every command. Switch it off, and give each app a login of its own. Valkey reads them from an ACL file, which you store as a Podman secret, so no password is ever written in a file of its own:

admin=$(openssl rand -hex 32)
app=$(openssl rand -hex 32)
printf %s "$admin" | podman secret create valkey-admin-password -
printf '%s\n' 'user default off' "user admin on >$admin ~* &* +@all" "user app on >$app ~* &* +@all -@admin" | podman secret create valkey-users -
unset admin app

That makes three users:

  • default is off, so nobody can use Valkey without a user name and a password.
  • admin may do everything. You use it for backups and changes, from the server itself.
  • app may use every key (~*) and every channel (&*), and every command except those in the @admin group: CONFIG, ACL, SHUTDOWN, SAVE, MONITOR, DEBUG, replication and the like. Commands such as KEYS, INFO and FLUSHDB stay allowed, because many apps and libraries use them.

The admin password is also stored on its own, so valkey-cli in the container can log in as admin without you typing it. To see the passwords, for your app's settings:

podman secret inspect --showsecret --format '{{.SecretData}}' valkey-users

The password comes after >. Valkey keeps only a SHA-256 hash of it in memory.

3. Configure memory and persistence

Create ~/valkey.conf:

# Users and passwords come from the valkey-users secret
aclfile /run/secrets/valkey-users

# Memory: at most 4 GB of data, and refuse writes when it is full
maxmemory 4gb
maxmemory-policy noeviction

# Persistence: a log of every write, flushed to disk every second,
# and a snapshot every hour, or sooner after many changes
appendonly yes
appendfsync everysec
save 3600 1 300 100 60 10000
dir /data

Memory. Valkey keeps all its data in memory, and without a limit it takes what it needs until Linux stops it. Each time it saves, the copy it makes with fork shares memory with Valkey, but every key that changes while the save runs takes memory twice. In our test with 1 GB of data, an AOF rewrite during heavy writes copied 845 MB, almost all of it, while the same save on a quiet server copied 34 MB. So keep as much memory free as Valkey may use: on an 8 GB server that is 4 GB for Valkey. If other apps run on the same server, lower it.

When it is full, maxmemory-policy decides:

  • noeviction, used here, refuses writes with OOM command not allowed when used memory > 'maxmemory', and keeps everything. Use it when Valkey holds sessions, queues or anything else your app cannot lose.
  • allkeys-lru deletes the keys used least recently to make room. Use it when Valkey is only a cache, where a missing key is fetched again. We saw it evict old keys and accept the new one at the limit.
  • volatile-lru deletes only keys that have an expiry time, for a mix of the two.

Persistence. Valkey has two ways to save, and this configuration uses both:

  • Snapshots (RDB): the whole dataset written to one compact file, here every hour if anything changed, every 5 minutes after 100 changes, and every minute after 10,000. After a crash you lose what changed since the last snapshot. Our 1 GB snapshot took 13 to 19 seconds to write and became a 990 MB file. They are what the backup in step 8 copies.
  • The append-only file (AOF): every write is also appended to a log, which appendfsync everysec flushes to disk once a second. After a crash you lose at most about a second. The log grows until Valkey rewrites it as a snapshot plus new writes, automatically when it has doubled.

We measured the cost with valkey-benchmark, 50 clients writing 100-byte values:

PersistenceWrites per second
Snapshots only55,000
AOF, everysec46,500
AOF, always9,700

appendfsync always waits for the disk at every write and loses nothing, at a fifth of the speed. For a pure cache that can be rebuilt, appendonly no saves the disk writes. A restart takes longer with more data: Valkey answers LOADING Valkey is loading the dataset in memory until it has read everything. For our 1 GB of small keys, 8.6 million of them, that took 24 seconds right after the append-only file was rewritten, and 39 seconds when it held many writes since then.

4. Define the container

mkdir -p ~/.config/containers/systemd

Create ~/.config/containers/systemd/valkey.container:

[Unit]
Description=Valkey

[Container]
ContainerName=valkey
Image=docker.io/valkey/valkey:9.1
Exec=valkey-server /etc/valkey/valkey.conf
Volume=%h/valkey.conf:/etc/valkey/valkey.conf:ro
Volume=valkey-data:/data
Secret=valkey-users
Secret=valkey-admin-password,type=env,target=VALKEYCLI_AUTH
# Only this server and WireGuard devices can reach Valkey: the port is not open to the internet.
PublishPort=127.0.0.1:6379:6379
PublishPort=10.8.0.1:6379:6379
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target
  • Secret=valkey-users puts the ACL file at /run/secrets/valkey-users in the container, where valkey.conf reads it.
  • VALKEYCLI_AUTH is the variable valkey-cli takes its password from.
  • The two PublishPort lines decide who can connect: the server itself on 127.0.0.1, and WireGuard devices on 10.8.0.1. Valkey does not listen on the public address at all. Without WireGuard, leave out the second line.
  • The 9.1 tag gets every bug and security fix of Valkey 9.1, and nothing more. Step 9 has the details.

Start it:

systemctl --user daemon-reload
systemctl --user start valkey
systemctl --user enable --now podman-auto-update.timer
podman exec valkey valkey-cli --user admin ping

It answers PONG. Without a user name, you get the default user, which is off:

podman exec valkey valkey-cli ping
AUTH failed: WRONGPASS invalid username-password pair or user is disabled.
NOAUTH Authentication required.

5. Let your apps in

Over WireGuard. Go back to root with exit, and open port 6379 on the tunnel only:

ufw allow in on wg0 to any port 6379 proto tcp

On the app server, which has the tunnel address 10.8.0.2, Debian's valkey-tools package has valkey-cli:

apt install -y valkey-tools
valkey-cli -h 10.8.0.1 --user app --askpass

--askpass asks for the password, so it is not left in your shell history. Then:

10.8.0.1:6379> set greeting hello
OK
10.8.0.1:6379> get greeting
"hello"
10.8.0.1:6379> config get maxmemory
(error) NOPERM User app has no permissions to run the 'config|get' command

In an app, the connection URL is redis://app:PASSWORD@10.8.0.1:6379/0. Most Redis libraries take it as it is, and some newer ones accept valkey:// too. The traffic is encrypted by WireGuard.

Apps in containers on this server. Inside a container, 127.0.0.1 is the container itself. Use 10.8.0.1 instead: the server's WireGuard address can be reached from rootless containers, as we checked from a container under another user.

From your own computer, through SSH. An SSH tunnel needs no change on the server:

ssh -N -L 6380:localhost:6379 root@203.0.113.10

While that runs, connect in another terminal to port 6380 on your own computer:

valkey-cli -p 6380 --user app --askpass

Port 6380 is used so it does not clash with a Valkey or Redis on your own computer.

About TLS. Valkey can encrypt connections itself, with tls-port and a certificate. This guide does not use it: WireGuard and SSH already encrypt everything that leaves the server, and connections on 127.0.0.1 never leave it. Do not open port 6379 to the internet instead, with or without TLS.

6. Check what others see

On the server, as root:

ss -tlnp | grep 6379
LISTEN 0      128          10.8.0.1:6379      0.0.0.0:*    users:(("pasta.avx2",pid=2814,fd=7))
LISTEN 0      128         127.0.0.1:6379      0.0.0.0:*    users:(("pasta.avx2",pid=2814,fd=6))

pasta is the program that passes connections into the container. Only those two addresses: nothing on 0.0.0.0, [::] or the public address. From any computer that is not on the VPN:

nc -zv -w 10 203.0.113.10 6379
nc: connect to 203.0.113.10 port 6379 (tcp) timed out: Operation now in progress

ufw drops the attempt without an answer, and on the server journalctl -k | grep 'DPT=6379' shows it as [UFW BLOCK]. Valkey is not listening there either, so it stays closed even if the firewall is switched off.

7. Connect from an app

An example in Python, with the valkey library, a fork of redis-py by the Valkey project. On the app server:

python3 -m venv ~/venv
~/venv/bin/pip install valkey

Create app.py:

import os
import valkey

r = valkey.Valkey(
    host=os.environ.get("VALKEY_HOST", "10.8.0.1"),
    port=6379,
    username="app",
    password=os.environ["VALKEY_PASSWORD"],
    decode_responses=True,
)

r.set("greeting", "Hello from the app")
print(r.get("greeting"))

# A counter, and a list used as a simple queue
print("visits:", r.incr("visits"))
r.lpush("jobs", "send-invoice:1042")
print("next job:", r.rpop("jobs"))
read -rs VALKEY_PASSWORD && export VALKEY_PASSWORD
~/venv/bin/python app.py
Hello from the app
visits: 1
next job: send-invoice:1042

The redis library works too: we ran the same script with redis.Redis(...) and the same arguments.

A login for each app

Give a second app a login of its own, limited to keys that start with its own prefix. As valkey, add a line to the secret and restart:

{ podman secret inspect --showsecret --format '{{.SecretData}}' valkey-users; echo "user shop on >$(openssl rand -hex 32) ~shop:* &shop:* +@all -@admin -flushall -flushdb"; } | podman secret create --replace valkey-users -
systemctl --user restart valkey

shop can now read and write only keys such as shop:cart:1, and a GET user:42 gets NOPERM No permissions to access a key. -flushall -flushdb matter here: those two commands empty the whole database, whatever the key limits say. KEYS and SCAN still list the names of every key, though not their values. Apps that must not even see each other's key names need a Valkey of their own.

8. Back up every night

A snapshot can be taken from the running Valkey, while apps keep using it. Create ~/valkey-backup:

#!/bin/sh
# Save a snapshot of Valkey, and keep 14 days of them
set -e
umask 077
mkdir -p ~/backup
podman exec valkey valkey-cli --user admin --rdb /tmp/backup.rdb
podman cp valkey:/tmp/backup.rdb ~/backup/valkey-$(date +%F).rdb
podman exec valkey rm /tmp/backup.rdb
find ~/backup -name 'valkey-*.rdb' -mtime +13 -delete

valkey-cli --rdb asks Valkey for a fresh snapshot, as a replica would. It writes it to a file in the container first: written straight to the screen, with -, the snapshot came out with 40 extra bytes at the end. Run the script every night with a timer of the user's own. Create ~/.config/systemd/user/valkey-backup.service:

[Unit]
Description=Back up Valkey

[Service]
Type=oneshot
ExecStart=%h/valkey-backup

And ~/.config/systemd/user/valkey-backup.timer:

[Unit]
Description=Back up Valkey every night

[Timer]
OnCalendar=*-*-* 02:30
Persistent=true

[Install]
WantedBy=timers.target
chmod 755 ~/valkey-backup
systemctl --user daemon-reload
systemctl --user enable --now valkey-backup.timer
systemctl --user start valkey-backup.service
ls -l ~/backup
-rw-r--r-- 1 valkey valkey 646899 Oct 14 01:45 valkey-2026-10-14.rdb

Save the logins once, too, since a new server needs them and your apps know the passwords:

podman secret inspect --showsecret --format '{{.SecretData}}' valkey-users > ~/backup/users.acl

Copy ~/backup to another machine, for example with restic, and keep it private: it holds all your data and the passwords.

Test a restore. When the AOF is on and its directory is missing, Valkey starts empty and ignores a snapshot next to it. We saw exactly that: the first restore came up with no keys. So switch the AOF off for the first start, and back on once the data is in. As valkey, with the snapshot in ~/backup:

systemctl --user stop valkey
podman volume rm valkey-data
podman volume create valkey-data
podman run --rm -v valkey-data:/data -v ~/backup:/backup:ro docker.io/valkey/valkey:9.1 cp /backup/valkey-2026-10-14.rdb /data/dump.rdb
sed -i 's/^appendonly yes/appendonly no/' ~/valkey.conf
systemctl --user start valkey
podman exec valkey valkey-cli --user admin dbsize
podman exec valkey valkey-cli --user admin config set appendonly yes
sed -i 's/^appendonly no/appendonly yes/' ~/valkey.conf

dbsize shows how many keys came back: all 10,002 of ours. config set appendonly yes writes a new AOF from the data in memory. Wait until podman exec valkey valkey-cli --user admin info persistence | grep aof_rewrite_in_progress shows 0 before you restart Valkey. On a new server, create valkey-users from users.acl in step 2, with podman secret create valkey-users ~/backup/users.acl, and valkey-admin-password from the admin password in it.

9. Update

podman auto-update checks once a day for a new image under the 9.1 tag, and restarts Valkey when there is one: the patch releases with bug and security fixes. To see what it would do right now:

podman auto-update --dry-run

A new minor version, such as 9.2, or a new major version needs a change of tag. Take a backup, read the release notes on GitHub, change Image= in valkey.container, and restart:

systemctl --user daemon-reload
systemctl --user restart valkey
podman exec valkey valkey-cli --user admin info server | grep valkey_version

We tested an update from 9.0.6 to 9.1.2 this way, and the data came along. Going back to an older version may not work, since a newer Valkey can write snapshots an older one cannot read: that is what the backup is for. Use only plain version tags such as 9.1: when we tested, the 9.2 tag pointed at a release candidate, 9.2.0-rc1.

Troubleshooting

Valkey does not start after a reboot, and its log says Failed to bind port 6379 (Cannot assign requested address) for option '-t 10.8.0.1/6379-6379:6379-6379'. Valkey started before WireGuard, and net.ipv4.ip_nonlocal_bind is not set. Check that sysctl net.ipv4.ip_nonlocal_bind shows 1 (step 1), then run systemctl --user reset-failed valkey and systemctl --user start valkey as valkey.

NOAUTH Authentication required. or WRONGPASS invalid username-password pair or user is disabled. The client connects without a user name, as default, or with a wrong password. Set the user name app as well as the password. With Debian 13's valkey-cli 8.1, --user needs --askpass or --pass; the VALKEYCLI_AUTH variable is not used with it.

NOPERM User app has no permissions to run the '...' command. The command is in the @admin group. Run it as admin on the server, or if the app really needs it, add it to the app's line in the secret, such as +config|get, and restart.

OOM command not allowed when used memory > 'maxmemory'. Valkey is full. Raise maxmemory if the server has memory to spare, delete data, or use allkeys-lru if Valkey is only a cache (step 3).

LOADING Valkey is loading the dataset in memory. Valkey has just started and is still reading its data from disk. Wait: it took 24 to 39 seconds for 1 GB on our server.

The data is gone after a restore. The AOF was on at the first start. Start again from the restore steps, with appendonly no for the first start.

An app server gets Connection timed out to 10.8.0.1. Check that WireGuard is up on both ends with wg show, and that ufw has the rule from step 5.

Run it on your own server

SSD RAID 1 · ECC DDR4 · 10 Gbit

VC-P Alloy

€8.99/mo

vCPU
2
Memory
8 GB
Storage
60 GB
Transfer
10 TB
Performance
SSD · RAID 1
  • Full root access
  • Native /64 IPv6
  • RAID-protected storage
  • Malmö, Sweden
  • Month to month, no lock-in
  • 7-day money-back guarantee
All guides