Docker 集群化部署

Docker 集群化部署

九月 12, 2025 次阅读

在当今云计算与微服务架构盛行的时代,集群化已经成为应用部署与运维的核心诉求。而Docker的出现,无疑为集群化的发展注入了强劲动力。它以轻量化的容器技术为基础,彻底改变了应用的交付方式,让部署环境更加一致、迁移过程更加高效,也让大规模调度与弹性伸缩成为可能。从最初的镜像与容器,到原生的Swarm集群,再到与Kubernetes生态的深度结合,Docker不仅提供了标准化的运行环境,更推动了整个分布式系统的演进。可以说,Docker在集群化道路上的贡献,不仅是工具层面的革新,更是理念上的一次飞跃。

下面,我将分别通过 Mysql 的主从集群和 Redis 的集群架构,来介绍 Docker 集群化部署的相关内容。

Docker 部署 Mysql 主从集群

Mysql 主从集群简介

在介绍具体过程之前,我们先来了解一下如何在 docker-compose.yml 文件中使用 build 选项来编译镜像

build 选项编译镜像有两种格式:

services:
  # 格式一
  frontend:
    image: awesome/webapp
    build: ./webapp
  # 格式二
  backend:
    image: awesome/database
    build:
      # 构建上下文目录
      context: ./backend
      # Dockerfile 文件路径
      dockerfile: ./backed.Dockerfile

两种格式的区别在于,第一种格式只能指定构建上下文目录,而第二种格式可以同时指定构建上下文目录和 Dockerfile 文件路径。

也就是说,第一种格式只能用于 Dockerfile 文件名为 Dockerfile 的情况,而第二种格式可以用于 Dockerfile 文件名不是 Dockerfile 的情况。

MySQL 的主从同步有多种方式,这里我们使用最常见的基于二进制日志(binlog)的异步复制方式。主节点负责写操作,并将数据变更记录到二进制日志中;从节点则通过读取主节点的二进制日志来同步数据。而这些配置的实现只需要我们对 MySQL 的配置文件进行一些修改即可。

什么是二进制日志(binlog)?
二进制日志是 MySQL 用于记录所有更改数据库数据的操作的日志文件。它以二进制格式存储,包含了所有的 INSERT、UPDATE、DELETE 等数据修改语句。二进制日志对于数据恢复、复制和审计等功能非常重要。

具体实现步骤

首先,我们需要分别给主从节点创建属于他们的专属目录 masterslave

然后,分别在 masterslave 目录下创建 Dockerfile-masterDockerfile-slave 文件,内容如下:

# Dockerfile-master
FROM mysql:8.0.41
# 设置时区为上海
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime

# Dockerfile-slave
FROM mysql:8.0.41
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
# 设置初始化脚本
COPY ./slave/slave.sql /docker-entrypoint-initdb.d

接下来我们实现从库的配置脚本 slave.sql,使得从库能够连接到主库并进行数据同步,内容如下:

change master to master_host=`mysql-master`,
master_user='root',master_password='root',master_port=3306;
start slave;
# 查看从库状态
show slave status\G;

接下来我们进入主目录 mysql_cluster,创建 docker-compose.yml 文件,内容如下:

services:
  mysql-master:
    build:
      context: ./master
      dockerfile: Dockerfile-master
    image: mysql-master:v1.0
    # restart 用于设置容器的重启策略,always 表示无论容器退出状态如何,都会自动重启容器
    restart: always
    container_name: mysql-master
    volumes:
      - ./master/var/lib/mysql:/var/lib/mysql
    ports:
      - "9306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_ROOT_HOST: '%'
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      timeout: 10s
      retries: 5
    command: ['--server-id=1',
              '--log-bin=mysql-bin',
              '--binlog-ignore-db=mysql',
              '--binlog_cache_size=256M',
              '--binlog_format=ROW',
              '--lower_case_table_names=1',
              '--character-set-server=utf8mb4',
              '--collation-server=utf8mb4_general_ci']
  
  mysql-slave1:
    build:
      context: ./slave
      dockerfile: Dockerfile-slave
    image: mysql-slave:v1.0
    restart: always
    container_name: mysql-slave1
    volumes:
      - ./slave/var/lib/mysql:/var/lib/mysql
    ports:
      - "9307:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      timeout: 10s
      retries: 5
    command: ['--server-id=2',
              '--relay_log=slave-relay',
              '--lower_case_table_names=1',
              '--character-set-server=utf8mb4',
              '--collation-server=utf8mb4_general_ci']
    depends_on:
      mysql-master:
        condition: service_healthy
  mysql-slave2:
    build:
      context: ./slave
      dockerfile: Dockerfile-slave
    image: mysql-slave:v1.0
    restart: always
    container_name: mysql-slave2
    volumes:
      - ./slave/var/lib/mysql2:/var/lib/mysql
    ports:
      - "9308:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      timeout: 10s
      retries: 5
    command: ['--server-id=3',
              '--relay_log=slave-relay',
              '--lower_case_table_names=1',
              '--character-set-server=utf8mb4',
              '--collation-server=utf8mb4_general_ci']
    depends_on:
      mysql-master:
        condition: service_healthy

