容器数据卷

基本概念

  • 容器数据卷是 Docker 中用于持久化存储容器数据的一种解决方案
  • 它允许容器中的数据在容器重新创建或迁移时得以保留,而不会丢失
  • 数据卷可以看作是 Docker 主机和容器之间的一个共享目录
  • 容器可以将数据写入数据卷,而这些数据将储存在 Docker 主机上
  • 如果容器被删除、重新创建或者被移动到其他主机上,数据卷依然会存在,从而保证了容器中的数据在不同主机或容器之间的无缝转移

可以通过挂载数据卷的方式使用容器数据卷,具体步骤如下:

# 创建一个数据卷 [数据卷实际上就是宿主机上的目录或者是文件]
docker volume create my-volume
# 将数据卷挂载到 Docker 容器中
docker run -d --name my-container -v my-volume:/data my-image

其中,my-volume 表示数据卷的名称,my-container 表示容器的名称,/data 表示容器内部的挂载点,my-image 表示容器运行的镜像

docker 容器中 vi 插入模式 docker volume create_学习

在数据卷被挂载到容器中后,容器就可以在 /data 目录下读写数据,这些操作将被保存在数据卷中。

当容器被删除之后,数据卷不会被自动删除,可以通过 docker volume rm 命令手动删除。

容器数据卷提供了一种简便的方式来进行数据持久化处理,使得容器中的数据能够无缝转移,同时也避免了在容器重新创建或者迁移时数据丢失的问题。

使用数据卷

方式一:直接使用 -v 选项挂载 [在创建并运行容器的时候完成挂载]

# 通过 -v 宿主机目录:容器目录 来完成挂载
docker run -it -v /home/ceshi:/home --name centos02 centos /bin/bash
# 如果目录不存在会自动为我们创建目录
exit
ls /home

docker 容器中 vi 插入模式 docker volume create_数据_02

# 开启两个终端,分别操作容器和CentOS服务器,查看是否可以同步数据

docker 容器中 vi 插入模式 docker volume create_学习_03


测试结果,无论我们在容器操作文件,还是在本地操作文件,两边都是同步的

# 我们可以通过查看容器的元数据,找到数据卷挂载的相关信息
docker inspect centos02

docker 容器中 vi 插入模式 docker volume create_容器_04

MySQL 实战

  • 如果我们用Docker里面的MySQL容器,容器删除数据就会丢失,这不是我们想要的结果
  • 通过上面的学习,我们就可以用数据卷技术将MySQL中的数据和宿主机双向绑定,实现数据持久化
  • 接下来我们就从头到尾演示一下具体是如何实现的
# 从远程镜像仓库拉取一个 mysql5.7 的镜像
docker pull mysql:5.7
# 创建并启动一个mysql容器,完成 数据卷挂载(可以挂载多个卷)、 暴露端口、设置root用户登录密码
docker run -d -p 3310:3306 -v /home/mysql/conf:/etc/mysql/conf.d -v /home/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456--name mysql03 mysql:5.7

docker 容器中 vi 插入模式 docker volume create_数据_05


使用我们的 Navicat 连接一下我们这个CentOS 系统中的 Docker 中的 MySQL 容器

docker 容器中 vi 插入模式 docker volume create_容器_06


我们在本地为这个 MySQL 中创建一个新的数据库 zwh,查看我们容器中是否同步成功

docker 容器中 vi 插入模式 docker volume create_docker_07


当我们将 mysql 容器删除后,我们本地的数据也不会丢失,MySQL 测试成功!

匿名和具名挂载

  • 匿名挂载 是指在容器内部挂载一个没有指定名称的数据卷
  • 可以更方便地控制数据卷的名称和卷的属性,使得数据更易于管理和维护
  • 具名挂载 是指在容器内部挂载一个已经存在的具有名称的数据卷
  • 更适合一次性的任务和快速的试验
  • 接下来我们就通过案例演示来完成匿名挂载和具名挂载
# 匿名挂载通过 -v 容器路径 完成
docker run -d -P --name nginx01 -v /etc/nginx nginx
# 具名挂载挂载相比于匿名挂载多了一个卷名
docker run -d -P v juming-nginx:/etc/nginx nginx
# 查看数据卷
docker volume ls

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_08

# 查看数据卷中的一些信息
docker volume inspect 卷名

docker 容器中 vi 插入模式 docker volume create_docker_09

  • 之前我们使用的数据卷都是指定了主机的目录或文件,那么匿名挂载和具名挂载都没指定,那么具体是挂载到了主机的哪个目录或文件呢?
  • 通过上面这张图我们可以知道,实际docker容器中的卷在没有指定目录的情况下
  • 都会保存在 /var/lib/docker/volumes/卷名/_data
  • 那么我们去主机的这个目录查看一下,是否和我们期待的一样 >> 结果确实如此

docker 容器中 vi 插入模式 docker volume create_学习_10

  • 这里应该引申出一个问题:为什么我们当初指定主机目录挂载的数据卷在此处没有显示?
  • docker volume ls 命令只能列出 Docker 引擎管理的数据卷,不能列出主机目录挂载为数据卷的卷,因为主机目录挂载为数据卷的目录不是由 Docker 引擎进行管理的。
  • Docker 引擎管理的数据卷是通过 Docker 创建的,并且存储在 Docker 引擎中的一个特定的目录中。Docker 引擎利用设备映射技术,将该目录挂载到容器中,容器就可以访问这些数据卷。
  • 而主机目录挂载为数据卷是通过挂载主机目录到容器中来实现,该目录是由主机(宿主机)管理的,因此 Docker 引擎无法对其进行管理和操作,所以也不会在 docker volume ls 命令输出的列表中列出。
  • 要列出主机目录挂载为数据卷的卷,可以使用 docker inspect <container> 命令来查看容器的详细信息,其中会列出容器挂载的所有数据卷信息,包括主机目录挂载为数据卷的卷。
  • 总结与拓展:
