Migrating from Redis to Valkey can significantly reduce costs associated with private licensing for your application. However, the biggest concern in any migration is transitioning without disrupting service or causing downtime. In this blog post, we’ll guide you through a step-by-step approach to smoothly migrate from Redis to Valkey without interrupting your application’s operations, providing the best practices to ensure a seamless switch while keeping your systems running.
We will work with a three-node replica set with auto-failover using redis-server and redis-sentinel and their Valkey alternatives running on Docker. We will migrate using the same version, 7.2. Here is a compatibility matrix from Valkey’s official documentation.
Migration compatibility matrix
You can migrate a Redis server to Valkey. The following table provides migration options depending on the Redis version you run:
Redis Valkey
6.x 7.2.x
7.2.x 7.2.x
7.2.x 8.0.x
7.4 n/a
We will use port 600X for our data nodes and 2600X for our sentinel nodes. Check those ports are available before running the snippet, and change them if they are already used on your system.
The next command should return empty if the ports are available.
1 |
sudo ss -tunlp | grep -E '6000|6001|6002|26000|26001|26002' |
To quickly create a test environment, run this snippet to create directories, configuration files, and ACL files and run the containers with all the required parameters.
Our data, config, and logs directory for each node are created as follows.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
/node-0 ├── config │ ├── redis.conf │ ├── sentinel.conf │ └── users.acl ├── data └── logs ├── redis.log └── sentinel.log /node-1 ├── config │ ├── redis.conf │ ├── sentinel.conf │ └── users.acl ├── data └── logs ├── redis.log └── sentinel.log /node-2 ├── config │ ├── redis.conf │ ├── sentinel.conf │ └── users.acl ├── data └── logs ├── redis.log └── sentinel.log |
The snippet.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 |
# create directories mkdir -p $HOME/node-{0..2}/{config,data,logs} # config # nodes cat <<EOF > $HOME/node-0/config/redis.conf port 6000 bind 0.0.0.0 logfile /logs/redis.log dir /data aclfile /config/users.acl masteruser replica-user masterauth somepassword EOF cat <<EOF > $HOME/node-1/config/redis.conf port 6001 bind 0.0.0.0 logfile /logs/redis.log dir /data aclfile /config/users.acl masteruser replica-user masterauth somepassword replicaof 127.0.0.1 6000 EOF cat <<EOF > $HOME/node-2/config/redis.conf port 6002 bind 0.0.0.0 logfile /logs/redis.log dir /data aclfile /config/users.acl masteruser replica-user masterauth somepassword replicaof 127.0.0.1 6000 EOF # sentinels cat <<EOF > $HOME/node-0/config/sentinel.conf port 26000 logfile /logs/sentinel.log dir /data sentinel monitor mymaster 127.0.0.1 6000 2 sentinel auth-user mymaster sentinel-user sentinel auth-pass mymaster somepassword user default on >password ~* &* +@all EOF cat <<EOF > $HOME/node-1/config/sentinel.conf port 26001 logfile /logs/sentinel.log dir /data sentinel monitor mymaster 127.0.0.1 6000 2 sentinel auth-user mymaster sentinel-user sentinel auth-pass mymaster somepassword user default on >password ~* &* +@all EOF cat <<EOF > $HOME/node-2/config/sentinel.conf port 26002 logfile /logs/sentinel.log dir /data sentinel monitor mymaster 127.0.0.1 6000 2 sentinel auth-user mymaster sentinel-user sentinel auth-pass mymaster somepassword user default on >password ~* &* +@all EOF # acl cat <<EOF > $HOME/node-0/config/users.acl user default on >password ~* &* +@all user replica-user on >somepassword -@all +psync +replconf +ping user sentinel-user on >somepassword allchannels -@all +multi +slaveof +ping +exec +subscribe +config|rewrite +role +publish +info +client|setname +client|kill +script|kill EOF cat <<EOF > $HOME/node-1/config/users.acl user default on >password ~* &* +@all user replica-user on >somepassword -@all +psync +replconf +ping user sentinel-user on >somepassword allchannels -@all +multi +slaveof +ping +exec +subscribe +config|rewrite +role +publish +info +client|setname +client|kill +script|kill EOF cat <<EOF > $HOME/node-2/config/users.acl user default on >password ~* &* +@all user replica-user on >somepassword -@all +psync +replconf +ping user sentinel-user on >somepassword allchannels -@all +multi +slaveof +ping +exec +subscribe +config|rewrite +role +publish +info +client|setname +client|kill +script|kill EOF # Set nonrestrictive permissions to ensure the docker users can access the local directories. sudo chmod -R 777 $HOME/node-{0..2} # redis docker run -d --restart always --name redis-0 --net host -v $HOME/node-0/data:/data -v $HOME/node-0/logs:/logs -v $HOME/node-0/config:/config redis:7.2 redis-server /config/redis.conf docker run -d --restart always --name redis-1 --net host -v $HOME/node-1/data:/data -v $HOME/node-1/logs:/logs -v $HOME/node-1/config:/config redis:7.2 redis-server /config/redis.conf docker run -d --restart always --name redis-2 --net host -v $HOME/node-2/data:/data -v $HOME/node-2/logs:/logs -v $HOME/node-2/config:/config redis:7.2 redis-server /config/redis.conf # redis-sentinel docker run -d --restart always --name redis-sentinel-0 --net host -v $HOME/node-0/data:/data -v $HOME/node-0/logs:/logs -v $HOME/node-0/config:/config redis:7.2 redis-sentinel /config/sentinel.conf docker run -d --restart always --name redis-sentinel-1 --net host -v $HOME/node-1/data:/data -v $HOME/node-1/logs:/logs -v $HOME/node-1/config:/config redis:7.2 redis-sentinel /config/sentinel.conf docker run -d --restart always --name redis-sentinel-2 --net host -v $HOME/node-2/data:/data -v $HOME/node-2/logs:/logs -v $HOME/node-2/config:/config redis:7.2 redis-sentinel /config/sentinel.conf |
Once you run the snippet, you should have the following containers running.
1 2 3 4 5 6 7 8 |
docker ps --format '{{.Image}} {{.Status}} {{.Names}}' redis:7.2 Up 2 minutes redis-sentinel-2 redis:7.2 Up 2 minutes redis-sentinel-1 redis:7.2 Up 2 minutes redis-sentinel-0 redis:7.2 Up 2 minutes redis-2 redis:7.2 Up 2 minutes redis-1 redis:7.2 Up 2 minutes redis-0 |
Once we have our cluster ready, we will insert 10K keys with random values.
1 2 3 |
for i in {1..10000};do echo "SET key${i} '$(openssl rand -base64 48 | xargs echo)'" >> /tmp/set_keys done |
Now that we created all the SET statements we can execute them in bulk using the --pipe option.
1 2 3 4 |
cat /tmp/set_keys | docker exec -i redis-0 redis-cli -p 6000 –-pass password –-pipe All data transferred. Waiting for the last reply... Last reply received from server. errors: 0, replies: 10000 |
We can check the number of keys with the INFO keyspace command.
1 2 3 |
docker exec -it redis-0 redis-cli -p 6000 --pass password INFO keyspace # Keyspace db0:keys=10000,expires=0,avg_ttl=0 |
The migration process involves migrating one node at a time. We will start by migrating the sentinel nodes. After every container is migrated, it is a good practice to check the logs and status of the node to be able to roll back in case anything goes wrong.
You can also wait a day or more before proceeding to the next node to ensure everything works properly.
We will reuse the same configuration, log, and ACL files and directories.
The changes we are making are the container name prefix from redis to valkey, the container image from redis:7.2 to valkey/valkey:7.2, and the command run by the container to match the proper binary. We will highlight the differences in bold text for the first command, and all following commands will have the exact differences.
Let’s begin by migrating the first redis-sentinel container.
1 2 3 4 5 6 7 8 9 10 11 |
# stop redis-sentinel-2 docker stop redis-sentinel-2 # start valkey-sentinel-2 docker run -d --restart always --name valkey-sentinel-2 --net host -v $HOME/node-2/data:/data -v $HOME/node-2/logs:/logs -v $HOME/node-2/config:/config valkey/valkey:7.2 valkey-sentinel /config/sentinel.conf |
You can tail the log file and use the INFO command to check the status.
1 2 3 4 5 6 |
sudo tail $HOME/node-2/logs/sentinel.log 1:X 14 Oct 2024 17:54:54.750 * User requested shutdown... 1:X 14 Oct 2024 17:54:54.750 # Sentinel is now ready to exit, bye bye... 1:X 14 Oct 2024 17:54:57.199 * oO0OoO0OoO0Oo Valkey is starting oO0OoO0OoO0Oo ... 1:X 14 Oct 2024 17:54:57.200 # +monitor master mymaster 127.0.0.1 6000 quorum 2 |
We can also check the node with the INFO server sentinel command.
1 2 3 4 5 6 7 8 9 10 11 |
docker exec -it valkey-sentinel-2 valkey-cli -p 26002 --user default --pass password INFO server sentinel # Server redis_version:7.2.4 server_name:valkey valkey_version:7.2.7 ... # Sentinel ... master0:name=mymaster,status=ok,address=127.0.0.1:6000,slaves=2,sentinels=3 |
Then, we need to do the same for all other sentinels.
redis-sentinel-1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
# stop the node redis-sentinel-1 docker stop redis-sentinel-1 # Start the node valkey-sentinel-1 docker run -d --restart always --name valkey-sentinel-1 --net host -v $HOME/node-1/data:/data -v $HOME/node-1/logs:/logs -v $HOME/node-1/config:/config valkey/valkey:7.2 valkey-sentinel /config/sentinel.conf # check logs sudo tail $HOME/node-1/logs/sentinel.log # check sentinel and server info docker exec -it valkey-sentinel-1 valkey-cli -p 26001 --user default --pass password INFO server sentinel # stop the node redis-sentinel-0 docker stop redis-sentinel-0 |
redis-sentinel-0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
# stop the node redis-sentinel-0 docker stop redis-sentinel-0 # Start the node valkey-sentinel-0 docker run -d --restart always --name valkey-sentinel-0 --net host -v $HOME/node-0/data:/data -v $HOME/node-0/logs:/logs -v $HOME/node-0/config:/config valkey/valkey:7.2 valkey-sentinel /config/sentinel.conf # check logs sudo tail $HOME/node-0/logs/sentinel.log # check sentinel and server info docker exec -it valkey-sentinel-0 valkey-cli -p 26000 --user default --pass password INFO server sentinel |
Once all our sentinel nodes are running Valkey, we can continue migrating the redis-server nodes, starting with the replicas, to avoid downtime.
You can run this command to identify the current replicas.
1 2 3 4 5 |
docker exec -it redis-0 redis-cli -p 6000 --user default --pass password INFO replication | grep slave connected_slaves:2 slave0:ip=127.0.0.1,port=6001,state=online,offset=664355,lag=0 slave1:ip=127.0.0.1,port=6002,state=online,offset=664355,lag=1 |
We will start with node redis-2.
1 2 3 4 5 6 7 8 9 10 11 |
# stop redis-2 docker stop redis-2 # start valkey-2 docker run -d --restart always --name valkey-2 --net host -v $HOME/node-2/data:/data -v $HOME/node-2/logs:/logs -v $HOME/node-2/config:/config valkey/valkey:7.2 valkey-server /config/redis.conf |
Always check the logs, server, and replication information to ensure everything works.
When we stop a Redis/Valkey data node, the shutdown process saves the node’s state into an RDB file. This allows the server to recover from the disk when starting up again, so the node doesn’t need to do a full sync, which can take time in a big production deployment.
If you have a write-heavy cluster or you have one replica down for a long time, you might want to tune the repl-backlog-size configuration parameter.
The backlog serves as a buffer that holds replica data during periods of disconnection. This enables replicas to catch up with a partial resync, receiving only the data they missed rather than requiring a full resync.
Let’s analyze the log to understand the process better.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
... # The node received a shutdown signal 1:signal-handler (1729281536) Received SIGTERM scheduling shutdown... 1:S 18 Oct 2024 19:58:56.788 * User requested shutdown... # The server starts saving the current state into an RDB file and then exits 1:S 18 Oct 2024 19:58:56.788 * Saving the final RDB snapshot before exiting. 1:S 18 Oct 2024 19:58:56.818 * DB saved on disk 1:S 18 Oct 2024 19:58:56.818 # Redis is now ready to exit, bye bye… ... # Server started again as Valkey 1:C 14 Oct 2024 18:03:37.533 * oO0OoO0OoO0Oo Valkey is starting oO0OoO0OoO0Oo 1:C 14 Oct 2024 18:03:37.533 * Valkey version=7.2.7, bits=64, commit=00000000, modified=0, pid=1, just started ... 1:S 18 Oct 2024 19:59:06.979 * Server initialized ... # The node load the RDB file into memory to reach the state it had before the shutdown 1:S 18 Oct 2024 19:59:06.979 * Loading RDB produced by Redis version 7.2.6 ... 1:S 18 Oct 2024 19:59:06.984 * Done loading RDB, keys loaded: 10000, keys expired: 0. 1:S 18 Oct 2024 19:59:06.984 * DB loaded from disk: 0.006 seconds ... # The node starts replicating from the master again after a partial resync 1:S 14 Oct 2024 18:03:37.535 * Connecting to MASTER 127.0.0.1:6000 1:S 14 Oct 2024 18:03:37.535 * MASTER <-> REPLICA sync started … 1:S 14 Oct 2024 18:03:37.535 * MASTER <-> REPLICA sync: Master accepted a Partial Resynchronization. |
We can also run the INFO command to check the version, status, and number of keys.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
docker exec -it valkey-2 valkey-cli -p 6002 --user default --pass password INFO server replication keyspace # Server redis_version:7.2.4 server_name:valkey valkey_version:7.2.7 ... # Replication role:slave master_host:127.0.0.1 master_port:6000 master_link_status:up ... # Keyspace db0:keys=10000,expires=0,avg_ttl=0 |
We will do the same for node redis-1, the other replica.
Remember to check the logs and output of the INFO command.
The last step is to migrate the current master. First, we need to trigger a manual switchover so that the current master becomes a slave.
Connect to a sentinel node, authenticate, and run the SENTINEL FAILOVER command. Then, run the SENTINEL GET-MASTER-ADDR-BY-NAME command to ensure that a new master gets elected.
1 2 3 4 5 6 |
docker exec -it valkey-sentinel-0 redis-cli -p 26000 --user default --pass password 127.0.0.1:26000> SENTINEL FAILOVER mymaster OK 127.0.0.1:26000> SENTINEL GET-MASTER-ADDR-BY-NAME mymaster 1) "127.0.0.1" 2) "6002" |
Now, we can exit and migrate node redis-0.
1 2 3 4 5 6 7 8 9 10 11 |
# stop redis-0 docker stop redis-0 # start valkey-0 docker run -d --restart always --name valkey-0 --net host -v $HOME/node-0/data:/data -v $HOME/node-0/logs:/logs -v $HOME/node-0/config:/config valkey/valkey:7.2 valkey-server /config/redis.conf |
Again, check the logs and INFO for the new node to ensure everything works properly.
As we haven’t deleted the redis-server and redis-sentinel nodes, it is pretty simple to do a rollback by just stopping the Valkey containers and starting the Redis ones. In case this is needed, do it in a rolling way, starting with the replicas.
Please refer to this documentation for more information about migrating from Redis to Valkey or learn more about our support for the Valkey project and its community.
Thanks for that very interesting post.
I tested a migration from Redis 6.2.14 to Valkey 8.0.1 without any issue.
Do you confirm that ?
Thanks for letting us know. Valkey 8 wasn’t released when I created this post and upgrading several major versions is not fully tested yet. It’s great to know it worked for you.
Stay tuned for future Valkey posts.
Cheers!