diff --git a/data/Dockerfiles/valkeymigrator/Dockerfile b/data/Dockerfiles/valkeymigrator/Dockerfile new file mode 100644 index 000000000..798bb8d5e --- /dev/null +++ b/data/Dockerfiles/valkeymigrator/Dockerfile @@ -0,0 +1,8 @@ +FROM python:3.13.2-alpine3.21 + +WORKDIR /app + +COPY migrate.py /app/migrate.py +RUN pip install --no-cache-dir redis + +CMD ["python", "/app/migrate.py"] diff --git a/data/Dockerfiles/valkeymigrator/migrate.py b/data/Dockerfiles/valkeymigrator/migrate.py new file mode 100644 index 000000000..f5a76331b --- /dev/null +++ b/data/Dockerfiles/valkeymigrator/migrate.py @@ -0,0 +1,78 @@ +import subprocess +import redis +import time +import os + +# Container names +SOURCE_CONTAINER = "redis-old-mailcow" +DEST_CONTAINER = "valkey-mailcow" +VALKEYPASS = os.getenv("VALKEYPASS") + + +def migrate_redis(): + src_redis = redis.StrictRedis(host=SOURCE_CONTAINER, port=6379, db=0, password=VALKEYPASS, decode_responses=False) + dest_redis = redis.StrictRedis(host=DEST_CONTAINER, port=6379, db=0, password=VALKEYPASS, decode_responses=False) + + cursor = 0 + batch_size = 100 + migrated_count = 0 + + print("Starting migration...") + + while True: + cursor, keys = src_redis.scan(cursor=cursor, match="*", count=batch_size) + keys_to_migrate = [key for key in keys if not key.startswith(b"PHPREDIS_SESSION:")] + + for key in keys_to_migrate: + key_type = src_redis.type(key) + print(f"Import {key} of type {key_type}") + + if key_type == b"string": + value = src_redis.get(key) + dest_redis.set(key, value) + + elif key_type == b"hash": + value = src_redis.hgetall(key) + dest_redis.hset(key, mapping=value) + + elif key_type == b"list": + value = src_redis.lrange(key, 0, -1) + for v in value: + dest_redis.rpush(key, v) + + elif key_type == b"set": + value = src_redis.smembers(key) + for v in value: + dest_redis.sadd(key, v) + + elif key_type == b"zset": + value = src_redis.zrange(key, 0, -1, withscores=True) + for v, score in value: + dest_redis.zadd(key, {v: score}) + + # Preserve TTL if exists + ttl = src_redis.ttl(key) + if ttl > 0: + dest_redis.expire(key, ttl) + + migrated_count += 1 + + if cursor == 0: + break # No more keys to scan + + print(f"Migration completed! {migrated_count} keys migrated.") + + print("Forcing Valkey to save data...") + try: + dest_redis.save() # Immediate RDB save (blocking) + dest_redis.bgrewriteaof() # Rewrites the AOF file in the background + print("Data successfully saved to disk.") + except Exception as e: + print(f"Failed to save data: {e}") + +# Main script execution +if __name__ == "__main__": + try: + migrate_redis() + finally: + pass diff --git a/helper-scripts/redis-to-valkey.sh b/helper-scripts/redis-to-valkey.sh new file mode 100755 index 000000000..ab9fb6b64 --- /dev/null +++ b/helper-scripts/redis-to-valkey.sh @@ -0,0 +1,52 @@ +#!/bin/bash + +SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) +source "${SCRIPT_DIR}/../mailcow.conf" + +VOLUME="${COMPOSE_PROJECT_NAME}_redis-vol-1" +if ! docker volume inspect "$VOLUME" &>/dev/null; then + echo "Error: Docker volume '$VOLUME' does not exist. Nothing to migrate." + exit 1 +fi + +read -p "Do you want to proceed with the migration of your old redis data to valkey? (y/n) " CONFIRM +if [[ ! "$CONFIRM" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + echo "Migration aborted." + exit 0 +fi + +# Run the old Redis container +docker run -d --name redis-old-mailcow \ + --restart always \ + --network ${COMPOSE_PROJECT_NAME}_mailcow-network \ + --hostname redis-old \ + --volume ${VOLUME}:/data/ \ + --volume ${SCRIPT_DIR}/../data/conf/valkey/valkey-conf.sh:/valkey-conf.sh:z \ + --entrypoint "/bin/sh" \ + -e VALKEYPASS="${VALKEYPASS}" \ + redis:7.4.2-alpine -c "/valkey-conf.sh && redis-server /valkey.conf" + + +# Wait for old Redis to be ready +echo "Waiting for redis-old-mailcow to be ready..." +until docker exec redis-old-mailcow redis-cli -a "$VALKEYPASS" ping | grep -q "PONG"; do + echo "Redis not ready yet..." + sleep 2 +done +echo "redis-old-mailcow is ready!" + +# Run the migrate container +docker run --rm --name valkeymigrator-mailcow \ + --network ${COMPOSE_PROJECT_NAME}_mailcow-network \ + -e VALKEYPASS="${VALKEYPASS}" \ + mailcow/valkeymigrator:0.1 + +echo "Migration completed!" +docker stop redis-old-mailcow +docker rm redis-old-mailcow + +read -p "Do you want to delete the old Redis volume? (y/n) " CONFIRM +if [[ "$CONFIRM" =~ ^([yY][eE][sS]|[yY])+$ ]]; then + docker volume rm "$VOLUME" + echo "Docker volume '$VOLUME' has been deleted." +fi