# 匿名挂载
-v 容器内路径
# 具名挂载
-v 数据卷名:容器内路径
# 指定路径挂载
-v /宿主机路径:容器内路径
# 我们在数据卷挂载的同时,还可以指定文件或目录的访问权限[ro 代表只读、rw 代表可读可写]
docker run -d -P --name nginx01 -v /etc/nginx:ro nginx
docker run -d -P --name nginx02 -v juming-nginx:/etc/nginx:rw nginx

初始Dockerfile

1️⃣ 什么是 Dockerfile?

Dockerfile是一种文本文件,用于定义如何构建Docker镜像。Dockerfile包含一系列指令,告诉Docker引擎如何处理Docker镜像的不同层。

Dockerfile通过定义Docker镜像中的基本操作,例如安装软件包、添加文件、设置环境变量和运行命令等,来构建Docker镜像。这些操作被记录在Dockerfile中的一系列指令中,并且可以按照指令的顺序排列。

Dockerfile是一个非常重要的构建工具,因为它允许开发人员创建便携式、可重复使用的Docker镜像,并确保这些镜像可以与Docker网络、存储和其他组件进行良好的交互。通过使用Dockerfile,开发人员可以定义Docker镜像中的运行时环境,并为这些镜像打上标记,以便将来可以轻松地重新部署它们。

2️⃣ 我们通过Dockerfile构建一个镜像:

# 来到我们主机/home目录下创建dockerfile
cd /home
mkdir docker-test-volume
touch docker-test-volume/dockerfile01
# 编辑我们的 dockerfile01
vim docker-test-volume
	# 我们以centos作为构建镜像的基础
	FROM centos
	# 设置匿名数据卷挂载
	VOLUME ["volume01", "volume02"]
	# 输出测试前面的部分构建成功
	CMD echo "----build success----"
	# 设置默认走 bash 控制台
	CMD /bin/bash
# 保存文件后根据 dockerfile01 开始构建镜像
docker build -f /home/docker-test-volume/dockerfile01 -t zwh/centos .
  • 对于构建命令的参数说明:
  • -f 代表我们 dockerfile 的路径 【绝对路径和相对路径都可以】
  • -t 代表给镜像打上标签名,这个镜像将被标记为 zwh/centos
  • .:表示当前上下文路径,即Dockerfile所在的目录。Docker将使用这个路径下的所有文件作为构建上下文 【.代表当前路径】

docker 容器中 vi 插入模式 docker volume create_数据_11


可以看到我们的镜像是分层构建的,构建结束后已经可以在本地查看到我们的镜像了

3️⃣ 在第二次构建的时候,我发现一个问题:

在使用Dockerfile构建Docker镜像的过程中,很容易出现两个镜像ID相同的情况。这通常是由于缓存机制引起的。

当构建过程中的某个步骤发生更改时,Docker通常会缓存该步骤之前的所有步骤,以加速后续的构建过程。如果之后的构建中没有任何更改,则Docker将直接使用缓存中的结果,这有助于提高构建速度。

docker 容器中 vi 插入模式 docker volume create_学习_12

当然,使用缓存机制也可能导致问题。假设使用了同一个Dockerfile构建了两个镜像,第二个构建过程中没有任何更改,因此Docker使用了缓存。在这种情况下,两个镜像具有相同的ID,因为它们共享同一组缓存。

为避免这种情况,可以在构建过程中使用–no-cache选项来禁用缓存,这将强制Docker忽略缓存并重新执行每个步骤。

4️⃣ 基于我们使用dockerfile创建的镜像构造容器

# 创建并启动容器
docker run -it 容器ID /bin/bash
# 查看容器中的文件和目录 >> 发现了我们定义的匿名数据卷
ls

docker 容器中 vi 插入模式 docker volume create_学习_13

# 退出容器
exit
# 查看容器ID
docker ps -a
# 查看容器详细信息,找到数据卷挂载的内容
docker inspect 容器ID

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_14

  • 可以看到我们设置的匿名挂载数据卷挂载到了正确的位置
  • 我们采用创建镜像的时候就直接指定了 匿名挂载数据卷,也可以在创建容器的时候通过 -v 实现数据卷挂载

数据卷容器

1️⃣ 什么是数据卷容器?

数据卷容器是一种Docker容器,其目的是用于管理和共享一个或多个数据卷。它通常是通过在一个容器中创建一个或多个数据卷,并在需要数据存储的其他容器中使用这些数据卷来实现的。
数据卷容器可以提供以下好处:

  • 可以在多个容器之间共享数据,无需每次重新复制或使用复杂的共享方式
  • 可以更好地管理数据和其元数据,以及为特定应用程序分离数据存储和代码/二进制文件存储
  • 可以更轻松地备份和还原数据,以及在容器之间进行数据移动
  • 可以更好地组织和保护存储数据

docker 容器中 vi 插入模式 docker volume create_数据_15

通过使用数据卷容器,你可以在需要时轻松地管理和共享多个数据卷,而不必手动创建和挂载这些数据卷。数据卷容器可以让你更好地管理整个应用程序,专注于以容器为主体的开发和部署方式,而不是关注数据管理方面的细节。

2️⃣ 案例演示:开启三个centos容器,将二号容器和三号容器挂载到一号容器的数据卷,查看数据卷挂载情况

docker 容器中 vi 插入模式 docker volume create_学习_16

# 创建并启动容器 centosA
docker run -it --name centosA zwh/centos
# 创建并启动容器 centosB
docker run -it --name centosA --volumes-from centosA zwh/centos
# 创建并启动容器 centosC
docker run -it --name centosA --volumes-from centosA zwh/centos
# 创建并启动容器 centosD
docker run -it --name centosA --volumes-from centosB zwh/centos
  • 因为我这个 centos 镜像是我之前通过 dockerfile 生成的,指定了数据卷,所以这里就没设置挂载
  • 我们让centosA作为数据卷容器,将他的数据卷挂载到我们的 centosBcentosC
  • 然后将 centosB 容器的数据卷挂载到我们的 centosD 容器中 【为了后面的测试做准备】
  • 就是将一个已存在的容器中的一个或者多个数据卷挂载到新的容器中