显然,通过 command 选项来配置参数是非常不方便的,尤其是当参数较多时,容易出错且难以维护。更好的方式是通过配置文件来进行参数配置,下面我们对 masterslave 目录分别创建 my.cnf 文件,内容如下:

# master/my.cnf
[mysqld]
server-id=1
log-bin=mysql-bin
binlog-ignore-db=mysql
binlog_cache_size=256M
binlog_format=ROW
gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON
lower_case_table_names=1
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci


# slave1/my1.cnf
[mysqld]
server-id=2
relay_log=slave-relay
gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON
read_only=ON
skip_slave_start=ON
lower_case_table_names=1
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci
# slave2/my2.cnf
[mysqld]
server-id=3
relay_log=slave-relay
gtid_mode=ON
enforce_gtid_consistency=ON
log_slave_updates=ON
read_only=ON
skip_slave_start=ON
lower_case_table_names=1
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci[mysqld]
server-id=3
relay_log=slave-relay
lower_case_table_names=1
character-set-server=utf8mb4
collation-server=utf8mb4_general_ci

然后,我们需要修改 docker-compose.yml 文件,删除 command 选项,并将配置文件挂载到容器中,修改后内容如下:

services:
  mysql-master:
    build:
      context: ./master
      dockerfile: Dockerfile-master
    image: mysql-master:v1.0
    # restart 用于设置容器的重启策略,always 表示无论容器退出状态如何,都会自动重启容器
    restart: always
    container_name: mysql-master
    volumes:
      - ./master/var/lib/mysql:/var/lib/mysql
      - ./master/my.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - "9306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_ROOT_HOST: '%'
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      timeout: 10s
      retries: 5
  
  mysql-slave1:
    build:
      context: ./slave
      dockerfile: Dockerfile-slave
    image: mysql-slave:v1.0
    restart: always
    container_name: mysql-slave1
    volumes:
      - ./slave/var/lib/mysql:/var/lib/mysql
      - ./slave/my1.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - "9307:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      timeout: 10s
      retries: 5
    depends_on:
      - mysql-master
  mysql-slave2:
    build:
      context: ./slave
      dockerfile: Dockerfile-slave
    image: mysql-slave:v1.0
    restart: always
    container_name: mysql-slave2
    volumes:
      - ./slave/var/lib/mysql2:/var/lib/mysql
      - ./slave/my2.cnf:/etc/mysql/conf.d/my.cnf
    ports:
      - "9308:3306"
    environment:
      MYSQL_ROOT_PASSWORD: root
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      interval: 30s
      timeout: 10s
      retries: 5
    depends_on:
      - mysql-master

可以看到,修改后配置文件更加清晰易懂,也更容易维护。

现在,我们就可以通过命令来编译主从镜像了:

╭─ljx@VM-16-15-debian ~/docker_test/mysql_cluster  
╰─➤  docker compose build      
#1 [internal] load local bake definitions
#1 reading from stdin 1.53kB done
#1 DONE 0.0s

#2 [mysql-slave2 internal] load build definition from Dockerfile-slave
#2 transferring dockerfile: 191B done
#2 DONE 0.0s

#3 [mysql-slave1 internal] load metadata for docker.io/library/mysql:8.0.41
#3 DONE 0.0s

#4 [mysql-master internal] load build definition from Dockerfile-master
#4 transferring dockerfile: 145B done
#4 DONE 0.0s

#3 [mysql-master internal] load metadata for docker.io/library/mysql:8.0.41
#3 DONE 0.0s

#5 [mysql-slave1 internal] load .dockerignore
#5 transferring context: 2B done
#5 DONE 0.0s

#6 [mysql-master internal] load .dockerignore
#6 transferring context: 2B done
#6 DONE 0.0s

#7 [mysql-master 1/3] FROM docker.io/library/mysql:8.0.41
#7 DONE 0.0s

#8 [mysql-slave2 2/3] RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
#8 CACHED

#9 [mysql-slave1 internal] load build context
#9 transferring context: 31B done
#9 DONE 0.0s

#10 [mysql-slave1 3/3] COPY ./slave.sql /docker-entrypoint-initdb.d
#10 CACHED

#11 [mysql-master] exporting to image
#11 exporting layers done
#11 writing image sha256:e9a5cfeda1b61ce4641a13d723e8f193609d8ccfffd341f9017d58e86e59fa95 done
#11 naming to docker.io/library/mysql-master:v1.0 done
#11 DONE 0.0s

#12 [mysql-slave1] exporting to image
#12 exporting layers done
#12 writing image sha256:26ff73ef84d3905562391aeb8d0d7f7feffd8116a53e445d26529349138992ed done
#12 naming to docker.io/library/mysql-slave:v1.0 done
#12 DONE 0.0s

#13 [mysql-slave2] exporting to image
#13 exporting layers done
#13 writing image sha256:c5e5c6f68584530dee7bb856db7e5d3cb2a58bb2682270fb07665078acd37597 done
#13 naming to docker.io/library/mysql-slave:v1.0 done
#13 DONE 0.0s

#14 [mysql-master] resolving provenance for metadata file
#14 DONE 0.0s

