# 七、Docker🎄
查看 docker 安装:Docker 安装.
- 初始 Docker
- Docker 的基本操作
- Dockerfile 自定义镜像
- Docker-Compose
- Docker 镜像服务
# 7.1、初始 Docker🌳
- 什么是 Docker
- Docker 和虚拟机的区别
- Docker 架构
- 安装 Docker
项目部署的问题
- 依赖关系复杂,容易出现兼容性问题
- 开发,测试,生产环境有差异
大型项目组件较多,运行环境也较为复杂,部署时会碰到一些问题:
所有的应该都会部署到服务器上,而大多数服务都会采用 Linux 操作系统,在安装到 Linux 操作系统之前要做一些准备工作。因为这些应用都会有自己所需要的依赖和函数库。这时如此复杂的依赖关系很容易出现,兼容性问题,废了半天劲把这些问题都解决了你会发现这只是开始搞定了开发环境,还有测试环境,生产环境,预发环境等等。最可怕的是这些环境它们的 Linux 操作系统有可能不同
# 7.1.1、Docker 如何解决依赖的兼容问题?🌲
- 将应用的 Libs (函数库),Deps (依赖),配置与应用一起打包
- 将每个应该放到一个隔离容器去运行,避免互相干扰
现在不用考虑依赖的问题了,但是这只是解决了混乱依赖的问题。有了 Docker 不用去管了应用程序随时可以运行但是仅限于同一个操作系统。因为在打包这个应用时肯定会基于某种操作系统打包吧,比如这个应该是 ubuntu 版本的那么它的函数库和依赖肯定也是 ubuntu 版本的。把这个打包好的程序放到 CentOs 上肯定不能运行,那么 Docker 是怎么解决这个问题的呢?
要想知道 Docker 如何跨系统运行,我们得先知道操作系统的结构
# 7.1.2、基本了解操作系统结构🌲
以 ubuntu 为例讲讲操作系统结构
所有的 Linux 操作系统都可以分为两层:
一层是大家共享的内核 (Linux 内核)
而区别在于不同操作系统之间它们的上层系统应用不同
那内核做什么的呢?
内核与硬件交互,提供操作硬件的指令
系统应用做什么的呢?
将内核的指令进行一个组装,再封装形成函数,区域多的函数变成函数库,程序员可以基于这些函数库进行开发。程序调用函数库,函数库调用内核指令,指令调用计算机硬件从而实现应用的执行
这就是 Linux 系统的基本结构以及应用的执行原理了
那么问题来了:为什么 Ubuntu 系统上的应用为什么不能在 CentOs 上运行呢?
Ubuntu 和 CentOS 都是基于 Linux 内核,只是系统应用不同,提供的函数库有差异
这时把 Ubuntu 上的应用比如说 mysql 给它迁移到 CentOS 上尝试去执行当它去调用一个函数库时,因为代码是写死的而发现这个函数库在 centos 上根本不存在或者名字不一样那么程序就会报错。这就是为什么 应用不能跨系统运行的原因
那针对上面的问题 Docker 干了什么事儿呢?
- Docker 将用户程序与所需要调用的系统 (比如 Ubuntu) 函数库一起打包
- Docker 运行到不同操作系统时,直接基于打包的库函数,借助于操作系统的 Linux 内核来运行。
将当前系统的函数库与应用一起打包那么随便放到任何的 Linux 操作系统上只要是 Linux 内核,应用在执行的时候调用打包好的函数库,而这个函数库直接调用操作系统的内核而内核直接访问硬件这个调用就完成了。
至此,Docker 就解决了跨系统的问题了,可以理解为 Docker 打包好的应用可以运行在任何的 Linux 内核的操作系统上
Docker:
Docker 如何解决大型项目以来关系复杂,不同组件依赖的兼容性问题?
- Docker 允许开发中将应用,依赖,函数库,配置一起 <font color='red'> 打包 </font>,形成可移植镜像
- Docker 应用运行在容器中,使用沙箱机制,相互 <font color='red'> 隔离 </font>.
Docker 如何解决开发,测试,生产环境有差异的问题
- Docker 镜像中包含完整运行环境,包括系统函数库,仅依赖系统的 Linux 内核,因此可以在任意 Linux 操作系统上运行
总结:
Docker 是一个快速交付应用,运行应用的技术:
- 可以将程序及其依赖,运行环境一起打包为一个镜像,可以迁移到任意 Linux 操作系统
- 运行时利用沙箱机制形成隔离容器,各个应用互不干扰
- 启动,移除都可以通过一行命令完成,方便快捷
# 7.2、Docker 与虚拟机🌳
我们知道 Docker 可以实现一个应用可以在不同的 Linux 操作系统上运行和部署。而虚拟机也可以达到类似的效果,那它们两个的实现又有什么样的差别呢
首先回顾下 Docker 的实现原理,Docker 会把应用及其所需要的依赖,函数库,甚至于操作系统函数库也一起打包,这样 当应用运行时可以直接调用本地函数库而后与操作系统的内核进行交互。这样就不用关心操作系统是什么系统了,于是就能实现跨系统运行了
那虚拟机呢?
它怎么实现在一个系统里装了另一个系统呢。它是用了一个名为:Hyperisor 的技术,可以模拟出一个计算机的各种各样的硬件比如 CPU,内存等等,然后在这个模拟出的计算机上就可以去安装任意你想要的操作系统了,而操作系统又可以安装任意适合的依赖,函数库以及应用,那这样它不就实现跨系统的部署了吗
虚拟机是在一个系统里装了另外一个系统,所以当应用执行时,它会以为自己在一台真实的电脑上。因此它会先调用虚拟机内置的操作系统而它在与 Hypervisor 进行交互,然后再把信息传递给外部的操作系统,外部操作系统再去调用计算机硬件,于是应用执行就完成了。
可以看到虚拟机启动应用经过了层层传递所以性能是比较差的
<center> 两者之间的差异 </center>
特性 | Docker | 虚拟机 |
---|---|---|
性能 | 接近原生 | 性能较差 |
硬盘占用 | 一般为 MB | 一般为 GB |
启动 | 秒级 | 分钟级 |
总结:
Docker 和虚拟机的差异:
- Docker 是一个系统进程;虚拟机是在操作系统中的操作系统
- Docker 体积小,启动速度快,性能好;虚拟机体积大,启动速度慢,性能一般
# 7.2、镜像和容器🌳
镜像 (Image):Docker 将应用程序及其所需的依赖,函数库,环境,配置等文件打包在一起,称为镜像。
简单可以理解为,镜像就是一个文件。
容器 (Container):镜像中的应用程序运行后形成的进程就是 <font color='red'> 容器 </font>,只是 Docker 会给容器做隔离,对外不可见。
简单可以理解为,把这个 mysql 应用运行起来形成的这个进程就是容器,只不过在 Docker 里面容器还要做隔离
也就是说我们可以将容器看做是一个小盒子,这个盒子里面将来会利用 Linux 手段给它形成隔离空间。里面会有自己独立的 CPU 资源,内存资源,甚至于还有独立的文件系统。那么因此在这个容器内运行的这个进程,它就会以为自己是在这台计算机上的唯一进程了从而起到一种隔离的效果,将来 mysql 镜像不管是启动一个容器还是多个容器,它们之间都是相互隔离的,当容器运行的过程中必然会做数据读写的操作。比方说 mysql 要去存数据,存储到 data 目录中。但是不能将数据写到镜像的 data 目录中去。镜像都是只读的
# 7.3、共享镜像🌳
# 7.3.1、Docker 和 DockerHub🌲
- DockerHub:是一个 Docker 镜像的托管平台。这样的平台称为 Docker Registry。
程序员可以利用 Docker 提供的一些命令去完成镜像的构建,构建出 mysql,redis 等等各种各样的镜像
然后把这些镜像上传到 DockerHub 服务器上
- 国内也有类似于 DockerHub 的公开服务,比如 网易云镜像服务,阿里云镜像库等。
- 可以搭建私有的云服务
这些都叫 Docker Registry
我们要怎么利用 Docker 完成镜像构建或者是从远端拉取镜像呢又该怎么去运行容器呢?
这就需要了解 Docker 的架构了
# 7.4、Docker 架构🌳
Docker 是一个 CS 架构的程序,由两部分组成:
服务端 (server):Docker 守护进程,负责处理 Docker 指令,管理镜像,容器等
负责接收用户指令完成镜像和容器的各种各样的操作,不管是构建镜像还是从远端拉取镜像还是运行容器都是由 server 端来完成的 ,那谁给 server 端下指令呢?自然就是 client 端了
客户端 (client):通过命令或 RestAPI 向 Docker 服务端发送指令。可以在本地或远程向服务端发送指令。
这个要看在哪发送指令,如果是在 Docker 所在的机器上就直接用命令就可以了。如果是远程操作 Docker 就用 RestAPI 发
比方说就是本地发通过命令:dokcer build 可以构建镜像,这个命令到达 docker server 后会被 docker 的守护进程 docker daemon 接收和处理。它会利用你提供的数据构建成一个镜像。除了这种方式获取镜像以外我们还可以去 docker registry 中拉取镜像,使用命令:docker pull 这个命令发送到 docker server 端后 docker daemon 就会去 docker registry 中拉取我们指定的镜像了。下面就是运行镜像创建容器了,使用命令:docker run 这个命令发送到 docker server 端后 docker daemon 就会去帮助我们完成容器的创建,然后部署就完成了。
总结:
镜像:
- 将应用程序及其依赖,环境,配置打包在一起
容器:
- 镜像运行起来就是容器,一个镜像可以运行多个容器
Docker 结构:
- 服务端:接收命令或远程请求,操作镜像或容器
- 客户端:发送命令或者请求到 Docker 服务端
DockerHub:
- 一个镜像托管的服务器,类似的还有阿里云镜像服务,统称为 DockerRegistry
# 7.5、Docker 基本操作🌳
- 镜像操作
- 容器操作
- 数据卷 (容器数据管理)
# 7.5.1、镜像相关命令🌲
镜像名称一般分两部分组成:[repository]:[tag]
tag 一般指 版本
在没有指定 tag 时,默认是 latest,代表最新版本的镜像
# 7.5.1.1、镜像操作命令🌴
Docker 镜像的操作无非就是 CRUD,比如 local (本地) 就是本地的 Docker,我要在这个上面完成镜像的操作。比方说获取镜像,一般有两种获取方法。第一种为:本地获取,第二种为:远端获取。
本地获取:
需要一个名为 Dockerfile 文件,利用 docker build 命令把它构建成一个镜像,使用命令:docker pull 从 docker registry 的服务里拉取镜像,获取到镜像想查看下可以使用命令:docker images,删除镜像使用命令:docker rmi
分享镜像有两种方式:
方式一:
使用命令 docker push 将镜像推送到镜像服务器
方式二:
使用 docker save 将镜像打包为 zip 然后使用 u 盘拷贝给别人。使用 docker load 将 zip 加载为镜像
# 7.5.2、案例,从 DockerHub 中拉取一个 nginx 镜像并查看🌲
需求:从 DockerHub 中拉取一个 nginx 镜像并查看
1、首先去镜像仓库搜索 nginx 镜像,比如 DockerHub:
nginx 后面的勋章表示这个是官网的正版镜像
点开后可复制命令,默认拉取的是最新版本的 nginx
2、根据查看到的镜像名称,拉取自己需要的镜像,通过命令:docker pull nginx
3、通过命令:docker images 查看拉取到的镜像
# 7.5.3、案例,利用 docker save 将 nginx 镜像导出磁盘,然后再通过 load 加载回来🌲
步骤:
1、利用 docker xx --help
命令查看 docker save 和 docker load 的语法
[root@localhost ~]# docker save --help | |
# 命令示例:docker save [选项] 镜像名称 | |
Usage: docker save [OPTIONS] IMAGE [IMAGE...] | |
# 保存一个或多个镜像到一个 tar 文件中 (tar 压缩文件) | |
Save one or more images to a tar archive (streamed to STDOUT by default) | |
Aliases: | |
docker image save, docker save | |
# 选项说明 | |
Options: | |
# -o 输出的意思,输出到一个指定的文件当中 | |
-o, --output string Write to a file, instead of STDOUT | |
[root@localhost ~]# |
docker save 导出镜像为压缩文件
[root@localhost ~]# docker images | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
nginx latest 61395b4c586d 2 weeks ago 187MB | |
[root@localhost ~]# docker save -o nginx.tar nginx:latest | |
[root@localhost ~]# ls -ahl | |
total 183M | |
dr-xr-x---. 4 root root 191 Oct 8 04:47 . | |
dr-xr-xr-x. 17 root root 224 Jul 15 05:11 .. | |
-rw-------. 1 root root 1.4K Jul 15 05:12 anaconda-ks.cfg | |
-rw-------. 1 root root 78 Jul 15 05:20 .bash_history | |
-rw-r--r--. 1 root root 18 Dec 28 2013 .bash_logout | |
-rw-r--r--. 1 root root 176 Dec 28 2013 .bash_profile | |
-rw-r--r--. 1 root root 176 Dec 28 2013 .bashrc | |
-rw-r--r--. 1 root root 100 Dec 28 2013 .cshrc | |
drwxr-xr-x. 3 root root 20 Oct 8 03:52 etc | |
-rw-------. 1 root root 183M Oct 8 04:47 nginx.tar | |
drwxr-----. 3 root root 19 Jul 15 05:30 .pki | |
-rw-r--r--. 1 root root 129 Dec 28 2013 .tcshrc | |
-rw-------. 1 root root 841 Oct 8 03:56 .viminfo | |
[root@localhost ~]# |
docker load 导入 压缩文件
先将 docker 中的镜像删除
[root@localhost ~]# docker rmi nginx:latest | |
Untagged: nginx:latest | |
Untagged: nginx@sha256:32da30332506740a2f7c34d5dc70467b7f14ec67d912703568daff790ab3f755 | |
Deleted: sha256:61395b4c586da2b9b3b7ca903ea6a448e6783dfdd7f768ff2c1a0f3360aaba99 | |
Deleted: sha256:1c69f36a0d9b59b762eaba410fa9fd01b85140670a8d49199a7b37702cc956c0 | |
Deleted: sha256:bac209bfab6997cccf20779ae98d5f77a66867734499ecf604a50a5826f6b8a4 | |
Deleted: sha256:859676f4cd3004af025a02dade096ad6f9391d94d1b1a983fc6098debe435055 | |
Deleted: sha256:cbbd97cee0d824e5e82f9a4b2e93c5eb3c66fd72a2624d5c1521dc3395bfd1e2 | |
Deleted: sha256:0b41545d8c3de3b78778d591a8da3d9dfa5fa8807baef5edf21e5eb94ded792d | |
Deleted: sha256:7e87866b23143eb30086086a669b2e902368a5836446a885b2411d3feef18bef | |
Deleted: sha256:d310e774110ab038b30c6a5f7b7f7dd527dbe527854496bd30194b9ee6ea496e | |
[root@localhost ~]# | |
[root@localhost ~]# docker images | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
[root@localhost ~]# |
使用 docker load --help
来查看命令的说明
[root@localhost ~]# docker load --help | |
# 命令示例:docker load [选项] | |
Usage: docker load [OPTIONS] | |
# 从 tar 存档或 STDIN 加载图像 | |
Load an image from a tar archive or STDIN | |
Aliases: | |
docker image load, docker load | |
# -i :输入,指定读取哪一个文件 | |
# -q :不会打印日志 | |
Options: | |
-i, --input string Read from tar archive file, instead of STDIN | |
-q, --quiet Suppress the load output | |
[root@localhost ~]# |
使用 docker load 命令加载镜像
[root@localhost ~]# docker load -i nginx.tar | |
d310e774110a: Loading layer [==================================================>] 77.81MB/77.81MB | |
eb7e3384f0ab: Loading layer [==================================================>] 113MB/113MB | |
1dc45c680d0f: Loading layer [==================================================>] 3.584kB/3.584kB | |
ea43d4f82a03: Loading layer [==================================================>] 4.608kB/4.608kB | |
9c6261b5d198: Loading layer [==================================================>] 2.56kB/2.56kB | |
a7e2a768c198: Loading layer [==================================================>] 5.12kB/5.12kB | |
d26d4f0eb474: Loading layer [==================================================>] 7.168kB/7.168kB | |
Loaded image: nginx:latest | |
[root@localhost ~]# | |
[root@localhost ~]# docker images | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
nginx latest 61395b4c586d 2 weeks ago 187MB | |
[root@localhost ~]# |
总结:
镜像操作有哪些?
- docker images
- docker rmi
- docker pull
- docker push
- docker save
- docker load
# 7.5.4、(自己练习) 去 DockerHub 搜索并拉取一个 Redis 镜像🌲
1、去 DockerHub 搜索 Redis 镜像
2、查看 Redis 镜像的名称和版本
3、利用 docker pull 命令拉取镜像,并使用 docker images 查看拉取的情况
[root@localhost ~]# docker pull redis | |
Using default tag: latest | |
latest: Pulling from library/redis | |
a803e7c4b030: Already exists | |
57e05a1ddec3: Pull complete | |
4635c857038a: Pull complete | |
10001c6989b4: Pull complete | |
f93feb85ceb4: Pull complete | |
4f4fb700ef54: Pull complete | |
320349bd761d: Pull complete | |
Digest: sha256:b68c6efe2c5f2d7d7d14a2749f66d6d81645ec0cacb92572b2fb7d5c42c82031 | |
Status: Downloaded newer image for redis:latest | |
docker.io/library/redis:latest | |
[root@localhost ~]# | |
[root@localhost ~]# docker images | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
nginx latest 61395b4c586d 2 weeks ago 187MB | |
redis latest 7c4b517da47d 4 weeks ago 153MB | |
[root@localhost ~]# |
4、利用 docker save 命令将 redis:latest 打包为一个 redis.tar 包
[root@localhost ~]# docker save -o redis.tar redis:latest | |
[root@localhost ~]# | |
[root@localhost ~]# ls | |
anaconda-ks.cfg etc redis.tar | |
[root@localhost ~]# |
5、利用 docker rmi 删除本地的 redis:latest
[root@localhost ~]# docker rmi redis:latest | |
Untagged: redis:latest | |
Untagged: redis@sha256:b68c6efe2c5f2d7d7d14a2749f66d6d81645ec0cacb92572b2fb7d5c42c82031 | |
Deleted: sha256:7c4b517da47d331a47827390b9e8eb1be7ee68133af9c332660001b4d447828d | |
Deleted: sha256:06dafc2b2808183db1be226fd99e9dd9d374db9ef86480ccdec56bcb644c821a | |
Deleted: sha256:d31af4bfba5cce8499ed4df2f666ef1cf911d0936156af46da79d4b510c7687a | |
Deleted: sha256:f14f2bfc00d1db007798b7b81d043df4130673ae33591960d0917c484eded7f0 | |
Deleted: sha256:7778474585964f4869bd7b40e56a375312fc91284c26f7b84967efd383219a20 | |
Deleted: sha256:8fbe6eb1ebe310862ce5ebf21c01744994203df7d3f53fecabf53db86c3f4578 | |
Deleted: sha256:57489dc8f39be6c5bfe929829682b1dcd6c7497e2f5878f450557a381afacb52 | |
[root@localhost ~]# | |
[root@localhost ~]# docker images | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
nginx latest 61395b4c586d 2 weeks ago 187MB | |
[root@localhost ~]# |
6、利用 docker load 重新加载 redis.tar 文件
[root@localhost ~]# docker load -i redis.tar | |
ae2dc73bba26: Loading layer [==================================================>] 10.75kB/10.75kB | |
7c13dd56eefc: Loading layer [==================================================>] 4.141MB/4.141MB | |
33409257a91d: Loading layer [==================================================>] 75.28MB/75.28MB | |
c0a7ac48293c: Loading layer [==================================================>] 1.536kB/1.536kB | |
5f70bf18a086: Loading layer [==================================================>] 1.024kB/1.024kB | |
e6642d3d7b17: Loading layer [==================================================>] 4.096kB/4.096kB | |
Loaded image: redis:latest | |
[root@localhost ~]# | |
[root@localhost ~]# docker images | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
nginx latest 61395b4c586d 2 weeks ago 187MB | |
redis latest 7c4b517da47d 4 weeks ago 153MB | |
[root@localhost ~]# |
# 7.5.5、容器相关命令🌲
创建比较常见的一个命令就是 docker run 了,这个命令不仅仅可以帮我们创建一个容器而且还可以让这个容器处于运行状态。容器除了运行状态还有,暂停,停止两个状态。只需要简单的命令就可以实现两个状态之间的切换了
docker puse 可以让容器从运行状态进入到暂停状态
docker unpuse 让容器从暂停状态恢复到运行状态
docker stop 可以让容器从运行状态进入停止状态
docker start 让容器从停止状态恢复到运行状态
docker stop $(docker ps -q) 停止全部运行中的容器
docker rm $(docker ps -aq) 删除全部容器 运行中的需要强制删除 -f
docker stop $(docker ps -q) & docker rm $(docker ps -aq)
先停止全部容器再删除全部容器
那为什么暂停可以用 puse 和 unpause 而停止却是 stop 和 start 呢?为什么不是 unstop。
原因:
它们在操作系统的处理方式不同,如果容器进入暂停状态,操作系统会将容器内的进程挂起,容器关联的内存暂存起来。CPU 不再执行这个进程,如果将它恢复那么内存空间恢复程序接着运行,那么容器就又进入运行状态了。
但是停止不同,停止直接把这个进程杀死,容器所占的内存回收。把保留下来的就仅剩容器的文件系统了也就是那些静态的东西,因此停止是没有办法进行恢复的因为进程已经被杀死了。
这就是暂停和停止的区别了。
# 7.5.5.1、查看容器的状态🌴
Docker 提供了一个命令叫:docker ps 可以查看所有运行的容器以及状态,还有一个命令为:docker logs 查看容器运行的日志
如果想深入内部了解一下,Docker 还提供了一个命令:docker exec 可以进入容器的内部进行操作
docker rm 删除容器
# 7.5.5.2、创建运行一个 Nginx 容器🌴
1、去 docker hub 查看 Nginx 的容器运行命令
docker run --name some-nginx -d -p 8080:80 some-content-nginx |
命令解读:
- docker run:创建并运行一个容器
--name
:给容器起一个名字,比如叫做 mn- -p:将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右则是容器端口
- -d:后台运行容器
- nginx:镜像名称,例如 nginx
将宿主机端口与容器端口映射是什么意思呢?
比方说有一台 centos 的服务器 ip 地址为:192.168.150.101 现在我们在这台服务器上部署了一台 Nginx 容器它的端口是 80,如果用户想要来访问 Nginx 容器不能直接访问,因为容器是对外隔离的。不能访问怎么办呢,所以我们要做一个端口映射
端口映射的解释:
比如说宿主机有自己很多端口,其中一个端口为:80,让宿主机的 80 跟容器的 80 产生一个关联映射,那这样一来任何进入宿主机 80 的这个请求就都会被转发到容器的 80 端口去执行了。这样就等于是 nginx 处理了这个请求
所以端口映射的作用是把一个本来完全隔离的容器对外暴漏让用户可以访问
2、执行命令创建容器并运行
[root@localhost ~]# docker run --name mn -p 80:80 -d nginx | |
9672ddfc088cdf53966332bf06da44b6daf7093427c12ed636818bcaae20e312 | |
[root@localhost ~]# |
9672ddfc088cdf53966332bf06da44b6daf7093427c12ed636818bcaae20e312:容器的唯一 id
3、查看容器的状态
[root@localhost ~]# docker ps | |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | |
9672ddfc088c nginx "/docker-entrypoint.…" 14 minutes ago Up 14 minutes 0.0.0.0:80->80/tcp, :::80->80/tcp mn | |
[root@localhost ~]# |
4、访问服务
可以访问说明容器部署成功。
5、查看访问 nginx 后的日志信息
[root@localhost ~]# docker logs mn | |
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration | |
/docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/ | |
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh | |
10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf | |
10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf | |
/docker-entrypoint.sh: Sourcing /docker-entrypoint.d/15-local-resolvers.envsh | |
/docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh | |
/docker-entrypoint.sh: Launching /docker-entrypoint.d/30-tune-worker-processes.sh | |
/docker-entrypoint.sh: Configuration complete; ready for start up | |
2023/10/08 10:04:30 [notice] 1#1: using the "epoll" event method | |
2023/10/08 10:04:30 [notice] 1#1: nginx/1.25.2 | |
2023/10/08 10:04:30 [notice] 1#1: built by gcc 12.2.0 (Debian 12.2.0-14) | |
2023/10/08 10:04:30 [notice] 1#1: OS: Linux 3.10.0-1160.el7.x86_64 | |
2023/10/08 10:04:30 [notice] 1#1: getrlimit(RLIMIT_NOFILE): 1048576:1048576 | |
2023/10/08 10:04:30 [notice] 1#1: start worker processes | |
2023/10/08 10:04:30 [notice] 1#1: start worker process 29 | |
2023/10/08 10:04:30 [notice] 1#1: start worker process 30 | |
2023/10/08 10:04:30 [notice] 1#1: start worker process 31 | |
2023/10/08 10:04:30 [notice] 1#1: start worker process 32 | |
192.168.249.1 - - [08/Oct/2023:10:21:11 +0000] "GET / HTTP/1.1" 200 615 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36" "-" | |
[root@localhost ~]# |
# 7.5.5.2.1、持续输出日志🎋
每次请求都要执行指令才能查看日志很麻烦通过 docker logs --help
来查看有没有需要的帮助
[root@localhost ~]# docker logs --help | |
Usage: docker logs [OPTIONS] CONTAINER | |
Fetch the logs of a container | |
Aliases: | |
docker container logs, docker logs | |
Options: | |
--details Show extra details provided to logs | |
# 持续输出日志 | |
-f, --follow Follow log output | |
--since string Show logs since timestamp (e.g. "2013-01-02T13:23:37Z") or relative (e.g. "42m" for 42 minutes) | |
-n, --tail string Number of lines to show from the end of the logs (default "all") | |
-t, --timestamps Show timestamps | |
--until string Show logs before a timestamp (e.g. "2013-01-02T13:23:37Z") or relative (e.g. "42m" for 42 minutes) | |
[root@localhost ~]# |
我们可以看到选项中有一个持续输出日志的选项
总结:
docker run 命令的常见参数有哪些?
--name
:指定容器名称-p
:指定端口映射-d
:让容器后台运行查看容器日志的命令:
- docker logs
- 添加 - f 参数可以持续查看日志
查看容器状态:
- docker ps
# 7.5.5.3、进入 Nginx 容器,修改 HTML 文件内容,添加 “传智教育欢迎您”🌴
1、进入容器。进入我们刚刚创建的 nginx 容器的命令为:
docker exec -it mn bash |
命令解读:
- docker exec:进入容器内部,执行一个命令
- -it:给当前进入的容器创建一个标准输入,输出终端,允许我们与容器交互
- mn:要进入的容器的名称
- bash:进入容器后执行的命令,bash 是一个 linux 终端交互命令
[root@localhost ~]# docker exec -it mn bash | |
root@9672ddfc088c:/# |
通过 ls 命令查看内部都有什么:
root@9672ddfc088c:/# ls | |
bin dev docker-entrypoint.sh home lib32 libx32 mnt proc run srv tmp var | |
boot docker-entrypoint.d etc lib lib64 media opt root sbin sys usr | |
root@9672ddfc088c:/# |
这些我们常见的是 linux 根目录结构,但是这是阉割版的。容器的内部会有自己的一套文件系统,看起来和 linux 的系统目录一样,但是这里只有 nginx 运行它自己需要的文件
要想看 nginx 存放到哪里了我们需要去 Docker Hub 的 nginx 发布页面查看:
2、进入 nginx 的 HTML 所在目录 /user/share/nginx/html
cd /user/share/nginx/html |
3、修改 index.html 的内容
sed -i -e 's#Welcome to nginx#传智教育欢迎您#g' -e 's#<head>#<head><meta charset="utf-8">#g' index.html |
再去访问 nginx 首页
# 7.5.6、查看停止的容器🌲
先查看 docker 容器的运行状态
[root@localhost ~]# docker ps | |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | |
9672ddfc088c nginx "/docker-entrypoint.…" About an hour ago Up About an hour 0.0.0.0:80->80/tcp, :::80->80/tcp mn | |
[root@localhost ~]# |
停止该容器
[root@localhost ~]# docker stop mn | |
mn | |
[root@localhost ~]# docker ps | |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | |
[root@localhost ~]# |
可以看到停止后通过 docker ps 查看状态就看不到 nginx 容器了,我们可以使用 docker ps -a 来进行查看被杀死的容器
[root@localhost ~]# docker ps -a | |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | |
9672ddfc088c nginx "/docker-entrypoint.…" About an hour ago Exited (0) About a minute ago mn | |
[root@localhost ~]# |
总结:
查看容器状态
- docker ps
- 添加 - a 参数查看所有状态的容器
删除容器
- docker rm
- 不能删除运行中的容器,除非添加 - f 参数
进入容器:
- 命令是
docker exec -it [容器名][要执行的命令]
- exec 命令可以进入容器修改文件,但是在容器内修改文件时不推荐的
# 7.5.7、数据卷🌲
容器与数据耦合的问题
上面的问题怎么解决呢,这就要学习 数据卷 来解决了
数据卷 (volume) 是一个虚拟目录,指向宿主机文件系统中的某个目录。
比如说有一个 Docker 主机,那么在这个主机上就会由 Docker 去管理很多很多的数据卷。而所有的数据卷一定都会指向宿主机文件系统上的一个目录这个目录就是:var/lib/docker/volumes。
比方说我们现在利用 Docker 创建了一个新的数据卷,这个数据卷的名字叫 html,那么 Docker 一定会在 /var/lib/docker/volumes 指定的宿主机文件系统下创建出一个 html 的目录
比方说我又创建了一个新的数据卷 conf,它同样会在 docker 指定的 volumes 目录下再创建一个 conf 真是目录
每一个数据卷都跟一个真是目录进行映射。所以可以认为数据卷是一个虚拟的并不真实存在的。只是一个概念,而真正指向的是 (宿主机) 硬盘上的真实文件夹
容器创建后就可以使用这个数据卷,nginx 的内部目录与数据卷进行关联,当容器中 nginx 与数据卷进行关联时,它的本质其实是和宿主机文件系统上的 html 和 conf 目录进行关联的。这时 docker 就会去管理这个容器了,比方说在容器的 html 目录里写了点什么东西,这些东西会立即写入到宿主机文件系统里。而反过来如果在宿主机里对 html 目录里某一个文件进行了修改那么也会立即反应到容器内的 html 目录中
可以理解为 容器 与 宿主机 之间 的数据同步是通过数据卷这个中间桥梁完成的
这时就再也不用进入容器内去做修改了直接在宿主机通过高级编辑工具进行修改
# 7.5.7.1、数据共享🌴
将另一个容器挂载到数据卷的目录上也能访问操作里面的数据和配置的信息
# 7.5.7.2、数据安全删除问题🌴
将容器删除,数据卷不会被删除,还可以将新的容器重新挂载到数据卷上里面的配置和数据都回来了
# 7.5.7.3、操作数据卷🌴
数据卷操作的基本语法如下:
docker volume[COMMAND] |
docker volume 命令是数据卷操作,根据命令后跟随的 command 来确定下一步的操作:
- create 创建一个 volume
- inspect 显示一个或多个 volume 的信息
- ls 列出所有的 volume
- prune 删除未使用的 volume
- rm 删除一个或多个指定的 volume
# 7.5.7.3.1、案例,创建一个数据卷,并查看数据卷在宿主机的目录位置🎋
1、创建一个名为 html 的数据卷
[root@localhost ~]# docker volume create html | |
html | |
[root@localhost ~]# |
2、查看创建好的数据卷 使用命令列出所有的数据卷
[root@localhost ~]# docker volume ls | |
DRIVER VOLUME NAME | |
local 2c26106500b13248f06d9d3d89e3f9cc4395f02bc013e4ad20ef405b504b1ca0 | |
local 96f2fa7ba9c8131ae29e9e046d86d33c48ba9a14ad145b42837e281c0f8a0c00 | |
local 356ef50e5c9ff24e07f8bcc1f9d581f41df05ce8688025f2294d2333fd86842b | |
local d3c6bed2dc6b02bd7755c7e20a02a6679d5bc1bdc08e302adb0615f6d68b3b3a | |
local html | |
[root@localhost ~]# |
3、查看 html 数据卷的所在目录
[root@localhost ~]# docker inspect html | |
[ | |
{ | |
"CreatedAt": "2023-10-08T20:59:39-04:00", | |
"Driver": "local", | |
"Labels": null, | |
# 存储目录,也就是挂载点 | |
"Mountpoint": "/var/lib/docker/volumes/html/_data", | |
"Name": "html", | |
"Options": null, | |
"Scope": "local" | |
} | |
] | |
[root@localhost ~]# |
4、使用 prune 移除本地未使用的数据卷
[root@localhost ~]# docker volume prune | |
WARNING! This will remove anonymous local volumes not used by at least one container. | |
Are you sure you want to continue? [y/N] y | |
Deleted Volumes: | |
2c26106500b13248f06d9d3d89e3f9cc4395f02bc013e4ad20ef405b504b1ca0 | |
96f2fa7ba9c8131ae29e9e046d86d33c48ba9a14ad145b42837e281c0f8a0c00 | |
d3c6bed2dc6b02bd7755c7e20a02a6679d5bc1bdc08e302adb0615f6d68b3b3a | |
Total reclaimed space: 0B | |
[root@localhost ~]# |
5、使用 docker volume ls 列出所有的数据卷
[root@localhost ~]# docker volume ls | |
DRIVER VOLUME NAME | |
local 356ef50e5c9ff24e07f8bcc1f9d581f41df05ce8688025f2294d2333fd86842b | |
local html | |
[root@localhost ~]# |
少了一些数据卷那些被移除了
6、使用 rm 删除一个指定的数据卷,如果无法删除可以加上参数 -f 强制删除
[root@localhost ~]# docker volume rm html | |
html | |
[root@localhost ~]# docker volume ls | |
DRIVER VOLUME NAME | |
local 356ef50e5c9ff24e07f8bcc1f9d581f41df05ce8688025f2294d2333fd86842b | |
[root@localhost ~]# |
数据卷的作用:
- j 将容器与数据分离,解耦合,方便操作容器内数据,保证数据安全
数据卷操作:
docker volume create [名称]
创建一个数据卷docker volume ls
列出所有存在的数据卷docker volume inspect [名称]
查看指定数据卷的详细信息或所在目录docker volume rm [名称]
删除指定的数据卷,可以删除一个或多个,可以强制删除docker volume prune
移除未使用的数据卷
# 7.5.7.4、挂载数据卷🌴
我们在创建容器时,可以通过 -v
参数来挂载一个数据卷到某个容器目录
这里面唯一没见过的就是 -v
参数了,解释如下:
前半部分的 html 为数据卷:后半部分为容器内的目录。
这行命令的意思就是将名为 html 的数据卷挂载到容器内的 /root/html 目录下
# 7.5.7.4.1、案例,创建一个 Nginx 容器,修改容器内的 html 目录内的 index.html 内容🎋
需求说明:
上个案例中,我们进入 nginx 容器内部,已经知道 nginx 的 html 目录所在位置 /user/share/nginx/html ,我们需要把这个目录挂载到 html 这个数据卷上,方便操作其中的内容。
提示:运行容器时使用 -v
参数挂载数据卷
步骤:
<font color='red'> 提醒 </font>:如果数据卷不存在在创建容器的时候里面 -v
参数执行数据卷就会自动被创建
1、创建容器并挂载数据卷到容器内的 HTML 目录
[root@localhost ~]# docker run --name mn -p 80:80 -v html:/usr/share/nginx/html -d nginx | |
a4e6d36a4c1ccb8948d93990839244207cb1db9856a607680350871c13fc7458 | |
[root@localhost ~]# docker ps | |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | |
a4e6d36a4c1c nginx "/docker-entrypoint.…" 2 seconds ago Up 1 second 0.0.0.0:80->80/tcp, :::80->80/tcp mn | |
[root@localhost ~]# |
2、进入 html 数据卷所在位置,并修改 HTML 内容
2.1、首先看一下 html 数据卷的具体位置
2.2、进入到目录
[root@localhost ~]# docker volume inspect html | |
[ | |
{ | |
"CreatedAt": "2023-10-08T21:26:51-04:00", | |
"Driver": "local", | |
"Labels": null, | |
"Mountpoint": "/var/lib/docker/volumes/html/_data", | |
"Name": "html", | |
"Options": null, | |
"Scope": "local" | |
} | |
] | |
[root@localhost ~]# cd /var/lib/docker/volumes/html/_data | |
[root@localhost _data]# | |
[root@localhost _data]# ls -ahl | |
total 8.0K | |
drwxr-xr-x. 2 root root 40 Oct 9 02:54 . | |
drwx-----x. 3 root root 19 Oct 8 21:26 .. | |
-rw-r--r--. 1 root root 497 Aug 15 13:03 50x.html | |
-rw-r--r--. 1 root root 615 Aug 15 13:03 index.html | |
[root@localhost _data]# |
我们可以右键 index.html 直接在 windows 上进行修改然后保存那么 nginx 容器内的 index.html 也就被修改了
改完保存后这个工具会提示是否保存到 Linux 系统中 index 文件去还是很智能的
下面就是修改了内容成功了,但是乱码
总结:
数据卷挂载方式:
-v volumeName: /targetContainerPath
- 如果容器运行时 volume 不存在,会自动被创建出来
通过案例演示宿主机直接与容器进行挂载
# 7.5.7.4.2、宿主机直接与容器进行挂载🎋
案例,创建并运行一个 MYSQL 容器,将宿主机目录直接挂载到容器
提示:目录挂载与数据卷挂载的语法是类似的:
-v [宿主机目录]:[容器目录]
-v [宿主机文件]:[容器内文件]
实现思路如下:
1、在将课前资料中的 mysql.tar 文件上传到虚拟机,通过 load 命令加载为镜像
操作:直接拖进去即可
[root@localhost tmp]# docker load -i mysql.tar | |
5dacd731af1b: Loading layer [==================================================>] 58.45MB/58.45MB | |
f411d8bde01c: Loading layer [==================================================>] 338.4kB/338.4kB | |
0aa7d65147ef: Loading layer [==================================================>] 10.44MB/10.44MB | |
3437f67a712b: Loading layer [==================================================>] 4.472MB/4.472MB | |
ec41e34b35a0: Loading layer [==================================================>] 1.536kB/1.536kB | |
458d25c646d8: Loading layer [==================================================>] 46.15MB/46.15MB | |
97874ea0e7f9: Loading layer [==================================================>] 31.74kB/31.74kB | |
5075b9328698: Loading layer [==================================================>] 3.584kB/3.584kB | |
364557e875f1: Loading layer [==================================================>] 257.3MB/257.3MB | |
9209148debed: Loading layer [==================================================>] 9.728kB/9.728kB | |
82582edf9553: Loading layer [==================================================>] 1.536kB/1.536kB | |
Loaded image: mysql:5.7.25 | |
[root@localhost tmp]# | |
[root@localhost tmp]# docker images | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
nginx latest 61395b4c586d 2 weeks ago 187MB | |
redis latest 7c4b517da47d 4 weeks ago 153MB | |
mysql 5.7.25 98455b9624a9 4 years ago 372MB | |
[root@localhost tmp]# |
2、创建目录 /tem/mysql/data
[root@localhost tmp]# mkdir -p mysql/data | |
[root@localhost tmp]# mkdir -p mysql/conf |
3、创建目录 /tmp/mysql/conf,将课前资料提供的 hmy.cnf 文件上传到 /tmp/mysql/conf
hmy.conf 配置文件内容:
[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
4、去 DockerHub 查阅资料,创建并运行 MySQL 容器
docker run \ | |
--name some-mysql \ | |
# 环境变量 environment variables 设置 mysql 密码 | |
-e MYSQL_ROOT_PASSWORD=123 \ | |
# 设置宿主机与容器的端口都为 3306 | |
-p 3306:3306 | |
# 挂载配置文件目录,前面是宿主机目录后面是容器内的配置文件目录可以去官网查看 | |
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \ | |
# 挂载数据目录 | |
-v /tmp/mysql/data:/var/lib/mysql \ | |
-d mysql:5.7.25 |
可复制如下:
docker run \ | |
--name some-mysql \ | |
-e MYSQL_ROOT_PASSWORD=123 \ | |
-p 3306:3306 \ | |
-v /tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf \ | |
-v /tmp/mysql/data:/var/lib/mysql \ | |
-d mysql:5.7.25 |
my.conf 就是 mysql 的默认配置文件了但是并不建议去覆盖它,它里面东西很多,我们只是希望要简化的配置 下面文档说,my.conf 配置文件里包含了两个目录一个是 /etc/mysql/conf.d 或 /etc/mysql/mysql/conf.d 其中.d 后缀就是目录的意思,因此放到这两个包含的目录里面的一切文件都会被加载到一起作为合并配置
要求:
- 挂载 /tmp/myql/data 到 mysql 容器内数据存储到目录
- 挂载 /tmp/myql/conf/hmy/cnf 到 mysql 容器的配置文件
- 设置 MySQL 密码
打开链接 mysql 的工具进行连接,连接的是 Linux 中的 mysql
连接密码是上面创建容器的时候的密码也就是 123
连接成功!
查看宿主机的 data 目录可以看到里面的 mysql 数据就全都写进来了
➜ /tmp cd mysql/data | |
➜ data ls | |
auto.cnf ca.pem client-key.pem ibdata1 ib_logfile1 mysql private_key.pem server-cert.pem sys | |
ca-key.pem client-cert.pem ib_buffer_pool ib_logfile0 ibtmp1 performance_schema public_key.pem server-key.pem | |
➜ data |
# 7.5.7.5、数据卷挂载和目录直接挂载的区别🌴
当我们用数据卷时,Docker 会自动帮我们创建数据卷对应的目录,这样数据卷就指向了宿主机的 html 目录。而 Docker 挂载时只需要挂载到数据卷的 html 目录上就可以了。它不需要关系宿主机的 html 目录在哪里,这种方式等于全权交给了 Docker 去处理
这是数据卷挂载的优势,但是劣势呢?
劣势就是,目录不是我们创建的,到哪创建了我们也不知道目录结构也比较深想去找比较麻烦
而第二种方式直接挂载,宿主机目录是我们自己创建的,创建目录的时候挺麻烦的但是自己知道这个目录在哪,将来挂载的时候也没有人帮我做什么代理。我直接挂上去就行了。因此我想快速定位这个文件在哪就可以一目了然,这就是直接挂载的优势,劣势就是自己挂载
总结:
- docker run 的命令通过 -v 参数挂载文件或目录到容器中:
-v volume [名称]
:容器内目录-v
宿主机文件:容器内文件-v
宿主机目录:容器内目录- 数据卷挂载与目录直接挂载的区别:
- 数据卷挂载耦合度低,由 docker 来管理目录,但是目录较深,不好找
- 目录直接挂载耦合度高,需要我们自己管理目录,不过目录容易找到
# 7.6、Dockerfile 自定义镜像🌳
# 自定义镜像
# 7.6.1、镜像结构🌴
- 镜像是将应用程序及其需要的系统函数库,环境,配置,依赖打包而成。
如果要制作一个镜像,就一定要先了解它内部的构成
上面说到镜像就是系统函数库,环境,配置,依赖组成的,而且它们之间有相互之间的依赖关系或者顺序。镜像不仅仅是把一对东西组合在一起而且还要按照一定顺序去翻层
下面以 mysql 为例讲讲镜像结构,我们把 mysql 结构打散
下图中 mysql 镜像结构分成了 n 层,它就是按照上面的依赖顺序来分层的,要想构建一个镜像,最底层一定是它所在的系统函数库,而最下面就是用了 Ubuntu 操作系统,把 mysql 安装包拷贝下来然后在此基于上使用 rpm 进行安装 mysql,安装完了进行配置文件的配置等等,所有的安装配置完流程就完成的差不多了,还差一个入口 Entrypoint 通过这个入口启动程序的,这就是镜像的构成流程
总结:
镜像是分层结构,每一层称为一个 Layer
- BaseImage 层:包含基本的系统函数库,环境变量,文件系统
- Entrypoint:入口,是镜像中应用启动的命令
- 其它:在 BaseImage 基础上添加依赖,安装程序,完成整个应用的安装和配置
# 7.6.2、什么是 Dockerfile🌲
Dockerfile 就是一个文本文件,其中包含一个个的指令 (Instruction) ,用指令来说明要执行什么操作来构建镜像。每一个指令都会形成一层 Layer
指令 | 说明 | 示例 |
---|---|---|
FROM | 指定基础镜像 | FROM centos:6 |
ENV | 设置环境变量,可在后面指令使用 | ENV key value |
COPY | 拷贝本地文件到镜像的指定目录 | COPY ./mysql-5.7.rpm /tmp |
RUN | 执行 Linux 的 shell 命令,一般是安装过程的命令 | RUN yum install gcc |
EXPOSE | 指定容器运行时监听的端口,是给镜像使用者看的 | EXPOSE 8080 |
ENTPYOINT | 镜像中应用的启动命令,容器运行时调用 | ENTPYPOINT java -jar x |
更新详细语法说明,请参考官网文档:https://docs.docker.com/engine/reference/builder
# 7.6.3、案例,基于 Ubuntu 镜像构建一个新镜像,运行一个 java 项目🌲
步骤:
1、新建一个空文件夹 docker-demo
➜ /tmp mkdir docker-demo | |
➜ /tmp cd docker-demo | |
➜ docker-demo l | |
total 0 | |
drwxrwxr-x. 2 dkx dkx 6 Oct 9 05:52 . | |
drwxrwxrwt. 11 root root 228 Oct 9 05:52 .. | |
➜ docker-demo |
2、拷贝 docker-demo.jar 文件到 docker-demo 这个目录中
3、拷贝 jdk8.tar.gz 文件到 docker-demo 这个目录
4、拷贝 Dockerfile 到 docker-demo 目录
Dockerfile 内容
# 指定基础镜像 | |
FROM ubuntu:16.04 | |
# 配置环境变量,JDK 的安装目录 | |
ENV JAVA_DIR=/usr/local | |
# 拷贝 jdk 和 java 项目的包 | |
COPY ./jdk8.tar.gz $JAVA_DIR/ | |
COPY ./docker-demo.jar /tmp/app.jar | |
# 安装 JDK | |
RUN cd $JAVA_DIR \ | |
&& tar -xf ./jdk8.tar.gz \ | |
&& mv ./jdk1.8.0_144 ./java8 | |
# 配置环境变量 | |
ENV JAVA_HOME=$JAVA_DIR/java8 | |
ENV PATH=$PATH:$JAVA_HOME/bin | |
# 暴露端口 | |
EXPOSE 8090 | |
# 入口,java 项目的启动命令 | |
ENTRYPOINT java -jar /tmp/app.jar |
➜ docker-demo ll | |
total 202M | |
-rw-r--r--. 1 dkx dkx 25M Oct 9 06:02 docker-demo.jar | |
-rw-r--r--. 1 dkx dkx 494 Oct 9 06:02 Dockerfile | |
-rw-r--r--. 1 dkx dkx 177M Oct 9 06:02 jdk8.tar.gz | |
➜ docker-demo |
6、在 docker-demo 目录中运行命令: docker build -t javaweb:1.0 .
➜ docker-demo sudo docker build -t javaweb:1.0 . | |
[sudo] password for dkx: | |
[+] Building 46.3s (9/9) FINISHED docker:default | |
=> [internal] load build definition from Dockerfile 0.0s | |
=> => transferring dockerfile: 591B 0.0s | |
=> [internal] load .dockerignore 0.0s | |
=> => transferring context: 2B 0.0s | |
=> [internal] load metadata for docker.io/library/ubuntu:16.04 5.6s | |
=> [1/4] FROM docker.io/library/ubuntu:16.04@sha256:1f1a2d56de1d604801a9671f301190704c25d604a416f59e03c04f5c6ffee0d6 30.9s | |
=> => resolve docker.io/library/ubuntu:16.04@sha256:1f1a2d56de1d604801a9671f301190704c25d604a416f59e03c04f5c6ffee0d6 0.0s | |
=> => sha256:1f1a2d56de1d604801a9671f301190704c25d604a416f59e03c04f5c6ffee0d6 1.42kB / 1.42kB 0.0s | |
=> => sha256:a3785f78ab8547ae2710c89e627783cfa7ee7824d3468cae6835c9f4eae23ff7 1.15kB / 1.15kB 0.0s | |
=> => sha256:b6f50765242581c887ff1acc2511fa2d885c52d8fb3ac8c4bba131fd86567f2e 3.36kB / 3.36kB 0.0s | |
=> => sha256:58690f9b18fca6469a14da4e212c96849469f9b1be6661d2342a4bf01774aa50 46.50MB / 46.50MB 27.2s | |
=> => sha256:b51569e7c50720acf6860327847fe342a1afbe148d24c529fb81df105e3eed01 857B / 857B 0.9s | |
=> => sha256:da8ef40b9ecabc2679fe2419957220c0272a965c5cf7e0269fa1aeeb8c56f2e1 528B / 528B 0.8s | |
=> => sha256:fb15d46c38dcd1ea0b1990006c3366ecd10c79d374f341687eb2cb23a2c8672e 170B / 170B 1.5s | |
=> => extracting sha256:58690f9b18fca6469a14da4e212c96849469f9b1be6661d2342a4bf01774aa50 3.5s | |
=> => extracting sha256:b51569e7c50720acf6860327847fe342a1afbe148d24c529fb81df105e3eed01 0.0s | |
=> => extracting sha256:da8ef40b9ecabc2679fe2419957220c0272a965c5cf7e0269fa1aeeb8c56f2e1 0.0s | |
=> => extracting sha256:fb15d46c38dcd1ea0b1990006c3366ecd10c79d374f341687eb2cb23a2c8672e 0.0s | |
=> [internal] load build context 1.6s | |
=> => transferring context: 211.19MB 1.6s | |
=> [2/4] COPY ./jdk8.tar.gz /usr/local/ 0.7s | |
=> [3/4] COPY ./docker-demo.jar /tmp/app.jar 0.1s | |
=> [4/4] RUN cd /usr/local && tar -xf ./jdk8.tar.gz && mv ./jdk1.8.0_144 ./java8 4.8s | |
=> exporting to image 4.0s | |
=> => exporting layers 3.9s | |
=> => writing image sha256:7163e4e0d2b830e1f411e6d50c40cb258e0110888af48e8ddce9642f72f564c1 0.0s | |
=> => naming to docker.io/library/javaweb:1.0 0.0s | |
➜ docker-demo |
可以看到镜像构建好了
[root@localhost ~]# docker images | |
REPOSITORY TAG IMAGE ID CREATED SIZE | |
javaweb 1.0 7163e4e0d2b8 About a minute ago 722MB | |
nginx latest 61395b4c586d 2 weeks ago 187MB | |
redis latest 7c4b517da47d 4 weeks ago 153MB | |
mysql 5.7.25 98455b9624a9 4 years ago 372MB | |
[root@localhost ~]# |
创建容器并运行
[root@localhost ~]# docker run --name web -p 8090:8090 -d javaweb:1.0 | |
de4fc104d20fd0c7a44fe227aa07bbba8fb5fd6c7ca04400e7b90c93375573a5 | |
[root@localhost ~]# docker ps | |
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES | |
de4fc104d20f javaweb:1.0 "/bin/sh -c 'java -j…" 4 seconds ago Up 2 seconds 0.0.0.0:8090->8090/tcp, :::8090->8090/tcp web | |
10d26568836c mysql:5.7.25 "docker-entrypoint.s…" 2 hours ago Up 2 hours 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp, 33060/tcp some-mysql | |
2ba6c6cf79f4 nginx "/docker-entrypoint.…" 4 hours ago Up 4 hours 0.0.0.0:80->80/tcp, :::80->80/tcp mn | |
[root@localhost ~]# |
访问页面 uri:http://192.168.249.128:8090/hello/count
访问成功
<font color='red'> 问题 </font>:
如果每个微服务构建都要进行下面的流程会很麻烦
# 指定基础镜像 | |
FROM ubuntu:16.04 | |
# 配置环境变量,JDK 的安装目录 | |
ENV JAVA_DIR=/usr/local | |
# 拷贝 jdk 和 java 项目的包 | |
COPY ./jdk8.tar.gz $JAVA_DIR/ | |
COPY ./docker-demo.jar /tmp/app.jar | |
# 安装 JDK | |
RUN cd $JAVA_DIR \ | |
&& tar -xf ./jdk8.tar.gz \ | |
&& mv ./jdk1.8.0_144 ./java8 | |
# 配置环境变量 | |
ENV JAVA_HOME=$JAVA_DIR/java8 | |
ENV PATH=$PATH:$JAVA_HOME/bin | |
# 暴露端口 | |
EXPOSE 8090 | |
# 入口,java 项目的启动命令 | |
ENTRYPOINT java -jar /tmp/app.jar |
从配置环境前 n 层的内容不会变,我们可以对前 n 层提前构建好做一个镜像。以后就都在这个构建好的基础上去构建镜像是不是就方便多了。
但实际上已经有人帮我们做好了这件事儿了。
就是如下:
# 7.6.4、基于 java:8-alpine 镜像,将一个 java 项目构建为镜像🌲
实现思路如下:
1、新建一个空的目录,然后在目录中新建一个文件,命名为 Dockerfile
2、拷贝 docker-demo.jar 到这个目录中
3、编写 Dockerfile 文件:
- 基于 java:8-alpine 作为基础镜像
- 将 app.jar 拷贝到镜像中
- 暴漏端口
- 编写入口 ENTRYPOINT
4、使用 docker build 命令构建镜像
5、使用 docker run 创建容器并运行
# 指定基础镜像 | |
FROM java:8-alpine | |
# 暴露端口 | |
EXPOSE 8090 | |
# 入口,java 项目的启动命令 | |
ENTRYPOINT java -jar /tmp/app.jar |
直接节省了大量的代码工作
修改完成保存后,覆盖 Linux,/tmp/docker-demo 中的 Dockerfile 文件
覆盖成功
使用 cat 命令查看下修改情况
[root@localhost ~]# cat /tmp/docker-demo/Dockerfile | |
# 指定基础镜像 | |
FROM java:8-alpine | |
# 暴露端口 | |
EXPOSE 8090 | |
# 入口,java 项目的启动命令 | |
ENTRYPOINT java -jar /tmp/app.jar[root@localhost ~]# |
再次进行构建镜像
如果构建遇到报错如下:
➜ docker-demo sudo docker build -t javaweb:1.0 . | |
[+] Building 2.7s (3/3) FINISHED docker:default | |
=> [internal] load .dockerignore 0.0s | |
=> => transferring context: 2B 0.0s | |
=> [internal] load build definition from Dockerfile 0.0s | |
=> => transferring dockerfile: 245B 0.0s | |
=> ERROR [internal] load metadata for docker.io/library/openjava:8-alpine 2.6s | |
------ | |
> [internal] load metadata for docker.io/library/openjava:8-alpine: | |
------ | |
Dockerfile:2 | |
-------------------- | |
1 | # 指定基础镜像 | |
2 | >>> FROM openjava:8-alpine | |
3 | | |
4 | # 暴露端口 | |
-------------------- | |
ERROR: failed to solve: openjava:8-alpine: pull access denied, repository does not exist or may require authorization: server message: insufficient_scope: authorization failed |
则需要将 Dockerfile 文件中的 java:8-alpine 改为 openjdk:8-alpine
# 指定基础镜像 | |
FROM openjdk:8-alpine | |
# 暴露端口 | |
EXPOSE 8090 | |
# 入口,java 项目的启动命令 | |
ENTRYPOINT java -jar /tmp/app.jar |
再次进行构建镜像
➜ docker-demo sudo docker build -t javaweb:1.0 . | |
[+] Building 89.8s (5/5) FINISHED docker:default | |
=> [internal] load .dockerignore 0.0s | |
=> => transferring context: 2B 0.0s | |
=> [internal] load build definition from Dockerfile 0.0s | |
=> => transferring dockerfile: 244B 0.0s | |
=> [internal] load metadata for docker.io/library/openjdk:8-alpine 6.1s | |
=> [1/1] FROM docker.io/library/openjdk:8-alpine@sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3 83.6s | |
=> => resolve docker.io/library/openjdk:8-alpine@sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3 0.0s | |
=> => sha256:a3562aa0b991a80cfe8172847c8be6dbf6e46340b759c2b782f8b8be45342717 3.40kB / 3.40kB 0.0s | |
=> => sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10 2.76MB / 2.76MB 10.9s | |
=> => sha256:f910a506b6cb1dbec766725d70356f695ae2bf2bea6224dbe8c7c6ad4f3664a2 238B / 238B 0.8s | |
=> => sha256:c2274a1a0e2786ee9101b08f76111f9ab8019e368dce1e325d3c284a0ca33397 70.73MB / 70.73MB 80.6s | |
=> => sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3 1.64kB / 1.64kB 0.0s | |
=> => sha256:44b3cea369c947527e266275cee85c71a81f20fc5076f6ebb5a13f19015dce71 947B / 947B 0.0s | |
=> => extracting sha256:e7c96db7181be991f19a9fb6975cdbbd73c65f4a2681348e63a141a2192a5f10 0.2s | |
=> => extracting sha256:f910a506b6cb1dbec766725d70356f695ae2bf2bea6224dbe8c7c6ad4f3664a2 0.0s | |
=> => extracting sha256:c2274a1a0e2786ee9101b08f76111f9ab8019e368dce1e325d3c284a0ca33397 2.8s | |
=> exporting to image 0.0s | |
=> => exporting layers 0.0s | |
=> => writing image sha256:d92b26b799ba6c537e486f7619b96f16ae44e8d4fb296ab05cd80e9d764f5b30 0.0s | |
=> => naming to docker.io/library/javaweb:1.0 0.0s | |
➜ docker-demo |
构建完成!
再次进行访问页面 uri:http://192.168.249.128:8090/hello/count
总结:
- Dockerfile 的本质是一个文件,通过指令描述镜像的构建过程
- Dockerfile 的第一行必须是 FROM,从一个基础镜像来构建
- 基础镜像可以是基本操作系统,如 Ubuntu。也可以是其它人制作好的镜像,例如:java:8-alpine 亦或者 openjdk:8-alpine
# 7.6.5、DockerCompose🌲
- 初始 DockerCompose
- 部署微服务集群
实际生产环境中微服务很多很多,数十上百甚至上千个微服务我们如果一个一个去构建这可得累死了。所以我们需要有一种集群部署的手段,那么这就是我们要学习的 DockerCompose 了
# 7.6.5.1、什么是 DockerCompose🌴
- DockerCompose 可以基于 Compose 文件帮我们快速的部署分布式应用,而无需手动一个一个创建和运行容器!
- Compose 文件是一个文本文件, 通过指令定义集群中的每个容器如果运行。
- Compose 文件是把 Docker run 命令集合进来的然后把语法转换了一下
DockerCompose 文件就是把 Docker run 的各种参数转换成指令去定义了。
- DockerCompose 的详细语法参考官网:https://docs.docker.com/compose/compose-file/
上面了解后我们就进行安装 DockerCompose:查看安装文章.
总结:
DockerCompose 有什么用?
- 帮助我们快速部署分布式应用,无需一个一个微服务去构建镜像和部署
# 7.6.5.2、DockerCompose 部署🌴
案例,将之前学习的 cloud-demo 微服务集群利用 DockerCompose 部署.
实现思路如下:
1、创建与 cloud-demo 项目目录相同的几个文件夹
三个微服务需要依赖于 mysql 所以需要准备 mysql 的数据
2、每个文件夹里面存放 Dockerfile 文件,内容如下:
# 基础镜像 | |
FROM java:8-alpine | |
# 拷贝当前目录中的 app.jar 到 tem 目录中,将运行的 jar 包拷贝到镜像中 | |
COPY ./app.jar /tmp/app.jar | |
# 启动脚本 | |
ENTRYPOINT java -jar /tmp/app.jar |
除了 mysql 的目录中再创建两个目录
conf 目录中存放 mysql 配置文件
data 目录里面存放数据库的数据
3、最外层创建一个 docker-compose.yml 文件
内容为:
version: "3.2" | |
services: | |
nacos: | |
image: nacos/nacos-server | |
environment: # 环境配置,启动参数 | |
MODE: standalone | |
ports: | |
- "8848:8848" | |
mysql: | |
image: mysql:5.7.25 | |
environment: | |
MYSQL_ROOT_PASSWORD: 123 | |
# 数据卷挂载 | |
volumes: # 使用 Linux 指令 pwd 获取当前目录位置然后从这个位置找对应的目录,不写硬编码 | |
- "$PWD/mysql/data:/var/lib/mysql" | |
- "$PWD/mysql/conf:/etc/mysql/conf.d/" | |
userservice: | |
# 服务构建基于 dockerfile 完成构建,在当前目录下的 user-service 目录下找到 dockerfile 进行构建镜像 | |
build: ./user-service | |
# 服务构建基于 dockerfile 完成构建,在当前目录下的 order-service 目录下找到 dockerfile 进行构建镜像 | |
orderservice: | |
build: ./order-service | |
gateway: | |
# 网关构建基于 dockerfile 完成构建,在当前目录下的 gateway 目录下找到 dockerfile 进行构建镜像 | |
build: ./gateway | |
# 只有网关暴漏了端口,剩下上面的微服务都没有暴漏,因为网关才是入口而不应该将微服务暴漏出去 | |
ports: | |
- "10010:10010" |
将来 DockerCompose 就会帮我们自动的去构建这三个微服务的镜像了,实现一次性部署
4、修改自己的 cloud-demo 项目,将数据库,nacos 地址都命名为 docker-compose 中的服务名
不能把 nacos,数据库之类的配置地址写死,因为微服务可能配置在不同的机器上
用 DockerCompose 部署时所有的服务之间都可以用服务名称去互相访问,这是 DockerCompose 底层做了一些配置
user-service 中的 nacos 配置
spring: | |
application: | |
name: userservice # 服务名称 | |
profiles: | |
active: dev # 环境 | |
cloud: | |
nacos: | |
server-addr: nacos:8848 # nacos 地址 | |
discovery: | |
# cluster-name: TJ | |
config: | |
file-extension: yaml # 配置文件后缀名 | |
# 三要素:服务名称,环境,后缀名 而这三个决定了 DateId |
user-service 中的 mysql 配置
spring: | |
datasource: | |
url: jdbc:mysql://mysql:3306/cloud_user?useSSL=false&characterEncoding=utf-8&serverTimezone=UTC | |
username: root | |
password: dkx. | |
driver-class-name: com.mysql.cj.jdbc.Driver |
同样将 order-service 中的 mysql 和 nacos 地址配置改过来
spring: | |
datasource: | |
url: jdbc:mysql://mysql:3306/cloud_order?useSSL=false&characterEncoding=utf-8&serverTimezone=UTC | |
username: root | |
password: dkx. | |
driver-class-name: com.mysql.cj.jdbc.Driver | |
application: | |
name: orderservice | |
cloud: | |
nacos: | |
server-addr: nacos:8848 | |
discovery: | |
# cluster-name: HZ # 集群名称 | |
# namespace: ed32fa6f-e9e3-4483-b860-7bcd9731d0d9 # 命名空间,填 ID dev 环境 | |
ephemeral: true # 设置是否为临时实例 设置为非临时实例 |
修改网关服务的 nacos 地址配置
#=========== 这些配置让网关能够联系上 nacos 实现服务注册和发现 ==== | |
server: #|| | |
port: 10010 # 网关端口 #|| | |
spring: #|| | |
application: #|| | |
name: gateway # 服务名称 #|| | |
cloud: #|| | |
nacos: #|| | |
server-addr: nacos:8848 # nacos 地址 #|| | |
#======================================================== |
5、使用 maven 打包工具,将项目中的每个微服务都打包为 app.jar 文件
如果要让项目打包为 app.jar 名称则需要在 pom.xml 文件中配置如下:
<build> | |
<finalName>app</finalName> | |
</build> |
我们在所有需要打包的服务中的 pom 文件里面进行配置
先进行一下 clean 清空一个然后再进行 install 安装本地
6、将打包好的 app.jar 拷贝到 cloud-demo 中的每一个对应的子目录中
7、将 cloud-demo 上传至虚拟机,利用 docker-compose up -d 来部署
参数说明:up - 创建并执行容器
-d - 后台运行
其它参数 help 参见
[root@localhost tmp]# cd cloud-demo/ | |
[root@localhost cloud-demo]# ls | |
docker-compose.yml gateway mysql order-service user-service | |
[root@localhost cloud-demo]# |
使用命令进行部署
[root@localhost cloud-demo]# docker-compose up -d | |
Building userservice | |
[+] Building 3.5s (7/7) FINISHED docker:default | |
=> [internal] load build definition from Dockerfile 0.0s | |
=> => transferring dockerfile: 182B 0.0s | |
=> [internal] load .dockerignore 0.0s | |
=> => transferring context: 2B 0.0s | |
=> [internal] load metadata for docker.io/library/openjdk:8-alpine 2.7s | |
=> [internal] load build context 0.3s | |
=> => transferring context: 39.30MB 0.3s | |
=> CACHED [1/2] FROM docker.io/library/openjdk:8-alpine@sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3 0.0s | |
=> [2/2] COPY ./app.jar /tmp/app.jar 0.2s | |
=> exporting to image 0.3s | |
=> => exporting layers 0.2s | |
=> => writing image sha256:ab22b9b33217d7b1e7287dc7b16564ba7a683bcc390e961ffb7d6f77069ea697 0.0s | |
=> => naming to docker.io/library/cloud-demo_userservice 0.0s | |
WARNING: Image for service userservice was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. | |
Building orderservice | |
[+] Building 1.4s (7/7) FINISHED docker:default | |
=> [internal] load build definition from Dockerfile 0.0s | |
=> => transferring dockerfile: 182B 0.0s | |
=> [internal] load .dockerignore 0.0s | |
=> => transferring context: 2B 0.0s | |
=> [internal] load metadata for docker.io/library/openjdk:8-alpine 0.6s | |
=> [internal] load build context 0.3s | |
=> => transferring context: 41.82MB 0.3s | |
=> CACHED [1/2] FROM docker.io/library/openjdk:8-alpine@sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3 0.0s | |
=> [2/2] COPY ./app.jar /tmp/app.jar 0.1s | |
=> exporting to image 0.2s | |
=> => exporting layers 0.2s | |
=> => writing image sha256:4fb619347bfb2d9aac0e824d4334973c18761b4ee64c1f9fddbd3b6041b8fad1 0.0s | |
=> => naming to docker.io/library/cloud-demo_orderservice 0.0s | |
WARNING: Image for service orderservice was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. | |
Building gateway | |
[+] Building 1.2s (7/7) FINISHED docker:default | |
=> [internal] load build definition from Dockerfile 0.0s | |
=> => transferring dockerfile: 182B 0.0s | |
=> [internal] load .dockerignore 0.0s | |
=> => transferring context: 2B 0.0s | |
=> [internal] load metadata for docker.io/library/openjdk:8-alpine 0.6s | |
=> [internal] load build context 0.3s | |
=> => transferring context: 39.92MB 0.3s | |
=> CACHED [1/2] FROM docker.io/library/openjdk:8-alpine@sha256:94792824df2df33402f201713f932b58cb9de94a0cd524164a0f2283343547b3 0.0s | |
=> [2/2] COPY ./app.jar /tmp/app.jar 0.1s | |
=> exporting to image 0.2s | |
=> => exporting layers 0.2s | |
=> => writing image sha256:53993c010033dc8356f48ddd183ad1768544070ceb7e803ada30d20e9b3ae316 0.0s | |
=> => naming to docker.io/library/cloud-demo_gateway 0.0s | |
WARNING: Image for service gateway was built because it did not already exist. To rebuild this image you must use `docker-compose build` or `docker-compose up --build`. | |
Creating cloud-demo_nacos_1 ... done | |
Creating cloud-demo_userservice_1 ... done | |
Creating cloud-demo_mysql_1 ... done | |
Creating cloud-demo_gateway_1 ... done | |
Creating cloud-demo_orderservice_1 ... done | |
[root@localhost cloud-demo]# |
部署成功使用命令查看启动日志:docker-compose logs -f
[root@localhost cloud-demo]# docker-compose logs -f | |
Attaching to cloud-demo_orderservice_1, cloud-demo_gateway_1, cloud-demo_userservice_1, cloud-demo_mysql_1, cloud-demo_nacos_1 | |
gateway_1 | | |
gateway_1 | . ____ _ __ _ _ | |
gateway_1 | /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ | |
gateway_1 | ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ | |
gateway_1 | \\/ ___)| |_)| | | | | || (_| | ) ) ) ) | |
gateway_1 | ' |____| .__|_| |_|_| |_\__, | / / / / | |
gateway_1 | =========|_|==============|___/=/_/_/_/ | |
gateway_1 | :: Spring Boot :: (v2.3.9.RELEASE) | |
gateway_1 | | |
gateway_1 | 10-09 13:37:38:143 INFO 1 --- [ main] cn.itcast.gateway.GatewayApplication : No active profile set, falling back to default profiles: default | |
gateway_1 | 10-09 13:37:41:667 INFO 1 --- [ main] o.s.cloud.context.scope.GenericScope : BeanFactory id=87ef38f0-5652-3427-832e-5397c91e63b6 | |
.... | |
.... |
启动时 nacos 会报错因为它启动的比较慢我们需要先启动 nacos 执行命令: docker-compose up -d nacos
等待 nacos 启动完成后可以访问 uri 测试一下:http://192.168.249.128:8848/nacos
如果有报错信息可以使用命令重启服务 docker-compose restart gateway userservice orderservice
[root@localhost cloud-demo]# docker-compose restart gateway userservice orderservice | |
Restarting cloud-demo_orderservice_1 ... done | |
Restarting cloud-demo_gateway_1 ... done | |
Restarting cloud-demo_userservice_1 ... done | |
[root@localhost cloud-demo]# |
# 7.6.6、Docker 镜像仓库🌲
- 搭建私有镜像仓库
- 向镜像仓库推送镜像
- 从镜像仓库拉取镜像
# 7.6.6.1、常见镜像仓库服务🌴
镜像仓库 (Docker Registry) 有公共和私有的两种形式:
- 公共仓库:例如 Docker 官方的 Docker Hub,国内也有一些服务商提供类似于 Docker Hub 的公开服务,比如 网易云镜像服务,DaoCloud 镜像服务,阿里云镜像服务等。
- 除了使用公开仓库外,用户还可以在本地搭建私有 Docker Registry。企业自己的镜像最好是采用私有 Docker Registry 来实现。
# 7.6.6.2、创建私服镜像仓库查看文章🌴
# 7.6.6.3、在私有镜像仓库推送或拉取镜像🌴
推送镜像到私有镜像服务必须先 tag,步骤如下:
1、重新 tag 本地镜像,名称前缀为私有仓库的地址:192.168.150.101:8080/
docker tag nginx:latest 192.168.150.101:8080/nginx:1.0 |
使用 docker images 查看打包好的镜像
2、推送镜像
docker push 192.168.150.101:8080/nginx:1.0 |
3、拉取镜像
docker pull 192.168.150.101:8080/nginx:1.0 |
总结:
- 推送本地镜像到仓库前都必须重命名 (docker tag) 镜像,以镜像仓库地址为前缀
- 镜像仓库推送前需要把仓库地址配置到 docker 服务的 daemon.json 文件中,被 docker 信任
- 推送使用 docker push 命令
- 拉取使用 docker pull 命令