docker 容器中 vi 插入模式 docker volume create_数据_17

# 我们查看容器的详细数据,来看他们的数据卷挂载情况
docker inspect centosA
docker inspect centosB
docker inspect centosC
docker inspect centosD

docker 容器中 vi 插入模式 docker volume create_学习_18

  • 这个图片截取的不全,通过这个图片我想说明:对于这centosA、B、C、D四个容器的数据卷目录是相同的
  • 这也就实现了容器的数据共享,我们来还想测试:
  • 四个容器数据变化同步 -> 通过在一个容器中添加修改文件,再其他三个容器中查看
  • 当我们的源头数据卷容器删除后,对于 centosA 容器 和 centosB 容器的数据还是共享的吗
  • 如果只剩我们的 centosD 容器,在数据发生变化后,其他三个容器重启后数据会和 centosD容器数据保持一直吗
  • 结果是肯定的,因为这几个容器的数据卷绑定的是主机的同一个目录
  • 使用 --volumes-from 实现数据卷容器,在之后容器数据变化是一个双向拷贝的过程

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_19

3️⃣ 实战分析:如何实现多个MySQL容器的数据共享?

  • 无论采用-v 挂载和 --volumes-from 选项绑定数据卷容器 都可以实现数据共享
  • 接下来通过代码来演示具体是如何完成的
# 创建并启动mysql01容器 [-P 代表容器内部端口随机映射到主机的端口]
docker run -d -p 3306:3306 -v /home/mysql/conf:/etc/mysql/conf.d -v /home/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name mysql01 mysql:5.7
# 创建并启动mysql02容器
docker run -d -p 3310:3306 -e MYSQL_ROOT_PASSWORD=123456 --volumes-from mysql01 --name mysql02 mysql:5.7
  • 这样我们 mysql01 容器和 mysql02 容器就可以实现数据共享了
  • 容器之间的配置信息的传递,数据卷容器的生命周期一直持续到没有容器使用为止
  • 但是一旦你持久化到了本地,这个时候,本地的数据是不会删除的

DockerFile

DockerFile 介绍

  • dockerfile 是用来构建 docker 镜像的文件, 属于一个 命令参数脚本
  • 利用 dockerfile 构建镜像的步骤:
  • 编写一个 dockerfile 文件
  • docker build 构建成一个镜像
  • docker run 基于镜像创建并运行容器
  • docker push 发布镜像到远程仓库

docker 容器中 vi 插入模式 docker volume create_数据_20


当我们选择一个具体的版本,就会跳转到对应的 GitHub 仓库

docker 容器中 vi 插入模式 docker volume create_数据_21

很多官方镜像都是基础包,很多功能没有,我们通常会基于官方提供的镜像搭建自己的镜像

DockerFile 构建过程

  • 首先,我们要知道一些基础知识
  • DockerFile 中使用的每个保留关键字都是由大写字母构成的
  • 构建时采用从上到下的执行顺序
  • 每条指令都会创建一个新的镜像层并提交
  • # 开头的内容表示注释信息
  • DockerFile文件中的指令可以分为多个部分,每个部分都对应于不同的构建层,以下是一般的DockerFile 构建过程:
  • 基础镜像指令
  • 使用 FROM 关键词声明我们所使用的基础镜像
  • 例如 FROM alpine:3.14 就表示以 alpine:3.14 作为基础镜像来构建我们的镜像。
  • 镜像元数据指令
  • 通过一些指令为我们构建的镜像添加元数据信息,包括我们的姓名、版本、描述、维护者等,例如:
LABEL maintainer="Docker User <docker@user.com>"
LABEL version="1.0"
LABEL description="My web application"
  • 环境变量指令
  • 使用 ENV 关键词为镜像设置环境变量,例如:
ENV JAVA_VERSION=11
ENV APP_HOME=/app
  • 安装软件包指令
  • 使用 RUN 关键词安装需要的软件包,例如: RUN apt-get update &amp;&amp; apt-get install -y openssh-server
  • 拷贝文件指令
  • 使用 COPYADD 关键词从本地文件系统或其他镜像中复制文件和目录到镜像中,例如: COPY app.jar $APP_HOME/
  • 数据卷指令
  • 使用 VOLUME 关键词为镜像创建数据卷,例如: VOLUME [ "/data" ]
  • 入口点指令
  • 使用 ENTRYPOINT 或 CMD 关键词为镜像设置默认启动命令或者入口点,例如:
  • ENTRYPOINT [ "java", "-jar", "/app/app.jar" ]

docker 容器中 vi 插入模式 docker volume create_容器_22

  • 以上步骤中的每个指令都会在Docker引擎中创建一个新的构建层,最终构建成一个完整的镜像。
  • 当执行 docker build 命令时,Docker会从 Dockerfile 文件中读取这些指令,并自动执行相应的操作,最终生成一个新的Docker镜像。

DockerFile 指令

点击查看 DockerFile指令官方文档

DockerFile中,指令是用来构建镜像并定义镜像行为的关键字。以下是一些常见的DockerFile指令:

命令

作用

FROM

用于指定构建镜像的基础镜像

MAINTAINER

用于指定镜像的维护者信息

RUN

用于在当前镜像中执行命令并创建新的镜像层

CMD

指定容器启动后默认执行的命令或者可执行文件,可以有多个CMD指令,但只有最后一个会生效

LABEL

为镜像添加自定义的元数据

EXPOSE

声明容器运行时监听的网络端口

ENV

用于设置环境变量

ADD

将本地文件、归档文件或远程文件复制到容器中

COPY

将本地文件复制到容器中

ENTRYPOINT

