【Docker】镜像制作实操案例 - 详解
目录
四.Dockerfile配合docker-compose.yml构建镜像和服务
4.1.docker-compose.yml的build参数
一.C++镜像制作
国内镜像源的替换
如果说,我们需要在镜像里面下载什么软件,那么我们最好是替换成国内下载源,因为国外的会限速。
我们可以去中科大的镜像源的站点:USTC Open Source Software Mirror


我们往下寻找到

我们直接拷贝这个命令即可
注意我们需要把前面的sudo指令去掉,因为镜像中没有sudo这个指令
sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list
执行完这条命令,我们再执行下面这条命令就完成了国内镜像源的替换
apt-get update
构建镜像
首先我们先在本目录下创建一个main.cpp
#include
using namespace std;
int main()
{
cout<<"hello docker"<
现在我们去写一下Dockerfile
FROM ubuntu:22.04
# 更新源并安装g++
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list && \
apt-get update && \
apt-get install -y g++
WORKDIR /src
COPY main.cpp .
# 编译并清理
RUN g++ main.cpp -o demo && \
apt-get remove --purge -y g++ && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* main.cpp
CMD ["/src/demo"]
注意,我们在里面安装了g++,但是在使用完g++之后,我们还是需要将g++彻底删除,以节约容器体积

现在我们就基于这个镜像去启动容器看看

非常的完美啊。
二.合理使用dockerignore
.dockerignore 是一个纯文本文件,它的作用和 .gitignore 对于 Git 类似。你把它放在 Docker 构建上下文(通常是你的 Dockerfile 所在的目录)的根目录下。
要理解 .dockerignore,必须先理解 Docker 的构建过程。
当你运行 docker build -t my-app . 时,那个最后的 . 就是构建上下文的路径。
Docker 引擎(后台守护进程)在做任何事之前,会先把整个构建上下文目录(包括所有子目录和文件)打包,然后发送给 Docker 守护进程。
- 发送的是“整个”目录:这意味着,即使你的 Dockerfile 里只需要一个 app.py 文件,但如果你的目录里有别的其他文件或者子目录的话,所有这些都会被一起打包并发送。
- 这个过程发生在评估 Dockerfile 之前。Docker 守护进程是先拿到这一大堆文件,然后再根据 Dockerfile 里的指令去操作。
.dockerignore 文件的作用就是在这个打包和发送之前,告诉 Docker:“在发送构建上下文时,请忽略掉以下这些文件和目录”。
所谓忽略,就是不要把这些文件或者目录打包发给Docker守护进程,然后Docker守护进程就访问不到了。
语法
文件基本结构
每行一个条目:文件中的每一行代表一个独立的匹配模式,不能在一行中写多个模式
空行处理:完全空白的行会被 Docker 完全忽略,不产生任何效果
注释行:以 # 字符开头的行被视为注释,Docker 在处理时会跳过这些行
行尾空格:每行开头和结尾的空格会被自动去除,不会影响匹配
示例说明:
# 这是一个注释 - 这整行都会被忽略
file.txt # 这是一个模式 - 井号后面有空格,所以这是模式的一部分!
# 上面那行会匹配名为 "file.txt # 这是一个模式" 的文件
我们现在来看看具体的匹配模式
*号和?号
* 星号通配符
- 匹配零个或多个任意字符
- 但不能跨越目录边界(不能匹配 / 路径分隔符)
- 工作范围仅限于当前路径层级
详细示例:
*.txt
✅ 匹配:file.txt、123.txt、.txt(注意:可以匹配只有扩展名的文件)
❌ 不匹配:file.log、src/file.txt(因为跨目录了)、backup.txt.bak
temp*
✅ 匹配:temp、temporary、temp_backup、temp123
❌ 不匹配:src/temp、my_temp(星号只在末尾起作用)
? 问号通配符
- 匹配恰好一个任意字符
- 同样不能匹配路径分隔符 /
详细示例:
file?.txt
✅ 匹配:file1.txt、fileA.txt、file_.txt
❌ 不匹配:file.txt(缺少一个字符)、file10.txt(多了一个字符)、file/.txt
?.md
✅ 匹配:a.md、1.md、_.md
❌ 不匹配:file.md、README.md、/.md
递归匹配 **
** 用于匹配任意层级的目录结构,是最强大的模式之一。
**/*.ext - 匹配任意深度的特定扩展名文件:
**/*.log
- ✅ 匹配:app.log、logs/app.log、src/utils/debug.log、a/b/c/d/error.log
**/temp
- ✅ 匹配:temp、src/temp、a/b/c/temp、project/src/utils/temp
directory/** - 匹配指定目录下的所有内容:
logs/**
- ✅ 匹配:logs/ 下的所有文件和子目录
- ✅ 匹配:logs/debug.log、logs/2023/error.log、logs/archive/old.log
- ❌ 不匹配:backup_logs/(必须是精确的 logs 目录)
组合使用 **:
src/**/test
✅ 匹配:src/test、src/unit/test、src/a/b/c/test
/ 路径分隔符的特殊含义
首先我们需要了解一些东西
/home/user/my-project/ ← 这就是你的项目文件夹
├── Dockerfile
├── app.py
├── requirements.txt
├── src/
│ └── utils.py
└── data/
└── config.json
我们在这个目录里面执行docker build -t my-app .
- 那个 . 就是构建上下文根目录!
- . 表示"当前目录"
- 所以 /home/user/my-project/ 就是构建上下文根目录
Docker 引擎(后台守护进程)在做任何事之前,会先把整个构建上下文目录(包括所有子目录和文件)打包,然后发送给 Docker 守护进程。
情况1:前缀 / - 只匹配根目录
规则:以 / 开头的模式,只匹配构建上下文根目录中的文件/目录
/README.md
- ✅ 匹配:构建上下文根目录的README.md(就在项目根目录)
- ❌ 不匹配:构建上下文根目录的src/README.md(在子目录里)
- ❌ 不匹配:构建上下文根目录的docs/README.md(在另一个子目录里)
/config
- ✅ 匹配:构建上下文根目录的config(根目录的文件)
- ✅ 匹配:构建上下文根目录的config/(根目录的目录)
- ❌ 不匹配:构建上下文根目录的src/config(在 src 目录里)
- ❌ 不匹配:构建上下文根目录的utils/config(在 utils 目录里)
简单记忆:/ 开头 = "只看第一层,不看子文件夹"
情况2:后缀 / - 只匹配目录(不匹配文件)
规则:以 / 结尾的模式,只匹配目录,不匹配同名文件
假设你的项目中有:
/my-project/
├── logs ← 这是一个文件
├── logs/ ← 这是一个目录
│ └── app.log
└── node_modules/ ← 这是一个目录
那么
logs/
- ✅ 匹配:构建上下文根目录的logs/ 目录(包括里面的所有文件和子目录)
- ❌ 不匹配:/my-project/logs 文件(因为这是文件,不是目录)
- ❌ 不匹配:/my-project/backup_logs/(名字不完全匹配)
node_modules/
- ✅ 匹配:构建上下文根目录的node_modules/ 目录及里面的所有文件和子目录
- ❌ 不匹配:如果存在 /my-project/node_modules 文件(但实际上这通常是目录)
- 简单记忆:/ 结尾 = "我只关心文件夹,不关心文件"
情况3:无 / - 匹配任何位置的同名文件或目录
规则:没有特殊 / 的模式,会在所有目录层级中搜索匹配
temp
- ✅ 匹配:构建上下文根目录的temp(根目录的文件)
- ✅ 匹配:构建上下文根目录的temp/(根目录的目录)
- ✅ 匹配:构建上下文根目录的src/temp(src 目录里的文件)
- ✅ 匹配:构建上下文根目录的utils/temp/(utils 目录里的目录)
- ✅ 匹配:构建上下文根目录的src/components/temp(深层目录里的文件)
*.log
- ✅ 匹配:构建上下文根目录的app.log(根目录的 .log 文件)
- ✅ 匹配:构建上下文根目录的src/debug.log(src 目录里的 .log 文件)
- ✅ 匹配:构建上下文根目录的logs/error.log(logs 目录里的 .log 文件)
- ✅ 匹配:构建上下文根目录的logs/2023/access.log(深层目录里的 .log 文件)
- ❌ 不匹配:构建上下文根目录的app.txt(扩展名不匹配)
简单记忆:没有特殊符号 = "在整个项目中搜索,不管在哪个文件夹里"
可能大家还是有点难去理解,我们看下面这个例子
假设项目结构:
/my-project/
├── config ← 根目录的文件
├── config/ ← 根目录的目录
├── src/
│ ├── config ← src 目录里的文件
│ └── config/ ← src 目录里的目录
假如我们在my-project目录里面执行了docker build -t my-app .
那么构建上下文根目录就是my-project目录
模式 匹配结果 解释 /config✅ 根目录的 config 文件
✅ 根目录的 config/ 目录
❌ src 里的 config
❌ src 里的 config/ 只看根目录 config/✅ 根目录的 config/ 目录
✅ src 里的 config/ 目录
❌ 根目录的 config 文件
❌ src 里的 config 文件 只看目录,不看文件 config✅ 根目录的 config 文件
✅ 根目录的 config/ 目录
✅ src 里的 config 文件
✅ src 里的 config/ 目录 所有位置的都看
还行吧
排除规则 ! 的深度解析
! 用于创建例外,但有着严格的限制条件。
基本排除语法:
*.txt
!important.txt
效果:忽略所有 .txt 文件,但不忽略 important.txt
排除规则的严格限制:
错误示例(不会生效):
src/
!src/important.py
- ❌ 这个排除无效,因为父目录 src/ 已经被完全忽略
- Docker 在处理时根本不会看到 src/important.py,所以排除规则无法应用
正确的方法:使用通配符而不是直接忽略目录
src/*
!src/important.py
!src/utils/
这样表示:
- 忽略 src/ 目录下的所有直接子项
- 但不忽略 src/important.py 文件
- 但不忽略 src/utils/ 目录
多层排除的复杂性:
src/**
!src/**/*.py
!src/assets/
- 第一行:忽略 src/ 下的所有内容
- 第二行:但不忽略所有 .py 文件
- 第三行:但不忽略 src/assets/ 目录
处理顺序和优先级
规则按照从上到下的顺序逐行处理,后面的规则可以推翻前面的决定。
示例分析处理流程:
*.log
!app.log
error.log
处理过程:
- 遇到 *.log:标记所有 .log 文件为"忽略"
- 遇到 !app.log:将 app.log 从忽略列表中移除
- 遇到 error.log:将 error.log 标记为"忽略"(即使它符合 *.log)
最终结果:
被忽略:debug.log、error.log、any.log
不被忽略:app.log
特殊字符和转义
当文件名包含特殊字符时,需要使用反斜杠 \ 进行转义。
转义示例:
file\*.txt
✅ 匹配:字面名为 file*.txt 的文件
❌ 不匹配:file123.txt、file_backup.txt
test\?.js
✅ 匹配:字面名为 test?.js 的文件
❌ 不匹配:test1.js、testA.js
config\#.json
✅ 匹配:字面名为 config#.json 的文件
❌ 不匹配:这行不会被当作注释
无法覆盖的默认行为
无论你怎么写规则,以下文件总是会被包含在构建上下文中:
Dockerfile(构建指令文件)
.dockerignore(忽略规则文件本身)
即使你明确写入:
Dockerfile
!.dockerignore
这些规则也不会生效,这两个文件始终会被发送到 Docker 守护进程。
三.多阶段构建
如果不太理解多阶段构建,请去FROM指令那里看看:【Dokcer】Dockerfile指令讲解-CSDN博客
首先我们先在本目录下创建一个main.cpp
#include
using namespace std;
int main()
{
cout<<"hello docker"<
现在我们去写一下Dockerfile
FROM ubuntu:22.04
# 更新源并安装g++
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list && \
apt-get update && \
apt-get install -y g++
WORKDIR /src
COPY main.cpp .
# 编译并清理
RUN g++ main.cpp -o demo && \
apt-get remove --purge -y g++ && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* main.cpp
CMD ["/src/demo"]
注意,我们在里面安装了g++,但是在使用完g++之后,我们还是需要将g++彻底删除,以节约容器体积


完全没有问题。
- 采用多阶段构建
接下来我们对上面那个Dockerfile进行多阶段构建进行优化
FROM ubuntu:22.04 AS buildtarget1
# 更新源并安装g++
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list && \
apt-get update && \
apt-get install -y g++
WORKDIR /src
COPY main.cpp .
# 编译并清理
RUN g++ main.cpp -o demo && \
apt-get remove --purge -y g++ && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* main.cpp
FROM ubuntu:22.04 AS buildtarget2
COPY --from=buildtarget1 /src/demo /src/
CMD ["/src/demo"]

我们发现这个的镜像和之前的那个镜像的体积是完全一样的。
现在我们去比较一下两个镜像的体积
root@VM-16-14-ubuntu:~/test1# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myweb v4.8 08452177141e About a minute ago 77.9MB
myweb v4.7 7729c222efe8 7 hours ago 366MB
可以看到,使用多阶段构建的myweb:v4.8的镜像比不使用多阶段构建的myweb:v4.7的体积要小很多。
- 采用精简镜像源
我们在发布镜像的那个阶段,我们不在使用ubuntu22.04作为基础镜像源了,我们采用更为精简的镜像源busybox
FROM ubuntu:22.04 AS buildtarget1
# 更新源并安装g++
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list && \
apt-get update && \
apt-get install -y g++
WORKDIR /src
COPY main.cpp .
# 编译并清理
RUN g++ main.cpp -o demo && \
apt-get remove --purge -y g++ && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* main.cpp
FROM busybox:1.37 AS buildtarget2
COPY --from=buildtarget1 /src/demo /src/
CMD ["/src/demo"]

我们基于这个镜像启动一下容器,发现

- 第一个阶段(ubuntu)中编译的 demo 程序动态链接了 C++ 标准库
- 第二个阶段(busybox)是一个极简镜像,不包含 C++ 运行时库
- 当运行程序时,系统找不到 libstdc++.so.6 这个共享库
这个就是使用精简镜像的注意点啊!!
我们把g++编译时使用静态库编译
FROM ubuntu:22.04 AS buildtarget1
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list && \
apt-get update && \
apt-get install -y g++
WORKDIR /src
COPY main.cpp .
# 使用静态链接
RUN g++ -static main.cpp -o demo && \
apt-get remove --purge -y g++ && \
apt-get autoremove -y && \
apt-get clean && \
rm -rf /var/lib/apt/lists/* main.cpp
FROM busybox:1.37 AS buildtarget2
COPY --from=buildtarget1 /src/demo /src/
CMD ["/src/demo"]

我们基于这个镜像启动一下容器,发现

这就没有问题了
我们现在再去看看镜像的大小
root@VM-16-14-ubuntu:~/test1# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
myweb v5.0 63b27da52428 42 seconds ago 6.83MB
myweb v4.8 08452177141e 15 minutes ago 77.9MB
myweb v4.7 7729c222efe8 7 hours ago 366MB
可以看到,采用busybox镜像,又极大的缩减了镜像的大小!!
由此可得,我们如果可以合理的使用多阶段构建和使用精简镜像,就会大大的缩减镜像的大小!!
四.Dockerfile配合docker-compose.yml构建镜像和服务
4.1.docker-compose.yml的build参数
事实上,docker-compose.yml的build参数就是使用Dockerfile来构建镜像。
事实上呢,docker-compose.yml的build参数的作用就等价于docker build命令。
1. 指定构建上下文路径(最简单形式)
这是最常用、最简单的形式。你只需要提供一个构建上下文的路径。
version: '3.8'
services:
my_app:
build: .
详细解释:
build: .
- 这里的 . (一个点)是一个路径。它代表当前 docker-compose.yml 文件所在的目录。
- 当执行 docker-compose up 或 docker-compose build 时,Docker Compose 会做以下几件事:
- 定位 Dockerfile:它会在当前目录(.)下寻找一个名为 Dockerfile 的文件。这是默认行为。
- 发送构建上下文:它将当前目录(及其所有子目录和文件,除非被 .dockerignore 排除)作为“构建上下文”发送给 Docker 守护进程。你的 Dockerfile 中的指令(如 COPY . /app)就是在这个上下文中寻找文件的。
- 执行构建:Docker 守护进程根据找到的 Dockerfile 中的指令,一步步地构建出镜像。
- 标记镜像:构建成功后,该镜像会被自动赋予一个名称,通常格式为 [项目名]_[服务名]。例如,如果你的项目文件夹叫 myproject,服务名是 my_app,那么镜像名就是 myproject_my_app。
- 运行容器:最后,基于这个新构建的镜像启动一个容器。
这里的build . 就相当于docker build .
如果你想指定一个不同的目录,比如一个叫 app 的子目录:
services:
my_app:
build: ./app
Docker Compose 就会在 ./app 目录下寻找 Dockerfile。
这个就等于docker build ./app
但是,有的人可能就好奇了
我怎么指定这个生成的镜像的名称和标签啊?这个其实需要借助image参数
你经常会看到 build 和 image 一起使用:
services:
my_app:
build: . # 从哪里构建镜像
image: my_custom_app:tag # 给构建出的镜像起什么名字
它们的关系是:
- 只有 build:Compose 会构建镜像,并自动生成一个名字(如 myproject_my_app)。
- 只有 image:Compose 会直接从本地或远程仓库拉取这个指定名称的镜像来运行,不会构建。
- 两者都有:Compose 会根据 build 配置构建镜像,构建成功后,只会使用 image 指定的名称和标签来标记该镜像。
像上面这个build参数,就等价于docker build -t my_custom_app:tag .
2. 使用详细配置对象(高级形式)
当你的构建过程需要更多定制时,可以使用对象形式来配置 build 参数。
version: '3.8'
services:
my_app:
build:
context: . # 构建上下文路径(必需)
dockerfile: Dockerfile.dev # 指定 Dockerfile 文件名
args: # 构建时参数,对应 Dockerfile 中的 ARG 指令
- NODE_ENV=development
target: builder # 多阶段构建时,指定构建目标阶段
详细解释每个子参数:
context: .
- 这是对象形式中唯一必需的参数。
- 它的作用和 build: . 里的点一样,定义了构建上下文。所有在 Dockerfile 中 COPY 或 ADD 的文件,都必须是这个 context 路径下的文件。
dockerfile: Dockerfile.dev
- 指定要使用的 Dockerfile 文件名。默认是 Dockerfile。
- 如果你有不同的构建环境(如开发、生产),可以创建多个 Dockerfile,例如 Dockerfile.dev 和 Dockerfile.prod,并在这里指定。
args:
用于传递构建时参数(build-time variables),这些参数在 Dockerfile 中通过 ARG 指令定义。
示例:
Dockerfile:
ARG NODE_ENV
ENV NODE_ENV ${NODE_ENV}
RUN echo "Building for environment: $NODE_ENV"
docker-compose.yml:
build:
context: .
args:
NODE_ENV: development
当构建这个镜像时,Dockerfile 中的 $NODE_ENV 就会被替换为 development。
target: builder
- 这个参数专门用于 多阶段构建 的 Dockerfile。
- 它告诉 Docker 只构建到 Dockerfile 中指定的那个阶段(stage)为止,而不会继续后续的阶段。这在开发时非常有用,可以跳过生产环境的优化步骤,加快构建速度。
示例:
Dockerfile:
# 第一阶段:构建阶段
FROM node:16 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
# 第二阶段:生产阶段
FROM nginx:alpine
COPY --from=builder /app/build /usr/share/nginx/html
docker-compose.yml:
build:
context: .
target: builder # 只构建到 'builder' 阶段,得到一个包含构建产物的中间镜像
这样子,你就获得了builder阶段的镜像了!!
这样,在开发时你就能得到一个包含所有源码和 node_modules 的镜像,方便调试。
测试一——指定构建上下文路径(最简单形式)
首先我们的目录结构如下

docker-compose.yml
services:
mynginx:
ports:
- 8080:80
image: mytestbuildimage:v1.0
build: ./mynginx
这个build和image参数的意义就等同于docker build -t mytestbuildimage:v1.0 ./mynginx
Dockerfile
FROM nginx:1.24.0
RUN echo "hello my bit" > /usr/share/nginx/html/index.html
现在我们
docker compose up -d
我们一执行,就发现他在pull

过了几十秒后

我们发现啊, Docker 在构建镜像时,即使我们使用的是本地构建,它也会尝试从远程仓库拉取基础镜像的最新元数据(比如 nginx:1.24.0),以确保使用的镜像是最新的。
现在我们去看看镜像,果然创建好了

root@VM-16-14-ubuntu:~/test2# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
78378e70f26b mytestbuildimage:v1.0 "/docker-entrypoint.…" 56 seconds ago Up 55 seconds 0.0.0.0:8080->80/tcp, [::]:8080->80/tcp test2-mynginx-1
也是在运行中,现在我们去浏览器看看

完美符合我们的预期。
在 docker compose up 时,默认行为是会尝试拉取镜像。
如果说,我们不想拉取远端镜像,我们需要先把这个镜像构建好来。
docker compose build
docker compose up -d
docker compose build 是 Docker Compose 的一个命令,专门用于构建镜像但不运行容器。
命令作用:
- 只构建在 docker-compose.yml 中定义了 build 字段的服务镜像
- 不启动任何容器
- 相当于只执行构建步骤,跳过运行步骤

怎么样?是不是就构建好了,这个时候,我们再去执行docker compose up -d,就会发现它不会去拉取远端的镜像了

测试二——使用详细配置对象(高级形式)
首先我们的目录结构如下

docker-compose.yml
services:
mynginx2:
ports:
- 8082:80
image: mytestbuildimage:v2.0
build:
context: ./mynginx2
dockerfile: dockerfile2
dockerfile2
FROM nginx:1.24.0
RUN echo "hello my bit" > /usr/share/nginx/html/index.html
现在我们进行构建项目

还是很不错的!!
4.2.构建mysql主从复制
其实整个过程是非常简单的
- 创建主库,并在主库中创建单独的mysql用户用于同步数据,授予该用户数据同步权限
- 创建从库,配置从库的数据同步的主库信息
- 启动从库,开始同步
我们这里是为了创建一个一主二从的mysql集群

mkdir -p /data/maxhou/mysqlcluster/ && \
mkdir -p /data/maxhou/mysqlcluster/master/ && \
mkdir -p /data/maxhou/mysqlcluster/slave/ && \
cd /data/maxhou/mysqlcluster/

除此之外,我们还需要创建一些文件

那么这些文件的内容是啥?我们往下看即可
- 主节点的Dockerfile和初始化脚本
首先我们看看主节点的Dockerfile-master
FROM mysql:8.0.42
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
COPY ./master/master.sql /docker-entrypoint-initdb.d
我们看看master.sql是啥
CREATE USER 'repl'@'%' IDENTIFIED WITH mysql_native_password BY 'repl';
GRANT REPLICATION SLAVE ON *.* TO 'repl'@'%';
FLUSH PRIVILEGES;
- 从节点的Dockerfile和初始化脚本
先看Dockerfile-slave
FROM mysql:8.0.42
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='repl',master_password='repl',master_port=3306,MASTER_AUTO_POSITION=1;
start slave;
注意:master_host='mysql-master'这个是指定主机IP地址的,我们将IP地址填写成了容器名。
这就需要创建自定义网络才能让容器之间通过服务名进行通信。
- 编写docker-compose.yml
services:
mysql-master:
build:
context: ./
dockerfile: ./master/Dockerfile-master
image: mysqlmaster:v1.0
restart: always
container_name: mysql-master
volumes:
- ./mastervarlib:/var/lib/mysql
ports:
- 8306:3306
environment:
MYSQL_ROOT_PASSWORD: root
privileged: true
networks:
- mysql-cluster
command: [
'--server-id=1',
'--log-bin=master-bin',
'--binlog-ignore-db=mysql',
'--binlog_cache_size=256M',
'--binlog_format=mixed',
'--lower_case_table_names=1',
'--character-set-server=utf8',
'--collation-server=utf8_general_ci',
'--gtid-mode=ON',
'--enforce-gtid-consistency=ON'
]
mysql-slave:
build:
context: ./
dockerfile: ./slave/Dockerfile-slave
image: mysqlslave:v1.0
restart: always
container_name: mysql-slave
volumes:
- ./slavevarlib:/var/lib/mysql
ports:
- 8307:3306
environment:
MYSQL_ROOT_PASSWORD: root
privileged: true
networks:
- mysql-cluster
command: [
'--server-id=2',
'--relay_log=slave-relay',
'--lower_case_table_names=1',
'--character-set-server=utf8',
'--collation-server=utf8_general_ci',
'--gtid-mode=ON',
'--enforce-gtid-consistency=ON'
]
depends_on:
- mysql-master
mysql-slave2:
build:
context: ./
dockerfile: ./slave/Dockerfile-slave
image: mysqlslave:v1.0
restart: always
container_name: mysql-slave2
volumes:
- ./slavevarlib2:/var/lib/mysql
ports:
- 8308:3306
environment:
MYSQL_ROOT_PASSWORD: root
privileged: true
networks:
- mysql-cluster
command: [
'--server-id=3',
'--relay_log=slave-relay',
'--lower_case_table_names=1',
'--character-set-server=utf8',
'--collation-server=utf8_general_ci',
'--gtid-mode=ON',
'--enforce-gtid-consistency=ON'
]
depends_on:
- mysql-master
networks:
mysql-cluster:
driver: bridge
怕有问题的,可以先使用docker compose config检查一下
- 构建镜像
docker-compose build
我们先用这个命令来构建好我们的镜像


我们可以去查看一下,果然是构建成功了

- 启动服务,检查服务和同步状态
docker compose up -d

我们可以检查一下容器的运行情况
root@VM-16-14-ubuntu:/data/maxhou/mysqlcluster# docker compose ps
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
mysql-master mysqlmaster:v1.0 "docker-entrypoint.s…" mysql-master 30 seconds ago Up 27 seconds 33060/tcp, 0.0.0.0:8306->3306/tcp, [::]:8306->3306/tcp
mysql-slave mysqlslave:v1.0 "docker-entrypoint.s…" mysql-slave 30 seconds ago Up 27 seconds 33060/tcp, 0.0.0.0:8307->3306/tcp, [::]:8307->3306/tcp
mysql-slave2 mysqlslave:v1.0 "docker-entrypoint.s…" mysql-slave2 30 seconds ago Up 27 seconds 33060/tcp, 0.0.0.0:8308->3306/tcp, [::]:8308->3306/tcp
都是没有问题的!!
现在我们去检查一下主从复制的情况
我们先去主节点看看
docker exec -it mysql-master bash

我们发现不能使用mysql -u root -p来进行登陆,必须带上-h选项


我们现在去从节点看看
docker exec -it mysql-slave bash



去第2个节点看看
docker exec -it mysql-slave2 bash


现在都是没有问题的!!
- 测试主从复制
我们现在去主节点写入一些数据看看
docker exec -it mysql-master bash
注意我这里是root用户进行写入数据的


现在我们去从库看看
docker exec -it mysql-slave bash


可以看到,实时同步了!!
我们换一个
docker exec -it mysql-slave2 bash


非常完美!!!!!
注意:如果说你在这个期间发生任何错误,然后你想要重启服务,可以按照下面这样子即可
# 停止所有容器
docker-compose down
# 删除数据目录(注意:这会清除所有数据!)
sudo rm -rf mastervarlib slavevarlib slavevarlib2
# 重新构建并启动
docker-compose build --no-cache
docker-compose up -d
这样子就可以完美清除掉所有缓存。
4.3.搭建redis集群
首先我们直接执行下面这个命令啊
mkdir -p /data/maxhou/rediscluster/redis && \
cd /data/maxhou/rediscluster/redis && \
wget http://download.redis.io/releases/redis-7.0.11.tar.gz

现在我们进行解压
tar -xzf redis-7.0.11.tar.gz

我们进去看看redis.conf

修改下面这些内容
#表示前台运行
daemonize no
#端口
port 6379
#持久化
dir /data/redis
#启用集群
cluster-enabled yes
#集群参数配置
cluster-config-file nodes.conf
#集群超时时间
cluster-node-timeout 5000
#密码配置
requirepass 123456
#主节点密码配置
masterauth 123456
#表示远端可以连接
bind * -::*
修改好了之后,我们就保存退出

现在我们就制作我们的镜像了
来编写我们的Dockerfile
FROM ubuntu:22.04 AS buildstage
# 配置镜像源并安装构建工具
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list && \
apt update && \
apt install -y build-essential
# 添加 本地的 Redis 源码和配置文件
ADD redis-7.0.11.tar.gz /
ADD redis.conf /redis/
# 编译 Redis
WORKDIR /redis-7.0.11
RUN make
# 移动编译好的二进制文件
RUN mv /redis-7.0.11/src/redis-server /redis/ && \
mv /redis-7.0.11/src/redis-cli /redis/
FROM ubuntu:22.04
# 创建必要的目录
RUN mkdir -p /data/redis && mkdir -p /redis
# 从构建阶段复制编译结果
COPY --from=buildstage /redis /redis
# 暴露端口
EXPOSE 6379
# 设置启动命令
ENTRYPOINT ["/redis/redis-server", "/redis/redis.conf"]
然后我们就构建镜像
docker build -t myrediscluster:v0.1 .
等待4分钟后


果然是创建好了
我们基于这个镜像来启动一个容器看看
docker run -d -p 8888:6379 --name myredis1 myrediscluster:v0.1

我们发现是可以正常运行的,那就没有问题了。
接下来我们就来编写docker-compose.yml

services:
redis01:
image: myrediscluster:v0.1
build: ./redis
ports:
- 6379:6379
container_name: redis01
networks:
- redis-cluster-net
healthcheck:
test: /redis/redis-cli ping
interval: 10s
timeout: 5s
retries: 10
redis02:
image: myrediscluster:v0.1
container_name: redis02
networks:
- redis-cluster-net
healthcheck:
test: /redis/redis-cli ping
interval: 10s
timeout: 5s
retries: 10
redis03:
image: myrediscluster:v0.1
container_name: redis03
networks:
- redis-cluster-net
healthcheck:
test: /redis/redis-cli ping
interval: 10s
timeout: 5s
retries: 10
redis04:
image: myrediscluster:v0.1
container_name: redis04
networks:
- redis-cluster-net
healthcheck:
test: /redis/redis-cli ping
interval: 10s
timeout: 5s
retries: 10
redis05:
image: myrediscluster:v0.1
container_name: redis05
networks:
- redis-cluster-net
healthcheck:
test: /redis/redis-cli ping
interval: 10s
timeout: 5s
retries: 10
redis06:
image: myrediscluster:v0.1
container_name: redis06
networks:
- redis-cluster-net
healthcheck:
test: /redis/redis-cli ping
interval: 10s
timeout: 5s
retries: 10
redis07:
image: myrediscluster:v0.1
container_name: redis07
networks:
- redis-cluster-net
entrypoint: ["/redis/redis-cli", "--cluster", "create", "redis01:6379", "redis02:6379", "redis03:6379", "redis04:6379", "redis05:6379", "redis06:6379", "--cluster-replicas", "1", "-a", "123456", "--cluster-yes"]
depends_on:
redis01:
condition: service_healthy
redis02:
condition: service_healthy
redis03:
condition: service_healthy
redis04:
condition: service_healthy
redis05:
condition: service_healthy
redis06:
condition: service_healthy
networks:
redis-cluster-net:
driver: bridge
name: redis-cluster-network
接下来我们就启动一下
docker compose build

docker compose up -d

我们去看看容器的运行情况
root@VM-16-14-ubuntu:/data/maxhou/rediscluster# docker compose ps -a
NAME IMAGE COMMAND SERVICE CREATED STATUS PORTS
redis01 myrediscluster:v0.1 "/redis/redis-server…" redis01 31 seconds ago Up 30 seconds (healthy) 0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp
redis02 myrediscluster:v0.1 "/redis/redis-server…" redis02 31 seconds ago Up 30 seconds (healthy) 6379/tcp
redis03 myrediscluster:v0.1 "/redis/redis-server…" redis03 31 seconds ago Up 30 seconds (healthy) 6379/tcp
redis04 myrediscluster:v0.1 "/redis/redis-server…" redis04 31 seconds ago Up 30 seconds (healthy) 6379/tcp
redis05 myrediscluster:v0.1 "/redis/redis-server…" redis05 31 seconds ago Up 30 seconds (healthy) 6379/tcp
redis06 myrediscluster:v0.1 "/redis/redis-server…" redis06 31 seconds ago Up 30 seconds (healthy) 6379/tcp
redis07 myrediscluster:v0.1 "/redis/redis-cli --…" redis07 31 seconds ago Exited (0) 16 seconds ago
我们发现redis7退出了,这个就是正常的,它的使命就是构建集群
docker logs -f redis07
我们最后看到的是

现在我们就测试一下集群的功能

完全是没有问题的!!
4.4.搭建C++微服务
我们现在要搭建的微服务的结构如下

我们的目录结构是

- cppweb目录内
首先我们进去cppweb创建一个main.cpp

#include
#include // sockaddr_in结构体头文件
#include // memset()头文件
#include // assert()头文件
#include // close()头文件
#include // 多线程头文件
// 线程参数结构体,用于传递客户端信息
struct pthread_data{
struct sockaddr_in client_addr; // 客户端地址信息
int sock_fd; // 客户端套接字描述符
};
using namespace std;
// 线程处理函数声明
void *serverForClient(void *arg);
int main(){
int socket_fd; // 服务器套接字
int conn_fd; // 客户端连接套接字
int res; // 函数返回值
int len; // 地址长度
struct sockaddr_in sever_add; // 服务器地址结构
memset(&sever_add, 0, sizeof(sever_add)); // 初始化服务器地址结构
// 设置服务器地址参数
sever_add.sin_family = PF_INET; // IPv4协议族
sever_add.sin_addr.s_addr = htons(INADDR_ANY); // 任意本地IP地址
sever_add.sin_port = htons(8081); // 监听8081端口
len = sizeof(sever_add);
// 创建服务器套接字
int option = 1;
socket_fd = socket(AF_INET, SOCK_STREAM, 0); // TCP套接字
assert(socket_fd >= 0);
// 设置套接字选项,允许地址重用
setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &option, sizeof(option));
// 绑定套接字到指定地址和端口
res = bind(socket_fd, (struct sockaddr*)&sever_add, len);
perror("bind");
assert(res != -1);
// 开始监听,最大连接数为1
res = listen(socket_fd, 1);
assert(res != -1);
cout << "server init" << endl;
// 主循环,持续接受客户端连接
while(1){
struct sockaddr_in client; // 客户端地址
int client_len = sizeof(client);
// 接受客户端连接
conn_fd = accept(socket_fd, (struct sockaddr*)&client, (socklen_t *)&client_len);
// 准备线程参数
pthread_data pdata;
pthread_t pt;
pdata.client_addr = client;
pdata.sock_fd = conn_fd;
std::cout << "in " << conn_fd << endl;
// 创建新线程处理客户端请求
pthread_create(&pt, NULL, serverForClient, (void *)&pdata);
}
return 0;
}
// 线程处理函数,为客户端提供服务
void *serverForClient(void *arg){
struct pthread_data *pdata = (struct pthread_data*)arg;
int conn_fd = pdata->sock_fd; // 获取客户端套接字
std::cout << "process " << conn_fd << endl;
if(conn_fd < 0) {
cout << "error" << endl;
} else {
char request[1024]; // 客户端请求缓冲区
// 接收客户端请求
int len = recv(conn_fd, request, 1024, 0);
if(len <= 0){ // 接收失败或连接关闭
close(conn_fd);
return nullptr;
}
request[strlen(request) + 1] = '\0'; // 确保字符串以空字符结尾(注意:这里应该是request[len] = '\0')
// 准备HTTP响应头
char buf[520] = "HTTP/1.1 200 ok\r\nconnection: close\r\n\r\n";
// 发送HTTP响应头
int s = send(conn_fd, buf, strlen(buf), 0);
if(s <= 0){
perror("send");
return nullptr;
} else {
// HTML页面内容
char buf2[1024] = "\n"
"\n"
"\n"
"\n"
"Welcome to C++ web server! \n"
"\n"
"\n"
"\n"
"Welcome to C++ webserver!
\n"
"If you see this page, the nginx web server is successfully installed and\n"
"working. Further configuration is required.
\n"
"\n"
"Thank you for using webserver.
\n"
"\n"
"";
// 发送HTML内容
int s2 = send(conn_fd, buf2, strlen(buf2), 0);
if(s2 <= 0){
perror("send");
return nullptr;
}
// 关闭客户端连接
close(conn_fd);
}
}
return nullptr;
}
我们直接编译运行
g++ main.cpp -o mycppweb -lpthread -std=c++11

注意:我们的这个程序绑定的是8081端口号
我们使用浏览器去访问一下

完全没有问题。
现在我们在cppweb这个目录下面编写Dockerfile
# 构建阶段
FROM ubuntu:22.04 AS buildstage
# 配置apt镜像源为国内源
RUN sed -i 's@//.*archive.ubuntu.com@//mirrors.ustc.edu.cn@g' /etc/apt/sources.list && apt update
# 安装编译工具
RUN apt install -y build-essential
# 设置工作目录
WORKDIR /src
# 复制源代码
COPY ./main.cpp .
# 编译程序
RUN g++ -static main.cpp -o mycppweb -lpthread -std=c++11
# 运行阶段 - 使用轻量级运行时环境
FROM ubuntu:22.04
# 从构建阶段复制编译好的程序
COPY --from=buildstage /src/mycppweb /
# 运行程序
CMD ["/mycppweb"]
好了
- nginx目录内
接下来我们去nginx目录看看
我们编写一个Dockerfile
FROM nginx:1.24.0
COPY ./bit.conf /etc/nginx/conf.d/
CMD ["nginx", "-g", "daemon off;"]
ENTRYPOINT ["/docker-entrypoint.sh"]
我们再编写一个bit.conf
# 定义上游服务器组,用于负载均衡
upstream backend {
# 后端服务器1,权重为1(处理较少流量)
server mycppweb:8081 weight=1;
# 后端服务器2,权重为2(处理较多流量)
server mycppweb2:8081 weight=2;
}
# 定义HTTP服务器块
server {
# 监听80端口(HTTP)
listen 80;
# 关闭访问日志记录
access_log off;
# 处理所有请求路径
location / {
# 将请求代理到上游后端服务器组
proxy_pass http://backend;
}
}
- docker-compose.yml
services:
web:
image: mynginx:v3.0
build:
context: ./nginx
ports:
- 8112:80
depends_on:
mycppweb:
condition: service_started
mycppweb:
build:
context: ./cppweb
image: mycppweb:v2.0
mycppweb2:
image: mycppweb:v2.0
然后我们直接执行
docker compose build


我们就来启动一下
docker compose up -d

接下来我们启动两个容器的日志


注意这次的端口号是8112了

我们多刷新几次


可以看到,两个的负载完全不一样了

浙公网安备 33010602011771号