#15 [mysql-slave1] resolving provenance for metadata file
#15 DONE 0.0s

#16 [mysql-slave2] resolving provenance for metadata file
#16 DONE 0.0s
[+] Building 2/2
 ✔ mysql-slave:v1.0   Built                                                                                                                                                                     0.0s 
 ✔ mysql-master:v1.0  Built                                                                                                                                                                     0.0s 
# 检查编译好的镜像
╭─ljx@VM-16-15-debian ~/docker_test/mysql_cluster  
╰─➤  docker images | grep mysql
mysql-master                                                                                  v1.0                     e9a5cfeda1b6   23 minutes ago   764MB
mysql-slave                                                                                   v1.0                     c5e5c6f68584   23 minutes ago   764MB
mysql                                                                                         8.0.41                   1c83f38450c3   7 months ago     764MB

编译完成后,我们就可以通过 docker compose up -d 命令来启动主从集群了:

# 启动主从集群
╭─ljx@VM-16-15-debian ~/docker_test/mysql_cluster  
╰─➤  docker compose up -d
[+] Running 3/3
 ✔ Container mysql-master  Started                                                                                                                                                        5.5s 
 ✔ Container mysql-slave1  Started                                                                                                                                                        3.3s 
 ✔ Container mysql-slave2  Started                                                                                                                                                        3.2s 

验证主从集群是否搭建成功

然后我们在主节点插入一个 db1 数据库,并添加一张 info 表,插入一条数据:

╭─ljx@VM-16-15-debian ~/docker_test/mysql_cluster  
╰─➤  docker exec -it mysql-master bash
bash-5.1# mysql -p
Enter password: 
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 13
Server version: 8.0.41 MySQL Community Server - GPL

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
4 rows in set (0.04 sec)

mysql> create database db1;
Query OK, 1 row affected (0.01 sec)

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| db1                |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.00 sec)

mysql> use db1
Database changed
mysql> create table info(name varchar(50), age int);
Query OK, 0 rows affected (0.14 sec)

mysql> insert into info(name, age) values('ljx', 19);
Query OK, 1 row affected (0.09 sec)

然后我们通过第一个从节点 mysql-slave1 来查看数据是否同步成功:

╭─ljx@VM-16-15-debian ~/docker_test/mysql_cluster
╰─➤  docker exec -it mysql-slave2 bash
bash-5.1# mysql -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 355
Server version: 8.0.41 MySQL Community Server - GPL

Copyright (c) 2000, 2025, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> show databases;
+--------------------+
| Database           |
+--------------------+
| db1                |
| information_schema |
| mysql              |
| performance_schema |
| sys                |
+--------------------+
5 rows in set (0.02 sec)

mysql> use db1;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql> select * from db1;
ERROR 1146 (42S02): Table 'db1.db1' doesn't exist
mysql> select * from info;
+------+------+
| name | age  |
+------+------+
| ljx  |   19 |
+------+------+
1 row in set (0.00 sec)

可以看到,数据已经成功同步到从节点 mysql-slave1 上了。

Docker 部署 Redis 集群

Redis 集群是通过将数据分片存储在多个节点上来实现的,从而提高了系统的可扩展性和容错性。每个节点负责存储一部分数据,并且节点之间通过 Gossip 协议进行通信和协调。Redis 集群使用哈希槽(hash slot)来分配数据,每个键根据其哈希值被映射到一个特定的哈希槽上,而每个节点负责管理一定范围的哈希槽。

配置文件编写

这里不做过多解释,Redis 的配置文件是可以直接配置集群的,我们只需要在配置文件中添加以下几行:

#表示前台运行
daemonize no
#端口
port 6379
#持久化
dir /data/redis
#启用集群
cluster-enabled yes
#集群参数配置
cluster-config-file nodes.conf
#集群超时时间
cluster-node-timeout 5000
#密码配置
requirepass 123456
#主节点密码配置
masterauth 123456
#表示远端可以连接
bind * -::*

然后,针对于每一个Redis 节点,我们都需要对他们的配置文件进行一些修改,比如端口号、数据目录等,若我们一个一个手动配置,显然是非常麻烦的,因此我们可以制作一个 Redis 集群的专属中间件,来简化配置过程。

制作 Redis 集群专属中间件

首先,我们创建一个 redis_cluster 目录,创建一个 redis 目录,然后在该目录下创建 Dockerfile 文件,内容如下:

ARG REDIS_VERSION=7.0.15
ARG UBUNTU_VERSION=22.04
ARG BUSYBOX_VERSION=1.37.0

FROM ubuntu:${UBUNTU_VERSION} AS buildstage
ARG REDIS_VERSION