定义容器启动时执行的命令

VOLUME

声明容器数据卷

USER

指定容器的运行用户

WORKDIR

用于指定容器中的工作目录

实战练习

我们可以看一下官方Hubscratch 镜像的 DockerFile

FROM scratch
ADD centos-7-x86_64-docker.tar.xz /

LABEL \
    org.label-schema.schema-version="1.0" \
    org.label-schema.name="CentOS Base Image" \
    org.label-schema.vendor="CentOS" \
    org.label-schema.license="GPLv2" \
    org.label-schema.build-date="20200504" \
    org.opencontainers.image.title="CentOS Base Image" \
    org.opencontainers.image.vendor="CentOS" \
    org.opencontainers.image.licenses="GPL-2.0-only" \
    org.opencontainers.image.created="2020-05-04 00:00:00+01:00"

CMD ["/bin/bash"]

Docker Hub 中 99%的镜像都是从这个基础镜像过来的 FROM scratch,然后配置需要的软件和配置来进行构建

1️⃣ 定义一个 centos 镜像

  • 因为官方为我们提供的 centos 镜像是具备运行环境的最小版本,我们想使用 ifcongif 命令都用不了
  • 所以我们就通过 Dockerfile 自己构建一个 centos 镜像出来
# 编辑 DockerFile

docker 容器中 vi 插入模式 docker volume create_数据_23

# 构建镜像
docker build -f mydockerfile-centos -t mycentos:0.1 .

docker 容器中 vi 插入模式 docker volume create_容器_24

# 使用我们的镜像创建并启动一个centos容器
docker run -it --name mycentos01 mycentos:0.1
# 测试我们安装的程序包对应的功能
pwd
ifconfig

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_25


功能可以正常使用,所以我们指定的资源包成功下载并构建到了我们的镜像中

2️⃣ 查看镜像的构建过程

通过 docker history 镜像名|镜像ID 可以查看镜像的构建过程

docker 容器中 vi 插入模式 docker volume create_容器_26

3️⃣ CMD 和 ENTRYPOINT 指令都用于指定在容器启动时所要执行的命令,它们有什么区别?

  • CMD 指令用于指定容器启动后默认执行的命令,一个 Dockerfile 可以包含多个 CMD 指令,但是只有最后一个 CMD 指令会生效。当在启动容器时指定了命令,则会覆盖 CMD 中设置的默认命令。
  • ENTRYPOINT 指令用于指定运行容器时要执行的命令。与 CMD 不同,ENTRYPOINT 指令的参数不会被覆盖,而是会作为 CMD 命令的参数进行传递。如果希望覆盖 ENTRYPOINT 中设置的命令,可以在 docker run 命令行中使用 –entrypoint 参数。
  • 简单来说,CMD 指令指定了容器启动后要执行的默认命令和参数,但这些命令和参数可以在运行容器时覆盖,而 ENTRYPOINT 指令指定了要在容器启动时执行的命令,这些命令和参数在运行容器时不会被覆盖

例如,下面的 Dockerfile 中定义了两个 CMD 指令和一个 ENTRYPOINT 指令:

FROM ubuntu:latest
CMD ["echo", "Hello world!"]
CMD ["echo", "Goodbye!"]
ENTRYPOINT ["echo", "RUNNING: "]

当运行容器时不带任何参数时,会先输出 "RUNNING: ",然后执行最后一个 CMD 指令,输出 “Goodbye!”:

$ docker run my_image
RUNNING: Goodbye!

当在运行容器时传递参数时,这些参数会被传递给 CMD 指令并覆盖默认参数:

$ docker run my_image echo "Hello Docker!"
RUNNING: Hello Docker!

4️⃣ 通过测试发现,ENTRYPOINT 中的指令一定会比CMD中的指令先执行

  • 实际上,Docker 在启动容器时会将 ENTRYPOINTCMD 指令合并成一个完整的命令。
  • 具体来说,Docker 在启动容器时会执行以下步骤:
  • 拼接 ENTRYPOINTCMD 指令,生成一个完整的命令。
  • 如果 ENTRYPOINT 指令使用了 exec 格式 【通过语法判断 json数组】
  • CMD 指令中的参数会被作为 ENTRYPOINT 命令的参数传递;
  • 否则,CMD 指令会覆盖 ENTRYPOINT 指令中的命令。

举个例子,如果以下是Dockerfile的内容:

FROM ubuntu:latest
ENTRYPOINT ["echo", "Hello,"]
CMD ["World!"]

当运行容器时,将会执行以下命令:

$ docker run my_image
Hello, World!

这是因为 ENTRYPOINT 指令的命令 “echo” 会先执行,然后将 CMD 中的 “World!” 参数传递给 ENTRYPOINT 指令中的命令来组成完整的命令 “echo Hello, World!”

因此,如果在 ENTRYPOINT 中指定了一个可执行文件或脚本,它会在任何 CMD 命令之前执行。
如果需要在 ENTRYPOINT 中执行一些初始化代码或设置,这是非常有用的。

制作Tomcat镜像

1️⃣ 准备好 tomcat 和 jdk 的压缩包

# 在 /home/zwh/tomcat 目录下存放我们的压缩包
cd /home/zwh/tomcat
# 下载 lrzsz后,通过拖拽到XShell实现文件上传
yum install lrzsz

docker 容器中 vi 插入模式 docker volume create_docker_27


docker 容器中 vi 插入模式 docker volume create_docker_28


2️⃣ 编写 Dockerfile 文件

