【Docker】镜像制作实操案例 - 详解

目录

一.C++镜像制作

二.合理使用dockerignore

三.多阶段构建

四.Dockerfile配合docker-compose.yml构建镜像和服务

4.1.docker-compose.yml的build参数

4.2.构建mysql主从复制

4.3.搭建redis集群

4.4.搭建C++微服务


一.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主从复制

其实整个过程是非常简单的

  1. 创建主库,并在主库中创建单独的mysql用户用于同步数据,授予该用户数据同步权限
  2. 创建从库,配置从库的数据同步的主库信息
  3. 启动从库,开始同步

我们这里是为了创建一个一主二从的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了

我们多刷新几次

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

posted @ 2025-12-15 09:44  yangykaifa  阅读(9)  评论(0)    收藏  举报