# Use a local mirror for faster apt (replace only if needed), install build deps
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list || true && \
  apt-get update && apt-get install -y --no-install-recommends \
    build-essential \
    ca-certificates \
    wget \
  && rm -rf /var/lib/apt/lists/*

WORKDIR /tmp

# Copy source tarball, extract, build and cleanup in a single RUN to avoid
# leaving the tarball or full source tree in intermediate image layers.
# Note: you can use ADD redis-7.0.11.tar.gz /tmp/ to let Docker auto-extract,
# but COPY + explicit tar is preferred for clarity and to avoid unintended
# remote download side-effects when a URL is passed to ADD.
COPY redis-${REDIS_VERSION}.tar.gz /tmp/
RUN mkdir -p /tmp/build && \
  tar xzf /tmp/redis-${REDIS_VERSION}.tar.gz -C /tmp/build && \
  cd /tmp/build/redis-${REDIS_VERSION} && \
  make && \
  mkdir -p /redis && \
  mv src/redis-server src/redis-cli /redis/ && \
  rm -rf /tmp/build /tmp/redis-${REDIS_VERSION}.tar.gz

# Copy the provided configuration into the image
COPY redis.conf /redis/

FROM busybox:${BUSYBOX_VERSION}-glibc

RUN mkdir -p /data/redis /redis
COPY --from=buildstage /redis /redis
EXPOSE 6379

# Use ENTRYPOINT so the image runs redis-server with the supplied config by default
ENTRYPOINT ["/redis/redis-server", "/redis/redis.conf"]

# Build-time metadata: re-declare ARGs in final stage and persist as LABELs
ARG REDIS_VERSION
ARG BUILD_DATE=unspecified
ARG UBUNTU_VERSION
ARG IMAGE_VERSION=latest
ARG BUSYBOX_VERSION

LABEL org.opencontainers.image.version="${IMAGE_VERSION}" \
  org.opencontainers.image.base.name="ubuntu:${UBUNTU_VERSION}" \
  org.opencontainers.image.ref.name="myredis" \
  org.opencontainers.image.created="${BUILD_DATE}" \
  org.opencontainers.image.vendor="ljx" \
  org.opencontainers.image.title="Redis Cluster" \
  org.opencontainers.image.description="Redis Cluster(base on Redis: ${REDIS_VERSION}) build on busybox:${BUSYBOX_VERSION}-glibc"

该配置文件非常符合 OCI 标准,并且通过多阶段构建的方式,极大地减小了最终镜像的体积。接下来我会对这个配置文件进行逐行解释:

1️⃣ 构建阶段(Build Stage)

FROM ubuntu:${UBUNTU_VERSION} AS buildstage
ARG REDIS_VERSION

# 安装构建依赖
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list || true && \
    apt-get update && apt-get install -y --no-install-recommends \
        build-essential \
        ca-certificates \
        wget \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /tmp

# 拷贝 Redis 源码并编译
COPY redis-${REDIS_VERSION}.tar.gz /tmp/
RUN mkdir -p /tmp/build && \
    tar xzf /tmp/redis-${REDIS_VERSION}.tar.gz -C /tmp/build && \
    cd /tmp/build/redis-${REDIS_VERSION} && \
    make && \
    mkdir -p /redis && \
    mv src/redis-server src/redis-cli /redis/ && \
    rm -rf /tmp/build /tmp/redis-${REDIS_VERSION}.tar.gz

# 拷贝自定义配置
COPY redis.conf /redis/

在这一部分中:

  • 使用 ARG 参数化 Redis 版本和 Ubuntu 版本,便于升级和重用。
  • 使用镜像源加速下载依赖。
  • 在单个 RUN 中完成源码解压、编译和清理,避免中间层冗余文件,减小镜像体积。

2️⃣ 运行阶段(Runtime Stage)

FROM busybox:${BUSYBOX_VERSION}-glibc

RUN mkdir -p /data/redis /redis
COPY --from=buildstage /redis /redis
EXPOSE 6379

ENTRYPOINT ["/redis/redis-server", "/redis/redis.conf"]

在这一部分中:

  • 运行阶段使用轻量级 BusyBox 镜像,并自带 glibc 兼容库。
  • 只复制最终可执行文件和配置,进一步减小镜像体积。
  • 将 Redis 默认端口 6379 暴露给容器外部访问。
  • 使用 ENTRYPOINT 固定启动命令,保证容器启动时自动加载 Redis 配置。

3️⃣ OCI 标签与元数据

ARG REDIS_VERSION
ARG BUILD_DATE=unspecified
ARG UBUNTU_VERSION
ARG IMAGE_VERSION=latest
ARG BUSYBOX_VERSION

LABEL org.opencontainers.image.version="${IMAGE_VERSION}" \
      org.opencontainers.image.base.name="ubuntu:${UBUNTU_VERSION}" \
      org.opencontainers.image.ref.name="myredis" \
      org.opencontainers.image.created="${BUILD_DATE}" \
      org.opencontainers.image.vendor="ljx" \
      org.opencontainers.image.title="Redis Cluster" \
      org.opencontainers.image.description="Redis Cluster(base on Redis: ${REDIS_VERSION}) build on busybox:${BUSYBOX_VERSION}-glibc"

在这一部分中:

  • 使用标准的 OCI 标签记录镜像的基础镜像、版本号、创建时间和描述信息。
  • 标签有助于镜像管理、追踪和自动化部署,符合工业规范。

这个 Dockerfile 体现了以下规范化做法:

  1. 多阶段构建:编译和运行环境分离,减小最终镜像体积。
  2. 参数化构建:通过 ARG 灵活指定版本信息。
  3. 清理中间文件:单条 RUN 完成解压、编译、移动、清理,避免冗余层。
  4. 遵循 OCI 标签规范:记录镜像版本、基础镜像、创建时间和描述。
  5. 轻量化运行环境:使用 BusyBox 运行阶段,仅保留可执行文件和必要目录。

通过以上方式,我们就制作好了一个符合 OCI 标准的 Redis 集群专属中间件,接下来我们就可以通过该中间件来简化 Redis 集群的配置过程了。

Redis 集群化容器编排

在生产或测试环境中,为了实现高可用和分片存储,需要将 Redis 部署为集群模式。通过 Docker Compose,我们可以方便地在单台主机上快速搭建多节点 Redis 集群。

1. 服务定义

  • redis1 ~ redis5:五个 Redis 节点,每个节点都是独立的容器。
  • 每个节点都使用自定义镜像 myredis:v1.0,包含 Redis 可执行文件和配置文件。
  • entrypoint 指定容器启动时运行 redis-server,并加载对应的配置文件。
  • command 用于覆盖默认端口、启用集群模式、开启持久化、禁用保护模式(便于集群内部通信)。

2. 端口映射

  • 每个节点的 Redis 客户端端口(6379)映射到宿主机的不同端口(7000~7004),方便在本地主机访问。
  • 集群内部端口(节点间通信端口)由 Redis 自动处理,不需要额外映射。

3. 数据持久化

  • 每个节点的数据目录都映射到宿主机对应目录(./data/7000 等),保证容器重启或重建后数据不会丢失。

4. 健康检查

  • 每个节点配置了 healthcheck,定期通过 redis-cli ping 检查节点是否可用。
  • 健康检查有助于 Docker Compose 识别节点状态,保证集群启动顺序和稳定性。

5. 集群初始化

  • redis-init 容器负责在所有节点启动后,执行 redis-cli --cluster create 命令完成集群的节点注册和槽分配。
  • 容器依赖关系通过 depends_on 控制,确保初始化时各节点已启动。
  • restart: 'no' 表示初始化容器只运行一次,完成集群创建后退出。

6. 网络配置

  • 所有节点和初始化容器都连接到自定义桥接网络 redis-cluster-net,保证容器间可通过容器名互相访问。

具体的 docker-compose.yml 文件内容如下:

services:
  redis1:
    image: myredis:v1.0
    container_name: redis1
    hostname: redis1
    entrypoint: ["/redis/redis-server"]
    command: ["--port","6379","--cluster-enabled","yes","--cluster-config-file","nodes-7000.conf","--cluster-node-timeout","5000","--appendonly","yes","--bind","0.0.0.0","--protected-mode","no"]
    ports:
      - "7000:6379"
    volumes:
      - ./data/7000:/data/redis
    healthcheck:
      test: ["CMD", "/redis/redis-cli", "-p", "6379", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis2:
    image: myredis:v1.0
    container_name: redis2
    hostname: redis2
    entrypoint: ["/redis/redis-server"]
    command: ["--port","6379","--cluster-enabled","yes","--cluster-config-file","nodes-7001.conf","--cluster-node-timeout","5000","--appendonly","yes","--bind","0.0.0.0","--protected-mode","no"]
    ports:
      - "7001:6379"
    volumes:
      - ./data/7001:/data/redis
    healthcheck:
      test: ["CMD", "/redis/redis-cli", "-p", "6379", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis3:
    image: myredis:v1.0
    container_name: redis3
    hostname: redis3
    entrypoint: ["/redis/redis-server"]
    command: ["--port","6379","--cluster-enabled","yes","--cluster-config-file","nodes-7002.conf","--cluster-node-timeout","5000","--appendonly","yes","--bind","0.0.0.0","--protected-mode","no"]
    ports:
      - "7002:6379"
    volumes:
      - ./data/7002:/data/redis
    healthcheck:
      test: ["CMD", "/redis/redis-cli", "-p", "6379", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis4:
    image: myredis:v1.0
    container_name: redis4
    hostname: redis4
    entrypoint: ["/redis/redis-server"]
    command: ["--port","6379","--cluster-enabled","yes","--cluster-config-file","nodes-7003.conf","--cluster-node-timeout","5000","--appendonly","yes","--bind","0.0.0.0","--protected-mode","no"]
    ports:
      - "7003:6379"
    volumes:
      - ./data/7003:/data/redis
    healthcheck:
      test: ["CMD", "/redis/redis-cli", "-p", "6379", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis5:
    image: myredis:v1.0
    container_name: redis5
    hostname: redis5
    entrypoint: ["/redis/redis-server"]
    command: ["--port","6379","--cluster-enabled","yes","--cluster-config-file","nodes-7004.conf","--cluster-node-timeout","5000","--appendonly","yes","--bind","0.0.0.0","--protected-mode","no"]
    ports:
      - "7004:6379"
    volumes:
      - ./data/7004:/data/redis
    healthcheck:
      test: ["CMD", "/redis/redis-cli", "-p", "7004", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis-init:
    image: myredis:v1.0
    container_name: redis-cluster-init
    depends_on:
      - redis1
      - redis2
      - redis3
      - redis4
      - redis5
    entrypoint: ["/redis/redis-cli","--cluster","create",
             "redis1:6379","redis2:6379","redis3:6379","redis4:6379","redis5:6379",
             "--cluster-replicas","0","--cluster-yes"]
    restart: 'no'

networks:
  default:
    name: redis-cluster-net
    driver: bridge

这样一来,我们就完成了 Redis 集群的容器编排配置。

启动 Redis 集群

首先,我们要将镜像编译好,可以直接使用指令 docker build -t 来完成编译,在这里我利用一个简单的脚本实现了更加简单的自动化部署,避免了每次指定构建时间参数的时候需要手动实现:

#!/usr/bin/env bash
set -euo pipefail

# build.sh [version] [tag]
# Example: ./build.sh 7.0.15 myredis:7.0.15

IMAGE_VERSION=${1:-latest}
REDIS_VERSION=${2:-7.0.15}
IMAGE_TAG=myredis:${IMAGE_VERSION}
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)

echo "Building ${IMAGE_TAG} with REDIS_VERSION=${REDIS_VERSION} BUILD_DATE=${BUILD_DATE}"

docker build \
  --build-arg REDIS_VERSION=${REDIS_VERSION} \
  --build-arg BUILD_DATE="${BUILD_DATE}" \
  --build-arg IMAGE_VERSION="${IMAGE_VERSION}" \
  -t ${IMAGE_TAG} .

echo "Built ${IMAGE_TAG}"

执行该脚本后,我们就可以看到编译好的镜像了:

╭─ljx@VM-16-15-debian ~/docker_test/redis_cluster/redis  
╰─➤  ./build.sh v1.0                                                                                                                                                                                1 ↵
Building myredis:v1.0 with REDIS_VERSION=7.0.15 BUILD_DATE=2025-09-13T08:18:28Z
[+] Building 1.7s (15/15) FINISHED                                                                                                                                                       docker:default
 => [internal] load build definition from Dockerfile                                                                                                                                               0.0s
 => => transferring dockerfile: 2.15kB                                                                                                                                                             0.0s
 => [internal] load metadata for docker.io/library/ubuntu:22.04                                                                                                                                    0.0s
 => [internal] load metadata for docker.io/library/busybox:1.37.0-glibc                                                                                                                            0.8s
 => [internal] load .dockerignore                                                                                                                                                                  0.0s
 => => transferring context: 2B                                                                                                                                                                    0.0s
 => [buildstage 1/6] FROM docker.io/library/ubuntu:22.04                                                                                                                                           0.0s
 => CACHED [stage-1 1/3] FROM docker.io/library/busybox:1.37.0-glibc@sha256:a2c55ed708c564a69a695e0a3bb16a4c47d2bb268d2ebd06f0d77336801b80de                                                       0.0s
 => [stage-1 2/3] RUN mkdir -p /data/redis /redis                                                                                                                                                  0.4s
 => [internal] load build context                                                                                                                                                                  0.0s
 => => transferring context: 74B                                                                                                                                                                   0.0s
 => CACHED [buildstage 2/6] RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list || true &&  apt-get update && apt-get install -y --no-install-recommends   build-  0.0s
 => CACHED [buildstage 3/6] WORKDIR /tmp                                                                                                                                                           0.0s
 => CACHED [buildstage 4/6] COPY redis-7.0.15.tar.gz /tmp/                                                                                                                                         0.0s
 => CACHED [buildstage 5/6] RUN mkdir -p /tmp/build &&  tar xzf /tmp/redis-7.0.15.tar.gz -C /tmp/build &&  cd /tmp/build/redis-7.0.15 &&  make &&  mkdir -p /redis &&  mv src/redis-server src/re  0.0s
 => CACHED [buildstage 6/6] COPY redis.conf /redis/                                                                                                                                                0.0s
 => [stage-1 3/3] COPY --from=buildstage /redis /redis                                                                                                                                             0.2s
 => exporting to image                                                                                                                                                                             0.1s
 => => exporting layers                                                                                                                                                                            0.1s
 => => writing image sha256:71e3a1af7bb7b86893b9f696f7c7ebbf25b7e9ccb1029e1099f4d3897c908ac9                                                                                                       0.0s
 => => naming to docker.io/library/myredis:v1.0                                                                                                                                                    0.0s
Built myredis:v1.0

可以看到,我们的镜像经过多阶段构建后,体积非常小,只有 21.6MB:

╭─ljx@VM-16-15-debian ~/docker_test/redis_cluster/redis  
╰─➤  docker images | grep myredis     
myredis                                                                                       v1.0                     71e3a1af7bb7   24 minutes ago   21.6MB

然后,我们可以通过以下命令来启动 Redis 集群:

╭─ljx@VM-16-15-debian ~/docker_test/redis_cluster/redis  
╰─➤  docker compose up -d
[+] Running 7/7
 ✔ Network redis-cluster-net     Created                                                                                                                                                           0.1s 
 ✔ Container redis3              Started                                                                                                                                                           0.7s 
 ✔ Container redis4              Started                                                                                                                                                           0.7s 
 ✔ Container redis5              Started                                                                                                                                                           0.8s 
 ✔ Container redis1              Started                                                                                                                                                           0.6s 
 ✔ Container redis2              Started                                                                                                                                                           0.8s 
 ✔ Container redis-cluster-init  Started                                                                                                                                                           0.8s

接下来我们测试一下集群是否搭建成功:

╭─ljx@VM-16-15-debian ~/docker_test/redis_cluster/redis
╰─➤  docker logs redis-cluster-init --tail 200 || true                                                                                                                                                                                                                  137>>> Performing hash slots allocation on 5 nodes...
Master[0] -> Slots 0 - 3276
Master[1] -> Slots 3277 - 6553
Master[2] -> Slots 6554 - 9829
Master[3] -> Slots 9830 - 13106
Master[4] -> Slots 13107 - 16383
M: d424961a8086f707d1072e7fbd11c25293b2c685 redis1:6379
   slots:[0-3276] (3277 slots) master
M: ad75ddd3d447aea214761bc940a58c3a427cccae redis2:6379
   slots:[3277-6553] (3277 slots) master
M: a29e96133edb3fa8fda04abcfc6dcd46d5d10c87 redis3:6379
   slots:[6554-9829] (3276 slots) master
M: b40d3c33a3655881b8735835a84b4cbd9cb85f82 redis4:6379
   slots:[9830-13106] (3277 slots) master
M: bb68accc55da6082154662bf382aaa1a6b87a231 redis5:6379
   slots:[13107-16383] (3277 slots) master
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join

>>> Performing Cluster Check (using node redis1:6379)
M: d424961a8086f707d1072e7fbd11c25293b2c685 redis1:6379
   slots:[0-3276] (3277 slots) master
M: a29e96133edb3fa8fda04abcfc6dcd46d5d10c87 172.20.0.4:6379
   slots:[6554-9829] (3276 slots) master
M: bb68accc55da6082154662bf382aaa1a6b87a231 172.20.0.6:6379
   slots:[13107-16383] (3277 slots) master
M: ad75ddd3d447aea214761bc940a58c3a427cccae 172.20.0.5:6379
   slots:[3277-6553] (3277 slots) master
M: b40d3c33a3655881b8735835a84b4cbd9cb85f82 172.20.0.2:6379
   slots:[9830-13106] (3277 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

可以看到,集群已经成功搭建完成了,我们可以通过 redis-cli 来连接集群:

╭─ljx@VM-16-15-debian ~/docker_test/redis_cluster/redis
╰─➤  docker exec -it redis3 sh
/ # keys *
sh: keys: not found
/ # /redis/redis-cli -c
127.0.0.1:6379> keys *
(empty array)
127.0.0.1:6379> set k1 2
-> Redirected to slot [12706] located at 172.20.0.3:6379
OK
172.20.0.3:6379> exit
/ # exit
╭─ljx@VM-16-15-debian ~/docker_test/redis_cluster/redis
╰─➤  docker exec -it redis4 sh
/ #  /redis/redis-cli -c
127.0.0.1:6379> get k1
"2"

可以看到,我们成功地在 redis3 节点上设置了一个键 k1,并且在 redis4 节点上成功获取到了该键的值,说明我们的 Redis 集群已经搭建成功并且工作正常。

改进:使用 TLS 加密 Redis 集群通信

在生产环境中,为了确保数据传输的安全性,建议启用 TLS 加密来保护 Redis 集群的通信。以下是如何在 Docker Compose 配置中启用 TLS 的步骤:

一般来说,系统默认会有一些自签名的证书,我们可以直接利用这些证书来启用 TLS 功能。

首先我们需要在 redis.conf 配置文件中添加以下 TLS 相关配置:

tls-port 6379
port 0
tls-cert-file /redis/certs/redis.crt
tls-key-file /redis/certs/redis.key
tls-ca-cert-file /redis/certs/ca.crt
tls-auth-clients yes
cluster-announce-tls-port 6379
cluster-announce-port 0

这些配置项的含义如下:

  • tls-port 6379:指定 Redis 监听的 TLS 端口。
  • port 0:禁用非 TLS 端口,确保所有通信都
  • 通过 TLS 进行。
  • tls-cert-filetls-key-filetls-ca-cert-file:指定 TLS 证书、私钥和 CA 证书的路径。
  • tls-auth-clients yes:启用客户端认证,确保只有持有有效证书的客户端才能连接。
  • cluster-announce-tls-port 6379cluster-announce-port 0:配置集群节点在集群中宣布的 TLS 端口和非 TLS 端口。

然后,我们通过修改 Dockerfile 来将证书文件复制到镜像中:

ARG REDIS_VERSION=7.0.15
ARG UBUNTU_VERSION=22.04
ARG BUSYBOX_VERSION=1.37.0

FROM ubuntu:${UBUNTU_VERSION} AS buildstage
ARG REDIS_VERSION

# Use a local mirror for faster apt (replace only if needed), install build deps
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list || true && \
  apt-get update && apt-get install -y --no-install-recommends ca-certificates \
    build-essential \
    ca-certificates \
    wget \
    libssl-dev \
  && rm -rf /var/lib/apt/lists/*

RUN mkdir -p /tmp/certs && cp -r /etc/ssl/certs/* /tmp/certs/

WORKDIR /tmp

# Copy source tarball, extract, build and cleanup in a single RUN to avoid
# leaving the tarball or full source tree in intermediate image layers.
# Note: you can use ADD redis-7.0.11.tar.gz /tmp/ to let Docker auto-extract,
# but COPY + explicit tar is preferred for clarity and to avoid unintended
# remote download side-effects when a URL is passed to ADD.
COPY redis-${REDIS_VERSION}.tar.gz /tmp/
RUN mkdir -p /tmp/build && \
  tar xzf /tmp/redis-${REDIS_VERSION}.tar.gz -C /tmp/build && \
  cd /tmp/build/redis-${REDIS_VERSION} && \
  make BUILD_TLS=yes LDFLAGS="-static" && \
  mkdir -p /redis && \
  mv src/redis-server src/redis-cli /redis/ && \
  rm -rf /tmp/build /tmp/redis-${REDIS_VERSION}.tar.gz

# Copy the provided configuration into the image
COPY redis.conf /redis/

FROM busybox:${BUSYBOX_VERSION}

RUN mkdir -p /data/redis
COPY --from=buildstage /redis /redis
COPY --from=buildstage /tmp/certs /redis/certs
EXPOSE 6379

# Use ENTRYPOINT so the image runs redis-server with the supplied config by default
ENTRYPOINT ["/redis/redis-server", "/redis/redis.conf"]

# Build-time metadata: re-declare ARGs in final stage and persist as LABELs
ARG REDIS_VERSION
ARG BUILD_DATE=unspecified
ARG UBUNTU_VERSION
ARG IMAGE_VERSION=latest
ARG BUSYBOX_VERSION

LABEL org.opencontainers.image.version="${IMAGE_VERSION}" \
  org.opencontainers.image.base.name="ubuntu:${UBUNTU_VERSION}" \
  org.opencontainers.image.ref.name="myredis" \
  org.opencontainers.image.created="${BUILD_DATE}" \
  org.opencontainers.image.vendor="ljx" \
  org.opencontainers.image.title="Redis Cluster" \
  org.opencontainers.image.description="Redis Cluster(base on Redis: ${REDIS_VERSION}) build on busybox:${BUSYBOX_VERSION}-glibc"

在这个修改后的 Dockerfile 中,我们做了以下改动:

1️⃣ 基础镜像

  • 非 TLS 版本
    使用 busybox:glibc 作为最终镜像,依赖系统动态库(如 glibc)运行 Redis。
  • TLS 版本
    使用普通 busybox,结合静态编译的 Redis,Redis 二进制包含所有依赖库(包括 OpenSSL/TLS),不再依赖镜像中的动态库。

🔹 变化点:静态编译 + TLS 支持,让镜像可以用更轻量的 BusyBox,但二进制文件本身体积略增。


2️⃣ 编译参数

  • 非 TLS 版本

    make

    默认生成动态链接 Redis,不包含 TLS 功能。

  • TLS 版本

    make BUILD_TLS=yes LDFLAGS="-static"
    • BUILD_TLS=yes:开启 TLS/SSL 编译支持,使 Redis 支持加密的 TCP 通信。
    • LDFLAGS="-static":静态编译所有依赖库(包括 OpenSSL),无需依赖宿主系统的动态库。

🔹 变化点:通过静态编译 + TLS 支持,Redis 在镜像内自包含所有库,并可以安全传输数据。


3️⃣ 系统证书(CA)

  • 非 TLS 版本
    不需要系统 CA 文件,Redis 与客户端通信都是明文。

  • TLS 版本

    RUN mkdir -p /tmp/certs && cp -r /etc/ssl/certs/* /tmp/certs/
    COPY --from=buildstage /tmp/certs /redis/certs
    • 将宿主系统的 CA 证书拷贝到镜像中,用于 Redis TLS 验证客户端或集群节点证书。

🔹 变化点:增加了对 CA 证书的支持,使 Redis 能够验证 TLS 连接的合法性,保证集群节点和客户端之间安全通信。


4️⃣ 镜像大小

  • 非 TLS 版本:约 22MB(动态依赖库,镜像较小)
  • TLS 静态编译版本:约 33MB(静态编译 + OpenSSL/TLS 库)

🔹 变化点:增加的体积主要来源于静态编译的 OpenSSL/TLS 库,被打包进 Redis 二进制。


5️⃣ 总结新增功能

  1. 支持 TLS/SSL 加密,保证 Redis 集群与客户端通信安全。
  2. 通过 静态编译,减少对底层系统库依赖,提高可移植性。
  3. 镜像内包含 CA 证书,可验证集群节点和客户端的证书合法性。
  4. 保留原有的轻量 BusyBox 镜像特性,同时适配安全生产环境需求。

🔹 博客小结语:
通过这次改造,Redis 镜像不仅保留了原有的轻量化特性,还增加了数据传输安全保障,为未来生产环境部署 TLS Redis 集群打下基础。