vim Dockerfile
# 基础镜像
FROM centos:7
# 作者
MAINTAINER zwh<2192464651@qq.com>
# 拷贝 README 文件
CPOPY README /usr/local/README
# 添加JDK压缩包,会自动解压
ADD jdk-8u231-linux-x64.tar.gz /usr/local/ 	
# 添加Tomcat压缩包,会自动解压
ADD apache-tomcat-9.0.35.tar.gz /usr/local/ 	
# 安装 vim 命令
RUN yum install vim
# 环境变量设置
ENV MYPATH /usr/local
# 设置工作目录
WORKDIR $MYPATH
# 设置JAVA环境变量
ENV JAVA_HOME /usr/local/jdk1.8.0_231
EVN CLASS_PATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
# 设置Tomcat环境变量
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.35 	
ENV CATALINA_BASH /usr/local/apache-tomcat-9.0.35
# 设置PATH环境变量
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin 	
# 设置暴露的端口
EXPOSE 8080
# 设置进入容器默认执行的命令 [启动Tomcat并执行日志]
CMD /usr/local/apache-tomcat-9.0.35/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.35/logs/catalina.out

3️⃣ 构建镜像

# 对于我们使用了默认的命名 Dockerfile,就可以不用指定我们DockerFile的路径
docker build -t mycentos:0.1 .

4️⃣ 创建容器

# 创建并运行我们的 tomcat
docker run -d -p 8080:8080 --name tomcat01 
-v /home/kuangshen/build/tomcat/test:/usr/local/apache-tomcat-9.0.35/webapps/test 
-v /home/kuangshen/build/tomcat/tomcatlogs/:/usr/local/apache-tomcat-9.0.35/logs mytomcat:0.1

5️⃣ 访问测试

docker 容器中 vi 插入模式 docker volume create_数据_29

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd" version="2.4">  

</web-app>

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>hello wangjingkai</title>
</head>
<body>
Hello World!<br/>
<%
System.out.println("---my test web log");
%>
</body>
</html>

发布镜像

(1)发布到 DockerHub

1️⃣ 先注册一个 DockerHub 的账户,后续会使用到用户名和密码

2️⃣ 可以登录到我们的 DockerHub

# 密码是隐藏起来的
docker login -u zhaowenhan

docker 容器中 vi 插入模式 docker volume create_docker_30

# 退出登录
docker logout

docker 容器中 vi 插入模式 docker volume create_docker_31


3️⃣ 发布镜像到仓库

# 发布前要登录到我们的DockerHub账户
Docker login -u zhaowenhan
# 推送镜像到仓库
docker push mycentos:0.1

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_32

  • 发现我们的请求被拒绝,那么应该怎么办呢?【Docker Hub 只允许向带有特定前缀的仓库推送镜像】
  • 对于我们创建的镜像如果没有前缀的话,默认会发布到官方的 library
  • 解决方案一:在构建镜像的时候添加 dockerhub 用户名作为前缀,例如
  • docker build -t zhaowenhan/mycentos0.1
  • 解决方案二:在发布前使用 docker tag 命令将本地的 Docker 镜像打标签
  • docker tag 容器ID zhaowenhan/mycentos0.1
  • 然后再通过 docker push zhaowenhan/mycentos0.1 就可以成功发布到 DockerHub啦

(2)发布镜像到阿里云镜像服务上

1️⃣ 来到阿里云官网找到镜像服务,并创建个人实例

docker 容器中 vi 插入模式 docker volume create_容器_33


2️⃣ 设置Registry的登录密码

docker 容器中 vi 插入模式 docker volume create_学习_34

3️⃣ 设置命名空间

docker 容器中 vi 插入模式 docker volume create_docker_35


4️⃣ 创建镜像仓库 【选择本地仓库】

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_36

5️⃣ 最后根据阿里云的操作指南完成镜像的发布

1. 登录阿里云Docker Registry
$ docker login --username=zwh registry.cn-wulanchabu.aliyuncs.com
用于登录的用户名为阿里云账号全名,密码为开通服务时设置的密码。

您可以在访问凭证页面修改凭证密码。

2. 从Registry中拉取镜像
$ docker pull registry.cn-wulanchabu.aliyuncs.com/bonbons/bonbons:[镜像版本号]
3. 将镜像推送到Registry
$ docker login --username=zwh registry.cn-wulanchabu.aliyuncs.com
$ docker tag [ImageId] registry.cn-wulanchabu.aliyuncs.com/bonbons/bonbons:[镜像版本号]
$ docker push registry.cn-wulanchabu.aliyuncs.com/bonbons/bonbons:[镜像版本号]
请根据实际镜像信息替换示例中的[ImageId]和[镜像版本号]参数。

4. 选择合适的镜像仓库地址
从ECS推送镜像时,可以选择使用镜像仓库内网地址。推送速度将得到提升并且将不会损耗您的公网流量。

如果您使用的机器位于VPC网络,请使用 registry-vpc.cn-wulanchabu.aliyuncs.com 作为Registry的域名登录。

5. 示例
使用"docker tag"命令重命名镜像,并将它通过专有网络地址推送至Registry。

$ docker images
REPOSITORY                                                         TAG                 IMAGE ID            CREATED             VIRTUAL SIZE
registry.aliyuncs.com/acs/agent                                    0.7-dfb6816         37bb9c63c8b2        7 days ago          37.89 MB
$ docker tag 37bb9c63c8b2 registry-vpc.cn-wulanchabu.aliyuncs.com/acs/agent:0.7-dfb6816
使用 "docker push" 命令将该镜像推送至远程。

$ docker push registry-vpc.cn-wulanchabu.aliyuncs.com/acs/agent:0.7-dfb6816

通过一张图来总结一下相关内容:

docker 容器中 vi 插入模式 docker volume create_学习_37


Docker 网络

理解 Docker0

# 删除所有容器
docker stop $(docker ps -aq)
# 查看 Linux 中的网卡信息
ip addr

docker 容器中 vi 插入模式 docker volume create_docker_38


1️⃣ 我们思考一个问题,docker 是如何处理容器内网络访问的呢?

# 我们创建一个 tomcat 容器
docker run -it --name tomcat01 tomcat
# 进入到容器内部并查看IP信息
docker exec -it 容器ID ip addr

