├── .circleci └── config.yml ├── LICENSE ├── README.md ├── redis-look ├── Dockerfile ├── entrypoint.sh └── redis.conf ├── redis-sentinel ├── Dockerfile ├── sentinel-entrypoint.sh └── sentinel.conf ├── redis-utils ├── Dockerfile └── entrypoint.sh └── scripts ├── bootstrap.sh ├── build.sh ├── check_scaling.sh ├── cleanup.sh ├── docker-compose.yml ├── test_init.sh └── test_scale.sh /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | docker: 5 | - image: docker:18.06.1-ce-git 6 | steps: 7 | - checkout 8 | - setup_remote_docker: 9 | version: 18.06.0-ce 10 | - run: 11 | name: Build Images 12 | command: | 13 | sh scripts/build.sh $CIRCLE_BRANCH 14 | - run: 15 | name: Bootstrap tests 16 | command: | 17 | docker swarm init 18 | docker network create --attachable --driver overlay redis 19 | sh scripts/bootstrap.sh $CIRCLE_BRANCH 20 | - run: 21 | name: Test init 22 | command: | 23 | sh scripts/test_init.sh 24 | - run: 25 | name: Test Scale 26 | command: | 27 | sh scripts/test_scale.sh $CIRCLE_BRANCH 28 | - run: 29 | name: Teardown 30 | command: | 31 | sh scripts/cleanup.sh $CIRCLE_BRANCH 32 | docker network rm redis 33 | when: always 34 | - deploy: 35 | name: Push to Docker Hub 36 | command: | 37 | if [ "${CIRCLE_BRANCH}" == "master" ] && [ -z "$CIRCLE_PULL_REQUEST" ]; then 38 | docker login -u="${DOCKER_USERNAME}" -p="${DOCKER_PASSWORD}" 39 | look_redis_v=$(grep "FROM redis" redis-look/Dockerfile | cut -d ":" -f2 | cut -d "-" -f1) 40 | sentinel_redis_v=$(grep "FROM redis" redis-sentinel/Dockerfile | cut -d ":" -f2 | cut -d "-" -f1) 41 | utils_redis_v=$(grep "FROM redis" redis-utils/Dockerfile | cut -d ":" -f2 | cut -d "-" -f1) 42 | 43 | look_image_v=$(grep "LABEL version" redis-look/Dockerfile | cut -d "=" -f2 | tr -d '"') 44 | sentinel_image_v=$(grep "LABEL version" redis-sentinel/Dockerfile | cut -d "=" -f2 | tr -d '"') 45 | utils_image_v=$(grep "LABEL version" redis-utils/Dockerfile | cut -d "=" -f2 | tr -d '"') 46 | 47 | LOOK_VERSION="${look_image_v}-redis-${look_redis_v}" 48 | SENTINEL_VERSION="${sentinel_image_v}-redis-${sentinel_redis_v}" 49 | UTILS_VERSION="${utils_image_v}-redis-${utils_redis_v}" 50 | 51 | docker tag thomasjpfan/redis-look:$CIRCLE_BRANCH \ 52 | thomasjpfan/redis-look:$LOOK_VERSION 53 | docker tag thomasjpfan/redis-sentinel:$CIRCLE_BRANCH \ 54 | thomasjpfan/redis-sentinel:$SENTINEL_VERSION 55 | docker tag thomasjpfan/redis-utils:$CIRCLE_BRANCH \ 56 | thomasjpfan/redis-utils:$UTILS_VERSION 57 | 58 | docker push thomasjpfan/redis-look:$LOOK_VERSION 59 | docker push thomasjpfan/redis-sentinel:$SENTINEL_VERSION 60 | docker push thomasjpfan/redis-utils:$UTILS_VERSION 61 | 62 | docker tag thomasjpfan/redis-look:$CIRCLE_BRANCH \ 63 | thomasjpfan/redis-look:latest 64 | docker tag thomasjpfan/redis-sentinel:$CIRCLE_BRANCH \ 65 | thomasjpfan/redis-sentinel:latest 66 | docker tag thomasjpfan/redis-utils:$CIRCLE_BRANCH \ 67 | thomasjpfan/redis-utils:latest 68 | 69 | docker push thomasjpfan/redis-look:latest 70 | docker push thomasjpfan/redis-sentinel:latest 71 | docker push thomasjpfan/redis-utils:latest 72 | fi 73 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Thomas Fan 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Cluster Cache for Docker Swarm 2 | 3 | [![CircleCI](https://circleci.com/gh/thomasjpfan/redis-cluster-docker-swarm/tree/master.svg?style=svg)](https://circleci.com/gh/thomasjpfan/redis-cluster-docker-swarm/tree/master) 4 | 5 | Quick and dirty Redis cluster taking advantage of Redis Sentinel for automatic failover. Persistence is turned off by default. 6 | 7 | ## Usage 8 | 9 | 0. Setup docker swarm 10 | 1. Create a overlay network: 11 | 12 | ```bash 13 | docker network create --attachable --driver overlay redis 14 | ``` 15 | 16 | 2. Modify scripts/docker-compose.yml to how you want to deploy the stack. 17 | 3. Run `scripts/bootstrap.sh`. 18 | 19 | ```bash 20 | bash scripts/bootstrap.sh latest 21 | ``` 22 | 23 | 4. Connect to with redis-cli 24 | 25 | ```bash 26 | docker run --rm --network redis -ti redis:4.0.11-alpine redis-cli -h redis 27 | ``` 28 | 29 | To access the redis cluster outside of docker, port 6379 needs to be expose. This can be done by adding ports to the docker-compose file: 30 | 31 | ```yaml 32 | ... 33 | redis: 34 | image: thomasjpfan/redis-look 35 | ports: 36 | - "6379:6379" 37 | ... 38 | ``` 39 | 40 | ## Details 41 | 42 | A docker service called `redis-zero` is created to serve as the initial master for the redis sentinels to setup. The `redis-look` instances watches the redis sentinels for a master, and connects to `redis-zero` once a master has been decided. Once the dust has settled, remove the `redis-zero` instance and wait for failover to take over so a new redis-master will take over. Use `redis-utils` to reset sentinels so that its metadata is accurate with the correct state. 43 | 44 | The use of `redis-zero` as a bootstrapping step allows for the `docker-compose.yml` to provide only the long running services: 45 | 46 | ```yaml 47 | version: '3.1' 48 | 49 | services: 50 | 51 | redis-sentinel: 52 | image: thomasjpfan/redis-sentinel 53 | environment: 54 | - REDIS_IP=redis-zero 55 | - REDIS_MASTER_NAME=redismaster 56 | deploy: 57 | replicas: 3 58 | networks: 59 | - redis 60 | 61 | redis: 62 | image: thomasjpfan/redis-look 63 | environment: 64 | - REDIS_SENTINEL_IP=redis-sentinel 65 | - REDIS_MASTER_NAME=redismaster 66 | - REDIS_SENTINEL_PORT=26379 67 | deploy: 68 | replicas: 3 69 | networks: 70 | - redis 71 | 72 | networks: 73 | redis: 74 | external: true 75 | 76 | ``` 77 | 78 | ### Scaling 79 | 80 | From now on just scale `redis` to expand the number of slaves or scale `redis-sentinel` to increase the number of sentinels. 81 | -------------------------------------------------------------------------------- /redis-look/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM redis:4.0.11-alpine 2 | 3 | LABEL version="1.0.2" 4 | 5 | ENV REDIS_SENTINEL_IP redis-sentinel 6 | ENV REDIS_MASTER_NAME redismaster 7 | ENV REDIS_SENTINEL_PORT 26379 8 | 9 | COPY entrypoint.sh /usr/local/bin/ 10 | 11 | RUN chmod +x /usr/local/bin/entrypoint.sh 12 | 13 | WORKDIR /redis 14 | COPY redis.conf . 15 | 16 | EXPOSE 26379 17 | 18 | ENTRYPOINT ["entrypoint.sh"] 19 | -------------------------------------------------------------------------------- /redis-look/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | until [ "$(redis-cli -h $REDIS_SENTINEL_IP -p $REDIS_SENTINEL_PORT ping)" = "PONG" ]; do 4 | echo "$REDIS_SENTINEL_IP is unavailable - sleeping" 5 | sleep 1 6 | done 7 | 8 | master_info=$(redis-cli -h $REDIS_SENTINEL_IP -p $REDIS_SENTINEL_PORT sentinel get-master-addr-by-name $REDIS_MASTER_NAME) 9 | 10 | until [ "$master_info" ]; do 11 | echo "$REDIS_MASTER_NAME not found - sleeping" 12 | sleep 1 13 | master_info=$(redis-cli -h $REDIS_SENTINEL_IP -p $REDIS_SENTINEL_PORT sentinel get-master-addr-by-name $REDIS_MASTER_NAME) 14 | done 15 | 16 | master_ip=$(echo $master_info | awk '{print $1}') 17 | master_port=$(echo $master_info | awk '{print $2}') 18 | 19 | network_ips=$(ifconfig | grep inet | awk '{print $2}' | cut -d ':' -f 2) 20 | master_ip_masked=$(echo $master_ip | cut -d '.' -f 1,2,3) 21 | 22 | slave_announce_ip="" 23 | 24 | for network_ip in $network_ips; do 25 | network_ip_masked=$(echo $network_ip | cut -d '.' -f 1,2,3) 26 | if [ "$network_ip_masked" == "$master_ip_masked" ]; then 27 | slave_announce_ip="$network_ip" 28 | break 29 | fi 30 | done 31 | 32 | if [ ! "$slave_announce_ip" ]; then 33 | echo "Unable to resolve network ip" 34 | exit 1 35 | fi 36 | 37 | echo "Slave ip found: $slave_announce_ip" 38 | 39 | sed -i "s/{{ SLAVE_ANNOUNCE_IP }}/$slave_announce_ip/g" /redis/redis.conf 40 | 41 | redis-server /redis/redis.conf --slaveof $master_ip $master_port 42 | -------------------------------------------------------------------------------- /redis-look/redis.conf: -------------------------------------------------------------------------------- 1 | slave-announce-ip {{ SLAVE_ANNOUNCE_IP }} 2 | 3 | min-slaves-to-write 1 4 | min-slaves-max-lag 10 5 | 6 | save "" 7 | -------------------------------------------------------------------------------- /redis-sentinel/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM redis:4.0.11-alpine 2 | 3 | LABEL version="1.0.2" 4 | 5 | ENV SENTINEL_QUORUM=2 \ 6 | SENTINEL_DOWN_AFTER=1000 \ 7 | SENTINEL_FAILOVER=1000 \ 8 | REDIS_MASTER_NAME=redismaster \ 9 | REDIS_MASTER=redis-zero \ 10 | REDIS_SENTINEL_NAME=redis-sentinel 11 | 12 | RUN apk --no-cache add drill 13 | 14 | RUN mkdir -p /redis 15 | 16 | WORKDIR /redis 17 | 18 | COPY sentinel.conf . 19 | COPY sentinel-entrypoint.sh /usr/local/bin/ 20 | 21 | RUN chown redis:redis /redis/* && \ 22 | chmod +x /usr/local/bin/sentinel-entrypoint.sh 23 | 24 | EXPOSE 26379 25 | 26 | ENTRYPOINT ["sentinel-entrypoint.sh"] 27 | -------------------------------------------------------------------------------- /redis-sentinel/sentinel-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sed -i "s/{{ SENTINEL_QUORUM }}/$SENTINEL_QUORUM/g" /redis/sentinel.conf 4 | sed -i "s/{{ SENTINEL_DOWN_AFTER }}/$SENTINEL_DOWN_AFTER/g" /redis/sentinel.conf 5 | sed -i "s/{{ SENTINEL_FAILOVER }}/$SENTINEL_FAILOVER/g" /redis/sentinel.conf 6 | sed -i "s/{{ REDIS_MASTER_NAME }}/$REDIS_MASTER_NAME/g" /redis/sentinel.conf 7 | 8 | sentinel_ips=$(drill tasks.$REDIS_SENTINEL_NAME | grep tasks.$REDIS_SENTINEL_NAME | tail -n +2 | awk '{print $5}') 9 | 10 | for ip in $sentinel_ips; do 11 | master_info=$(redis-cli -h $ip -p 26379 sentinel get-master-addr-by-name $REDIS_MASTER_NAME) 12 | if [ "$master_info" ]; then 13 | REDIS_IP=$(echo $master_info | awk '{print $1}') 14 | break 15 | fi 16 | done 17 | 18 | if [ ! "$master_info" ]; then 19 | until [ "$(redis-cli -h $REDIS_IP -p 6379 ping)" = "PONG" ]; do 20 | echo "$REDIS_IP is unavailable - sleeping" 21 | sleep 1 22 | done 23 | fi 24 | 25 | sed -i "s/{{ REDIS_IP }}/$REDIS_IP/g" /redis/sentinel.conf 26 | redis-server /redis/sentinel.conf --sentinel 27 | -------------------------------------------------------------------------------- /redis-sentinel/sentinel.conf: -------------------------------------------------------------------------------- 1 | port 26379 2 | 3 | dir /tmp 4 | 5 | sentinel monitor {{ REDIS_MASTER_NAME }} {{ REDIS_IP }} 6379 {{ SENTINEL_QUORUM }} 6 | sentinel down-after-milliseconds {{ REDIS_MASTER_NAME }} {{ SENTINEL_DOWN_AFTER }} 7 | sentinel parallel-syncs {{ REDIS_MASTER_NAME }} 1 8 | sentinel failover-timeout {{ REDIS_MASTER_NAME }} {{ SENTINEL_FAILOVER }} 9 | -------------------------------------------------------------------------------- /redis-utils/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM redis:4.0.11-alpine 2 | 3 | LABEL version="1.0.2" 4 | 5 | RUN apk --no-cache add drill 6 | 7 | COPY entrypoint.sh /usr/local/bin/ 8 | 9 | RUN chmod +x /usr/local/bin/entrypoint.sh 10 | 11 | ENTRYPOINT ["entrypoint.sh"] 12 | -------------------------------------------------------------------------------- /redis-utils/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | REDIS_SENTINEL_NAME="$1" 4 | REDIS_MASTER_NAME="$2" 5 | shift 2 6 | 7 | sentinel_ips=$(drill tasks.$REDIS_SENTINEL_NAME | grep tasks.$REDIS_SENTINEL_NAME | tail -n +2 | awk '{print $5}') 8 | until [ "$sentinel_ips" ]; do 9 | sleep 1 10 | sentinel_ips=$(drill tasks.$REDIS_SENTINEL_NAME | grep tasks.$REDIS_SENTINEL_NAME | tail -n +2 | awk '{print $5}') 11 | done 12 | 13 | get_value() { 14 | ip="$1" 15 | port="$2" 16 | master_name="$3" 17 | key="$4" 18 | echo $(redis-cli -h $ip -p $port sentinel master $master_name | grep -A 1 $key | tail -n1) 19 | } 20 | 21 | case ${1} in 22 | reset) 23 | shift 1 24 | key="$1" 25 | target="$2" 26 | for ip in $sentinel_ips; do 27 | echo "Reseting sentinel for ip: ${ip}" 28 | redis-cli -h $ip -p 26379 sentinel reset $REDIS_MASTER_NAME 29 | until [ "$(get_value $ip 26379 $REDIS_MASTER_NAME $key)" = "$target" ]; do 30 | echo "$key not equal to $target - sleeping" 31 | sleep 2 32 | done 33 | done 34 | ;; 35 | value) 36 | shift 1 37 | key="$1" 38 | first_ip=$(echo $sentinel_ips | cut -d " " -f 1) 39 | value=$(get_value $first_ip 26379 $REDIS_MASTER_NAME $key) 40 | for ip in $sentinel_ips; do 41 | k_value="$(get_value $ip 26379 $REDIS_MASTER_NAME $key)" 42 | if [ "$k_value" != "$value" ]; then 43 | echo "-1" 44 | exit 1 45 | fi 46 | done 47 | echo "$value" 48 | ;; 49 | show) 50 | shift 1 51 | key="$1" 52 | for ip in $sentinel_ips; do 53 | k_value="$(get_value $ip 26379 $REDIS_MASTER_NAME $key)" 54 | echo "${ip}: ${k_value}" 55 | done 56 | ;; 57 | esac 58 | -------------------------------------------------------------------------------- /scripts/bootstrap.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export TAG=${1:-"latest"} 6 | 7 | NUM_OF_SENTINELS=3 8 | NUM_OF_REDIS=3 9 | REDIS_SENTINEL_NAME="redis-sentinel" 10 | REDIS_MASTER_NAME="redismaster" 11 | 12 | echo "Starting redis-zero" 13 | docker service create --network redis --name redis-zero redis:4.0.11-alpine 14 | 15 | echo "Starting services" 16 | docker stack deploy -c scripts/docker-compose.yml cache 17 | 18 | until [ "$(docker run --rm --network redis thomasjpfan/redis-utils:$TAG \ 19 | $REDIS_SENTINEL_NAME $REDIS_MASTER_NAME \ 20 | value num-other-sentinels)" = "$((NUM_OF_SENTINELS - 1))" ]; do 21 | echo "Sentinels not set up yet - sleeping" 22 | sleep 2 23 | done 24 | 25 | until [ "$(docker run --rm --network redis thomasjpfan/redis-utils:$TAG \ 26 | $REDIS_SENTINEL_NAME $REDIS_MASTER_NAME \ 27 | value "num-slaves")" = "$NUM_OF_REDIS" ]; do 28 | echo "Slaves not set up yet - sleeping" 29 | sleep 2 30 | done 31 | 32 | old_master=$(docker run --rm --network redis thomasjpfan/redis-utils:$TAG \ 33 | $REDIS_SENTINEL_NAME $REDIS_MASTER_NAME value ip) 34 | echo "redis-zero ip is ${old_master}" 35 | 36 | echo "Removing redis-zero" 37 | docker service rm redis-zero 38 | 39 | until [ "$(docker run --rm --network redis thomasjpfan/redis-utils:$TAG \ 40 | $REDIS_SENTINEL_NAME $REDIS_MASTER_NAME value ip)" != "$old_master" ]; do 41 | echo "Failover did not happen yet - sleeping" 42 | sleep 2 43 | done 44 | 45 | echo "Make sure the number of slaves are set" 46 | docker run --rm --network redis thomasjpfan/redis-utils:$TAG \ 47 | $REDIS_SENTINEL_NAME $REDIS_MASTER_NAME reset "num-slaves" "$((NUM_OF_REDIS - 1))" 48 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | TAG=${1:-"latest"} 6 | 7 | echo "Building redis-look" 8 | docker build -t thomasjpfan/redis-look:$TAG redis-look 9 | 10 | echo "Building redis-sentinel" 11 | docker build -t thomasjpfan/redis-sentinel:$TAG redis-sentinel 12 | 13 | echo "Building redis-utils" 14 | docker build -t thomasjpfan/redis-utils:$TAG redis-utils 15 | -------------------------------------------------------------------------------- /scripts/check_scaling.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | sentinels="$1" 6 | slaves="$2" 7 | HOST="-h redis-sentinel -p 26379" 8 | 9 | echo "Able to connect to sentinel" 10 | redis-cli $HOST ping 11 | 12 | echo "Getting redismaster" 13 | redis-cli $HOST sentinel get-master-addr-by-name redismaster 14 | 15 | echo "Make sure there are 2 other sentinels" 16 | master_info=$(redis-cli $HOST sentinel master redismaster) 17 | echo $master_info | grep "num-other-sentinels $sentinels" 18 | 19 | echo "Make sure there are 2 slaves" 20 | echo $master_info | grep "num-slaves $slaves" 21 | -------------------------------------------------------------------------------- /scripts/cleanup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | export TAG=${1:-"latest"} 6 | 7 | docker stack rm cache 8 | -------------------------------------------------------------------------------- /scripts/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.1' 2 | 3 | 4 | services: 5 | 6 | redis-sentinel: 7 | image: thomasjpfan/redis-sentinel:${TAG:-latest} 8 | environment: 9 | - REDIS_IP=redis-zero 10 | - REDIS_MASTER_NAME=redismaster 11 | deploy: 12 | replicas: 3 13 | networks: 14 | - redis 15 | 16 | redis: 17 | image: thomasjpfan/redis-look:${TAG:-latest} 18 | environment: 19 | - REDIS_SENTINEL_IP=redis-sentinel 20 | - REDIS_MASTER_NAME=redismaster 21 | - REDIS_SENTINEL_PORT=26379 22 | deploy: 23 | replicas: 3 24 | networks: 25 | - redis 26 | 27 | networks: 28 | redis: 29 | external: true 30 | -------------------------------------------------------------------------------- /scripts/test_init.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | echo "Create stand in volume for scripts" 6 | docker create -v /scripts --rm --name scripts alpine:3.6 /bin/true 7 | 8 | echo "Moving check_scaling.sh to scripts" 9 | docker cp scripts scripts:/ 10 | 11 | echo "Starting init tests" 12 | docker run --rm --network redis --volumes-from scripts \ 13 | redis:4.0.11-alpine sh /scripts/check_scaling.sh 2 2 14 | -------------------------------------------------------------------------------- /scripts/test_scale.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | REDIS_SENTINEL_NAME="redis-sentinel" 6 | REDIS_MASTER_NAME="redismaster" 7 | export TAG=${1:-"latest"} 8 | 9 | echo "Scale redis" 10 | docker service scale cache_redis-sentinel=5 11 | 12 | until [ "$(docker run --rm --network redis thomasjpfan/redis-utils:$TAG \ 13 | $REDIS_SENTINEL_NAME $REDIS_MASTER_NAME value num-other-sentinels)" = "4" ]; do 14 | echo "Sentinels not set up yet - sleeping" 15 | sleep 2 16 | done 17 | 18 | docker service scale cache_redis=5 19 | 20 | echo "Make sure the number of slaves are set" 21 | docker run --rm --network redis thomasjpfan/redis-utils:$TAG \ 22 | $REDIS_SENTINEL_NAME $REDIS_MASTER_NAME reset "num-slaves" 4 23 | 24 | echo "Starting following tests" 25 | docker run --rm --network redis --volumes-from scripts \ 26 | redis:4.0.11-alpine sh /scripts/check_scaling.sh 4 4 27 | --------------------------------------------------------------------------------