Docker 集群化部署
在当今云计算与微服务架构盛行的时代,集群化已经成为应用部署与运维的核心诉求。而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 等数据修改语句。二进制日志对于数据恢复、复制和审计等功能非常重要。
具体实现步骤
首先,我们需要分别给主从节点创建属于他们的专属目录 master 和 slave
然后,分别在 master 和 slave 目录下创建 Dockerfile-master 和 Dockerfile-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 选项来配置参数是非常不方便的,尤其是当参数较多时,容易出错且难以维护。更好的方式是通过配置文件来进行参数配置,下面我们对 master 和 slave 目录分别创建 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 /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 /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 体现了以下规范化做法:
- 多阶段构建:编译和运行环境分离,减小最终镜像体积。
- 参数化构建:通过
ARG灵活指定版本信息。 - 清理中间文件:单条
RUN完成解压、编译、移动、清理,避免冗余层。 - 遵循 OCI 标签规范:记录镜像版本、基础镜像、创建时间和描述。
- 轻量化运行环境:使用 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-file、tls-key-file、tls-ca-cert-file:指定 TLS 证书、私钥和 CA 证书的路径。tls-auth-clients yes:启用客户端认证,确保只有持有有效证书的客户端才能连接。cluster-announce-tls-port 6379和cluster-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 /redis /redis
COPY /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 /tmp/certs /redis/certs- 将宿主系统的 CA 证书拷贝到镜像中,用于 Redis TLS 验证客户端或集群节点证书。
🔹 变化点:增加了对 CA 证书的支持,使 Redis 能够验证 TLS 连接的合法性,保证集群节点和客户端之间安全通信。
4️⃣ 镜像大小
- 非 TLS 版本:约 22MB(动态依赖库,镜像较小)
- TLS 静态编译版本:约 33MB(静态编译 + OpenSSL/TLS 库)
🔹 变化点:增加的体积主要来源于静态编译的 OpenSSL/TLS 库,被打包进 Redis 二进制。
5️⃣ 总结新增功能
- 支持 TLS/SSL 加密,保证 Redis 集群与客户端通信安全。
- 通过 静态编译,减少对底层系统库依赖,提高可移植性。
- 镜像内包含 CA 证书,可验证集群节点和客户端的证书合法性。
- 保留原有的轻量 BusyBox 镜像特性,同时适配安全生产环境需求。
🔹 博客小结语:
通过这次改造,Redis 镜像不仅保留了原有的轻量化特性,还增加了数据传输安全保障,为未来生产环境部署 TLS Redis 集群打下基础。