但是发现出现 ip 命令不可用的问题,所以我打算基于官方的 tomcat 构建一个可以使用 ip addr 的镜像

# 编写 Dockerfile
FROM tomcat
RUN apt-get update && apt-get install -y iproute2
# 构建镜像
docker build -t

因为构建中下载太慢了,所以我就放弃了,选择在我们的 tomcat01 容器中去下载对应资源包

# 进入我们的 tomcat 容器
docker exec -it tomcat01 /bin/bash
# 下载资源
apt update && apt install -y iproute2
# 重新查看网卡信息
ip addr

docker 容器中 vi 插入模式 docker volume create_数据_39


2️⃣ 我们思考一个问题,容器与我们Linux宿主机是否可以相互 Ping 通呢?

# 如果我们容器用不了 Ping 命令
apt -y install iputils-ping
# 先进入容器Ping我们的 docker0
ping 127.17.0.1
# 退出容器再ping我们的tomcat容器
ping 127.17.0.2

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_40


为什么会出现这样的情况呢?

Docker 中,通常会创建一个虚拟网桥 docker0,它用于将 Docker 容器连接到宿主机的网络。
当启动 Docker 容器时,Docker 引擎会自动创建一个虚拟网络接口 veth 对,一端将被连接到容器,另一端被连接到 docker0 网桥上,从而使容器可以访问宿主机网络和其他容器。

3️⃣ 我们思考一个问题,对于 Docker 中容器之间可以Ping通吗?

# 因为用官方的阉割版tomcat创建tomcat容器还要重新下载资源,所以我基于下载的这个tomcat01创建个镜像
docker commit -m="new tomcat" -a="zwh" tomcat01 new_tomcat
# 用我们新tomcat镜像再创建一个tomcat容器
docker run -d -P --name tomcat02 new_tomcat
# 查看 tomcat02 容器的IP信息
docker exec -it tomcat02 ip addr
# 进入 tomcat02 容器
docker exec -it tomcat02 /bin/bash
# Ping我们的 tomcat01 容器
ping 172.17.0.2
# Ctrl + P + Q 退出但不关闭容器
# 进入 tomcat01 容器
docker exec -it tomcat01 /bin/bash
# Ping我们的 tomcat02 容器
ping 172.17.0.3

docker 容器中 vi 插入模式 docker volume create_学习_41


Docker 中,每个容器都运行在独立的网络命名空间中,拥有自己的独立 IP 地址和网络接口。默认情况下,Docker 使用 bridge 驱动程序来创建一个虚拟网桥 docker0,作为宿主机与容器之间通信的桥梁。

当您启动一个容器时,Docker 引擎会创建一个虚拟网络接口 veth 对,在容器的网络命名空间内,将一端附加到容器内部并且另外一端附加到 docker0 网桥上。这样一来,容器就可以与其他容器、宿主机进行通信。

docker 容器中 vi 插入模式 docker volume create_学习_42

例如,当您在一个容器中执行 ping 命令时,网络栈会将 IP 包发送到容器的网络接口上,并经过 veth 接口和 docker0 网桥发送给目标容器。在目标容器中,网络栈会接收到此 IP 包并进行响应,从而使容器之间可以互相ping通。

此外,Docker 还为每个容器分配了唯一的 IP 地址,这一点非常重要。通过这种方式,容器可以通过它们的 IP 地址直接通信,而不必依赖主机名或其他映射。这在构建容器化应用程序时非常重要,因为它简化了应用程序之间的通信,并避免了对外部网络进行暴露。

4️⃣ 小结:

Docker使用的是Linux的桥接,宿主机是一个Docker容器的网桥 docker0

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_43


Docker中所有网络接口都是虚拟的,虚拟的转发效率高(内网传递文件);只要容器删除,对应的网桥一对就没了!

Link

docker --link一种在容器之间创建链接的方法,通常用于在容器之间实现网络通信
使用 docker --link,可以将一个容器连接到另一个容器,并且在环境变量中传递有用的信息以进行通信。

以下是使用 docker --link 链接容器的一些基本步骤:

# 首先启动源容器,并为其指定一个名称或 ID:
$ docker run -it --name source_container your_image /bin/bash

# 然后启动目标容器,并通过 --link 选项将其连接到源容器:
$ docker run -it --name target_container --link source_container your_image /bin/bash

# 完成以上步骤后,源容器会被附加到目标容器的网络命名空间中,并使用自动生成的名称(例如 SOURCE_CONTAINER_NAME_PORT)创建一个环境变量。您可以使用该环境变量来访问源容器中的资源,如下所示:
$ echo $SOURCE_CONTAINER_NAME_PORT
tcp://172.17.0.2:8080

在上面的示例中,源容器的 IP 地址为 172.17.0.2,它的端口为 8080。

需要注意的是,由于 docker --link 不再被推荐使用,官方文档建议使用 docker network 来管理容器之间的网络连接。

推荐使用 docker network 的原因在于其提供更灵活的网络配置选项,并且更易于维护和扩展

如果我们 tomcat01 容器 通过 --link 选项链接到了 tomcat02 容器(单项链接),那么在 tomcat01 容器内部可以通过 ping tomcat02 成功

自定义网络

1️⃣ 查看docker中的所有网络

docker network ls

docker 容器中 vi 插入模式 docker volume create_docker_44


我们再看看 network 的其他用法

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_45

2️⃣ 常见的网络模式有哪些呢?

bridge :桥接 docker 【默认,自己创建也是用bridge模式】

none :不配置网络,一般不用

host :和所主机共享网络

container :容器网络连通 【因为局限大,所以很少使用】

3️⃣ 案例演示

  • 对于我们创建的容器,默认网络使用的是 --net bridge 桥接模式,我们可以自定义一个网络
  • 在自定义网络之前,我们先了解一下 docker network create 命令的一些参数
  • --driver:指定网络类型,常用的类型包括 bridge, overlay 等,默认为 bridge
  • --subnet:指定 IP 子网,格式为 IP/mask,例如 --subnet=172.18.0.0/16。
  • --gateway:指定网络网关 IP,如果不指定,则自动分配一个。
  • --ip-range:指定 IP 范围,格式为 起始 IP-结束 IP,例如 --ip-range=172.18.0.10-172.18.0.20。
  • --ipv6启用 IPv6 支持
  • --attachable指定网络是否可附加到容器上,默认为 false。
  • --internal指定网络是否只允许容器之间通信,默认为 false。
  • --opt指定其他选项,格式为 key=value。
  • 举例来说,要创建一个名为 my-netbridge 网络,子网为 192.168.0.0/16,可以使用以下命令:
  • docker network create --driver=bridge --subnet=192.168.0.0/16 my-net
  • 创建完毕后可以使用 docker network ls 命令查看所有网络列表。

接下来创建我们自定义的网络

# driver 代表网络类型、subnet 代表子网IP、gateway 代表网关IP
docker network create --driver=bridge --subnet=192.168.0.0/16 --gateway=192.168.0.1 mynet
# 查看自定义网络的详细信息
docker network inspect mynet

docker 容器中 vi 插入模式 docker volume create_容器_46

网关IP和子网IP是TCP/IP网络中的两个重要的概念,那么有什么区别呢?

子网IP(Subnet IP) 是指将同一网络划分成多个子网,每个子网有自己的IP地址范围,在子网内的主机可以直接通信,而不需要通过路由器。这种划分子网的方式可以提高网络的性能和安全性。子网IP的掩码标识了哪些部分是网络地址,哪些部分是设备地址。例如,如果子网掩码为255.255.255.0,则前三个数字表示网络地址,后一个数字表示主机地址。

网关IP(Gateway IP) 是指在一个子网中,连接该子网和本地网络之间的设备或者计算机(通常是路由器)。所有离开该子网的数据包都需要通过网关发送到其它子网或者互联网上。网关IP一般是该子网的第一个IP地址或者最后一个IP地址,也可以是任意的IP地址,只要网络设备可以识别并使用它即可。

简而言之,子网IP是指一个网络中划分出来的多个子网中的每个子网所拥有的独立地址范围,而网关IP是指每个子网所连向的上一层网络的地址

在我们自定义的网络中创建两个 tomcat 容器

# 创建我们 tomcat-net-01 容器
docker run -d -P --name tomcat-net-01 --net mynet new_tomcat
# 创建我们 tomcat-net-02 容器
docker run -d -P --name tomcat-net-02 --net mynet new_tomcat
# 再次查看我们 mynet 网络的详细信息 >> 发现这两个容器成功添加进来了
docker network inspect mynet

docker 容器中 vi 插入模式 docker volume create_docker 容器中 vi 插入模式_47

测试两个容器的网络联通性

# 通过容器名 Ping
docker exec -it tomcat-net-01 ping tomcat-net-02
docker exec -it tomcat-net-02 ping tomcat-net-01
# 通过容器内网络IP Ping
docker exec -it tomcat-net-01 ping 192.168.0.3
docker exec -it tomcat-net-02 ping 192.168.0.2
# 测试结果 >> 全部都达到了预期效果

docker 容器中 vi 插入模式 docker volume create_学习_48


优点: 同一个集群可以使用一个集群,实现了服务互通,不同的集群使用不同的网络,又可以实现隔离

docker 容器中 vi 插入模式 docker volume create_学习_49

网络连通

  • 这部分我们想实现不同网段的容器网络连通,如果通过两个网卡之间建立连接时完全不可能的
  • 接下来通过案例演示这部分的功能是如何实现的:
# 先启动我们之前创建的两个容器 tomcat01 和 tomcat02
docker start tomcat01
docker start tomcat02
# 让我们的 tomcat-net-01 容器去 Ping tomcat01 容器
docker exec -it tomcat-net-01 ping tomcat01

docker 容器中 vi 插入模式 docker volume create_学习_50

# 我们可以将 centos01 容器添加到我们 tomcat-net-01 所在的网络中
docker network connect mynet tomcat01
# 再次查看我们 mynet 网络的信息
docker network inspect mynet

docker 容器中 vi 插入模式 docker volume create_学习_51

# 用 tomcat-net-01 去 Ping tomcat01 >> 网络连通
docker exec -it tomcat-net-01 ping tomcat01

docker 容器中 vi 插入模式 docker volume create_学习_52


因为 tomcat01 加入到了这两个容器所在的网络,所以他们之前的网络是连通的

但是这两个容器和 tomcat02 不在一个网络中,所以他们之间的网络是 Ping 不通的

结论:假设要跨网络操作其他容器,就需要使用docker network connect 连通

部署 Redis 集群

  • 我们要实现三主三从的Redis集群,接下来通过代码来演示如何完成
# 搭建一个Redis集群专用的网络
docker nerwork create --driver=bridge --subnet=172.38.0.0/16

# 通过脚本快速创建六个Redis容器
for port in $(seq 1 6); \
do \
mkdir -p /mydata/redis/node-${port}/conf
touch /mydata/redis/node-${port}/conf/redis.conf
cat  << EOF >> /mydata/redis/node-${port}/conf/redis.conf
port 6379 
bind 0.0.0.0
cluster-enabled yes 
cluster-config-file nodes.conf
cluster-node-timeout 5000
cluster-announce-ip 172.38.0.1${port}
cluster-announce-port 6379
cluster-announce-bus-port 16379
appendonly yes
EOF
done

docker 容器中 vi 插入模式 docker volume create_容器_53

#启动一个redis
docker run -p 6371:6379 -p 16371:16379 --name redis-1 \
    -v /mydata/redis/node-1/data:/data \
    -v /mydata/redis/node-1/conf/redis.conf:/etc/redis/redis.conf \
    -d --net redis --ip 172.38.0.11 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
    
#启动第二个redis
docker run -p 6372:6379 -p 16372:16379 --name redis-2 \
    -v /mydata/redis/node-2/data:/data \
    -v /mydata/redis/node-2/conf/redis.conf:/etc/redis/redis.conf \
    -d --net redis --ip 172.38.0.12 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf

#启动第三个redis
docker run -p 6373:6379 -p 16373:16379 --name redis-3 \
    -v /mydata/redis/node-3/data:/data \
    -v /mydata/redis/node-3/conf/redis.conf:/etc/redis/redis.conf \
    -d --net redis --ip 172.38.0.13 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
#启动第四个redis
docker run -p 6374:6379 -p 16374:16379 --name redis-4 \
    -v /mydata/redis/node-4/data:/data \
    -v /mydata/redis/node-4/conf/redis.conf:/etc/redis/redis.conf \
    -d --net redis --ip 172.38.0.14 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf
   
#启动第五个redis
docker run -p 6375:6379 -p 16375:16379 --name redis-5 \
    -v /mydata/redis/node-5/data:/data \
    -v /mydata/redis/node-5/conf/redis.conf:/etc/redis/redis.conf \
    -d --net redis --ip 172.38.0.15 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf

#启动第六个redis
docker run -p 6376:6379 -p 16376:16379 --name redis-6 \
    -v /mydata/redis/node-6/data:/data \
    -v /mydata/redis/node-6/conf/redis.conf:/etc/redis/redis.conf \
    -d --net redis --ip 172.38.0.16 redis:5.0.9-alpine3.11 redis-server /etc/redis/redis.conf

docker 容器中 vi 插入模式 docker volume create_docker_54

# 创建三主三从的集群关系
redis-cli --cluster create 172.38.0.11:6379 172.38.0.12:6379 172.38.0.13:6379 172.38.0.14:6379 172.38.0.15:6379 172.38.0.16:6379 --cluster-replicas 1

docker 容器中 vi 插入模式 docker volume create_数据_55

# 接下来的代码我们模拟一个master宕机,从机上位的情况
/data # redis-cli -c
127.0.0.1:6379> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:1
cluster_stats_messages_ping_sent:174
cluster_stats_messages_pong_sent:175
cluster_stats_messages_sent:349
cluster_stats_messages_ping_received:170
cluster_stats_messages_pong_received:174
cluster_stats_messages_meet_received:5
cluster_stats_messages_received:349

127.0.0.1:6379> cluster nodes
b4607c2b19afd41f034ab49dac4dd7826d32cd34 172.38.0.15:6379@16379 slave 4c5124a0384f5dcc16fc2c46d4d898a00eac2af1 0 1648627924537 5 connected
c419506c3dfd89bb3c10c9a4e71798c6928841d9 172.38.0.14:6379@16379 slave cf80266d6a94f9f200ad0e75824a36e7d00ab1f0 0 1648627923033 4 connected
0a300109789e90a5713f9b453cf6827e573a4d38 172.38.0.16:6379@16379 slave 0a901487d8ff15de9e41706fbd5bb0bd99e9b052 0 1648627924537 6 connected
cf80266d6a94f9f200ad0e75824a36e7d00ab1f0 172.38.0.13:6379@16379 master - 0 1648627924000 3 connected 10923-16383
0a901487d8ff15de9e41706fbd5bb0bd99e9b052 172.38.0.12:6379@16379 master - 0 1648627924035 2 connected 5461-10922
4c5124a0384f5dcc16fc2c46d4d898a00eac2af1 172.38.0.11:6379@16379 myself,master - 0 1648627923000 1 connected 0-5460

127.0.0.1:6379> set a b
-> Redirected to slot [15495] located at 172.38.0.13:6379
OK
172.38.0.13:6379> get a
"b"
172.38.0.13:6379> get a
^C
/data # redis-cli -c
127.0.0.1:6379> get a
-> Redirected to slot [15495] located at 172.38.0.14:6379
"b"
172.38.0.14:6379> cluster nodes
c419506c3dfd89bb3c10c9a4e71798c6928841d9 172.38.0.14:6379@16379 myself,master - 0 1648628529000 7 connected 10923-16383
4c5124a0384f5dcc16fc2c46d4d898a00eac2af1 172.38.0.11:6379@16379 master - 0 1648628531534 1 connected 0-5460
0a300109789e90a5713f9b453cf6827e573a4d38 172.38.0.16:6379@16379 slave 0a901487d8ff15de9e41706fbd5bb0bd99e9b052 0 1648628530031 6 connected
0a901487d8ff15de9e41706fbd5bb0bd99e9b052 172.38.0.12:6379@16379 master - 0 1648628530532 2 connected 5461-10922
b4607c2b19afd41f034ab49dac4dd7826d32cd34 172.38.0.15:6379@16379 slave 4c5124a0384f5dcc16fc2c46d4d898a00eac2af1 0 1648628530532 5 connected
cf80266d6a94f9f200ad0e75824a36e7d00ab1f0 172.38.0.13:6379@16379 master,fail - 1648628462156 1648628461847 3 connected

SpringBoot 微服务打包Docker镜像

1️⃣ 在IDEA中构建一个SpringBoot的项目

# 创建了一个Control用来处理请求
@GetMapping("/hello")
public String hello(){
	return "hello Docker";
}

2️⃣ 借助右侧 maven 工具打包运行

3️⃣ 编写我们的 Dockerfile

FROM java:8
# 将当前目录下所有的 .jar 文件复制到容器的根目录下,并重命名为 app.jar
COPY *.jar /app.jar
CMD ["--server.port=8080"]
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.jar"]

4️⃣ 将项目打包后的 jar 包 和 Dockerfile 文件上传到服务器中

5️⃣ 构建镜像

docber build -t zhaowenhan666 .

6️⃣ 根据这个镜像创建并运行一个容器

# 创建并运行容器
docker run -d -P --name springboot zhaowenhan666
# 测试
curl localhost:49156/hello