Docker学习记录
聊聊DockerDocker是基于Go语言开发的开源项目官网[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmmP21tW-1610010941702)(docker.assets/image-20201026153235517.png)]文档地址 Docker文档是超级详细的,就是看不懂而已仓库地址Docker能干啥以前的虚拟技术虚拟机技术的缺点1、资源占用十分多
聊聊Docker
Docker
是基于Go
语言开发的开源项目
官网
文档地址 Docker文档是超级详细的,就是看不懂而已
Docker能干啥
-
以前的虚拟技术
虚拟机技术的缺点
1、资源占用十分多
2、冗余步骤多
3、启动很慢
-
容器化技术
比较Docker和虚拟机技术的不同
1、传统虚拟机虚拟出一套硬件,运行一个完整的操作系统,然后再这个系统上安装和运行软件
2、容器内的应用直接运行在宿主机的内容,容器没有自己的内核,也没有虚拟我们的硬件,所以就轻便了
3、每个容器间是互相隔离的,每个容器内都有一个属于自己的文件系统,互不影响
-
DevOps(开发运维)
-
应用更快速的交付和部署
传统:一堆帮助文档,安装程序
Docker
:打包镜像发布测试,一键运行 -
更快捷的升级和扩缩容
使用了
Docker
之后,我们部署的应用就和搭积木一样
-
Docker的基本组成
Docker的基本组成
镜像(image):
docker
镜像就好比是一个模板,可以通过这个模板来创建容器服务,tomcat
镜像——>run
——>tomcat01
容器(提供服务器),通过这个镜像可以创建多个容器。
容器(container):
Docker
利用容器技术,独立运行一个或者一组应用,通过镜像来创建
启动,停止,删除,基础命令
目前就可以把这个容器理解为一个简易的Linux
系统
仓库(reponsitory):
仓库就是存放镜像的地方
仓库分为公有仓库和私有仓库
Docker Hub
阿里云…都有容器服务器(配置镜像加速)
Docker安装
环境准备
1、CentOS8
2、Xshell
环境查看
#系统内核是3.10以上
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# uname -r
4.18.0-147.5.1.el8_1.x86_64
安装
#1、卸载旧版本
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
#2、下载需要的安装包
sudo yum install -y yum-utils
#3、设置镜像的仓库
yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo#默认是国外的
yum-config-manager \
--add-repo \
http://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo#推荐使用阿里云的,十分的快
#4、更新yum软件包的索引
yum makecache
#5安装新版的containerd.io
dnf install https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.6-3.3.el7.x86_64.rpm
#6、安装docker相关的内容 docker-ce 社区版 ee企业版
sudo yum install docker-ce docker-ce-cli
#7、启动Docker
systemctl start docker
#8、判断是否启动成功
docker version
#8、执行HelloWord程序
docker run hello-word
#9、查看下载的hello-world镜像
docker images
#10、卸载Docker
#卸载依赖
yum remove docker-ce docker-ce-cli containerd.io
#删除目录资源
rm -rf /var/lib/docker
阿里云镜像加速
找到镜像加速器
配置使用
sudo mkdir -p /etc/docker
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://ks6vp0jf.mirror.aliyuncs.com"]
}
EOF
sudo systemctl daemon-reload
sudo systemctl restart docker
#列出防火墙开放的端口
firewall-cmd --zone=public --list-ports
#打开8080端口
firewall-cmd --zone=public --add-port=8080/tcp --permanent
#项目部署
docker run -it -v /home/HaveFunResources/pictures:/home/HaveFunResources/pictures --net=host --name demo -p 8080:8080 demo
底层原理
Docker是怎么工作的?
Docker
是一个Client-Server
结构的系统,Docker
的守护进程运行在主机上,通过Socket
从客户端访问
DockerServer
接收到Docker-Client
的指令,就会执行这个命令
Docker为什么比VM快?
1、Docker
有着比虚拟机更少的抽象层
2、Docker
利用的是宿主机的内核,vm
需要是Guest OS
所以说,新建一个容器的时候,docker
不需要像虚拟机一样重新加载一个操作系统内核,避免引导性的操作,虚拟机是加载Guest OS
,分钟级别
而Docker
是利用宿主机的操作系统,省略掉这个复杂的过程,秒级。
Docker的常用命令
帮助命令
docker version #显示docker的版本信息
docker info #显示docker的系统信息,包括镜像和容器的数量
docker 命令 --help #帮助命令
帮助文档地址:https://docs.docker.com/reference/
镜像命令
docker images 查看所有本地的主机上的镜像
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
#解释
REPOSITORY 镜像的仓库源
TAG 镜像的标签
IMAGE ID 镜像的id
CREATED 镜像创建的时间
SIZE 镜像的大小
#可选项
-a, --all #列出所有镜像
-q, --quiet #只显示镜像的ID
docker search搜索镜像
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker search mysql
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
mysql MySQL is a widely used, open-source relation… 10103 [OK]
mariadb MariaDB is a community-developed fork of MyS… 3708 [OK]
mysql/mysql-server Optimized MySQL Server Docker images. Create… 738 [OK]
#可选项 通过STARS来过滤
docker search mysql --filter=STARS=3000 #会去搜索镜像STARS大于3000的
docker pull 下载镜像
#下载镜像 docker pull 镜像名[:tag]
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker pull mysql
Using default tag: latest #如果不写tag,默认最新
latest: Pulling from library/mysql
bb79b6b2107f: Pull complete #分层下载 docker images的核心 联合文件系统
49e22f6fb9f7: Pull complete
842b1255668c: Pull complete
9f48d1f43000: Pull complete
c693f0615bce: Pull complete
8a621b9dbed2: Pull complete
0807d32aef13: Pull complete
a56aca0feb17: Pull complete
de9d45fd0f07: Pull complete
1d68a49161cc: Pull complete
d16d318b774e: Pull complete
49e112c55976: Pull complete
Digest: sha256:8c17271df53ee3b843d6e16d46cff13f22c9c04d6982eb15a9a47bd5c9ac7e2d #签名,防伪
Status: Downloaded newer image for mysql:latest
docker.io/library/mysql:latest #真实地址
#即:docker pull mysql 等价于 docker pull docker.io/library/mysql:latest
#指定版本下载:
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker pull mysql:5.7
5.7: Pulling from library/mysql
bb79b6b2107f: Already exists
49e22f6fb9f7: Already exists
842b1255668c: Already exists
9f48d1f43000: Already exists
c693f0615bce: Already exists
8a621b9dbed2: Already exists
0807d32aef13: Already exists
f15d42f48bd9: Pull complete
098ceecc0c8d: Pull complete
b6fead9737bc: Pull complete
351d223d3d76: Pull complete
Digest: sha256:4d2b34e99c14edb99cdd95ddad4d9aa7ea3f2c4405ff0c3509a29dc40bcb10ef
Status: Downloaded newer image for mysql:5.7
docker.io/library/mysql:5.7
docker rmi 删除镜像
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker rmi -f 镜像id #删除指定的镜像
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker rmi -f 镜像id 镜像id 镜像id #删除多个镜像
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker rmi -f $(docker images -qa) #删除全部镜像
容器命令
说明:有了镜像才可以创建容器,linux,下载一个centos来测试学习
docker pull centos
新建容器并启动
docker run [可选参数] image
#参数说明
--name="name" #容器名字 用来区分容器
-d #后台方式运行
-it #使用交互方式运行,进入容器查看内容
-p #小写
-p ip:主机端口:容器端口
-p 主机端口:容器端口(常用)
-p 容器端口
容器端口
-P #随机指定端口(大写)
#测试,启动并进入容器,主机名就是镜像id
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker run -it centos /bin/bash
[root@258ac6601c93 /]# ls #查看容器内的centos,但是是基础版本,很多命令不完善
bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
[root@258ac6601c93 /]# exit #退出容器
exit
列出所有运行的容器
#docker ps 命令
#列出当前运行中的容器
-a #列出当前运行中的容器,带出历史运行过的容器
-n=?#列出最近创建的容器
-q #只显示容器的编号
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
258ac6601c93 centos "/bin/bash" 4 minutes ago Exited (0) About a minute ago compassionate_driscoll
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps -n=1
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
258ac6601c93 centos "/bin/bash" 7 minutes ago Exited (0) 5 minutes ago compassionate_driscoll
退出容器
exit #直接容器停止不退出
Ctrl + p + Q #容器不停止退出
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker run -it centos
[root@02a809f869be /]# [root@iZ2ze5wj5w33v3gyd9kv1bZ /]# cd ..
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
02a809f869be centos "/bin/bash" 51 seconds ago Up 50 seconds amazing_poitras
删除容器
docker rm 容器id #删除指定的容器,不能删除正在运行的容器,如果要强行删除,加-f
docker rm -f $(docker ps -aq) #删除全部容器
docker ps -a -q | xargs docker rm #删除全部容器
启动和停止容器的操作
docker start 容器id #启动
docker restart 容器id #重启
docker stop 容器id #停止
docker kill 容器id #杀死,如果停止报错,直接强行干掉
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
02a809f869be centos "/bin/bash" 7 minutes ago Up 7 minutes amazing_poitras
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker stop 02a809f869be
02a809f869be
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
02a809f869be centos "/bin/bash" 7 minutes ago Exited (0) 9 seconds ago amazing_poitras
258ac6601c93 centos "/bin/bash" 17 minutes ago Exited (0) 15 minutes ago compassionate_driscoll
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker start 02a809f869be
02a809f869be
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
02a809f869be centos "/bin/bash" 8 minutes ago Up 6 seconds amazing_poitras
常用的其他命令
后台启动容器
#命令 docker run -d 镜像名
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker run -d centos
#问题docker ps,发现centos停止了
#常见的坑,docker 容器使用后台运行,就必须要有一个前台进程,docker发现没有应用,就会自动停止
#nginx,容器启动后,发现自己没有提供服务,就会立刻停止,就是没有程序了。
查看日志
docker logs -tf --tail 10 容器 #没有日志
#自己编写一段shell脚本
docker run -d centos /bin/sh -c "while true;do echo phz;sleep 1;done"
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a20ddc3959fd centos "/bin/bash" 6 minutes ago Up 6 minutes naughty_heyrovsky
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker run -d centos /bin/sh -c "while true;do echo phz;sleep 1;done"
be6dd3b0adcdf7fe7a69da47d113e835bec20711469a01ad613ea5403bf02e42
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
be6dd3b0adcd centos "/bin/sh -c 'while t…" 4 seconds ago Up 3 seconds youthful_boyd
a20ddc3959fd centos "/bin/bash" 7 minutes ago Up 6 minutes naughty_heyrovsky
#显示日志
-tf #显示日志
--tail number #要显示的日志条数
docker logs -tf --tail 10 be6dd3b0adcd
查看容器中的进程命令
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
65002f0bcb84 centos "/bin/sh" 56 seconds ago Up 54 seconds sad_wescoff
[root@iZ2ze5wj5w33v3gyd9kv1bZ /]# docker top 65002f0bcb84
UID PID PPID C STIME TTY TIME CMD
root 31347 31331 0 10:29 pts/0 00:00:00 /bin/sh
查看镜像的元数据
#命令
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker inspect 65002f0bcb84
进入当前正在运行的容器
#我们通常容器都是使用后台方式运行的,需要进入容器,修改一些配置
#方式一
#命令
docker exec -it 容器id bashShell
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6e8f7795e92a centos "/bin/bash" About a minute ago Up About a minute agitated_driscoll
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
6e8f7795e92a centos "/bin/bash" 2 minutes ago Up About a minute agitated_driscoll
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker exec -it 6e8f7795e92a /bin/bash
[root@6e8f7795e92a /]# ls
bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
[root@6e8f7795e92a /]# ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 04:34 pts/0 00:00:00 /bin/bash
root 20 0 0 04:36 pts/1 00:00:00 /bin/bash
root 35 20 0 04:38 pts/1 00:00:00 ps -ef
#方式二
docker attach 容器id
#测试
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker attach 6e8f7795e92a
#正在执行的代码。。。。。
#区别:
#docker exec #进入容器后开启一个新的终端,可以在里面操作(常用)
#docker attach #进入容器正在执行的终端,不会启动新的进程。
从容器内拷贝文件到主机上
docker cp 容器id:容器内路径 目的主机路径
- 先创建一个centos容器,并在其home文件夹下新建一个文件
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker attach 5b402a1c7c33
[root@5b402a1c7c33 /]# ls
bin dev etc home lib lib64 lost+found media mnt opt proc root run sbin srv sys tmp usr var
[root@5b402a1c7c33 /]# cd home/
[root@5b402a1c7c33 home]# ls
[root@5b402a1c7c33 home]# touch my.java
[root@5b402a1c7c33 home]# ls
my.java
[root@5b402a1c7c33 home]# exit
exit
对于容器内部的数据或者文件,无论他是否在运行,都存在计算机中,所以这里直接退出了。
#将容器中的文件拷贝出来
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker cp 5b402a1c7c33:/home/my.java /root
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# ls /root
demo-0.0.1-SNAPSHOT.jar Dockerfile my.java
- 拷贝是一个手动过程,未来我们使用
-v
卷的技术,可以实现,自动同步
小结
attach #当前shell下attach连接指定运行镜像
buiTd #通过Dockerfile定制镜像
commit #提交当前容器为新的镜像
cp #从容器中拷贝指定文件或者目录到宿主机中
create #创建一个新的容器,同run,但不启动容器
diff #查看docker容器变化
events #从docker:那务获取容器实时事件
exec #在已存在的容器上运行命令
export #导出容器的内容流作为一个tar归档文件[对应 import]
history #展示一个镜像形成历史
images #列出系统当前镜像
import #从tar包中的内容创建一个新的文件系统映像[对应export]
info #显示系统相关信息
inspect #查看容器详细信息
kill #kill指定docker容器
load #从一个 tar包中加载一个镜像[对应save]
login #注册或者登陆一个 docker 服务器
logout #从当前Docker registry 退出
logs #输出当前容器日志信息
port #查看映射端口对应的容器内部源端口
pause #暂停容器
ps #列出容器列表
pull #从docker镜像源服务器拉取指定镜像或者库镜像
push #推送指定镜像或者库镜像至docker源服务器
restart #重启运行的容器
rm #移除一个或者多个容器
rmi #移除一个或多个镜像[无容器使用该镜像才可删除,否则需删除相关容器才可继续或-f强制删除]
run #创建一个新的容器并运行一个命令
save #保存一个镜像为一个 tar 包[对应load]
search #在docker hub中搜索镜像
start #启动容器
stop #停止容器
tag #给源中镜像打标签
top #查看容器中运行的进程信息
unpause #取消暂停容器
version #查看docker 版本号
wait #截取容器停止时的退出状态值
练习
安装Nginx
docker search nginx
#拉取nginx镜像
docker pull nginx
#创建以一个nginx容器,-d后台运行,--name取个别名,-p指定端口映射
docker run -d --name nginx -p 8181:80 nginx
这里使用主机的8181
端口映射容器中nginx
的默认80
端口,保证安全组打开:
#执行本机测试
curl localhost:8181
#进入容器
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker exec -it 70988532f1b4 /bin/bash
root@70988532f1b4:/# whereis nginx
nginx: /usr/sbin/nginx /usr/lib/nginx /etc/nginx /usr/share/nginx
root@70988532f1b4:/# ls /etc/nginx/
conf.d fastcgi_params koi-utf koi-win mime.types modules nginx.conf scgi_params uwsgi_params win-utf
安装Tomcat
#我们之前的启动都是后台,停止了容器之后,容器还是可以查到docker run -it --rm,一般用来测试,用完及删除,只是删除容器,下载下来的镜像还在
#docker run -it --rm tomcat:9.0
#这里我们正常启动tomcat
docker run -d -p 9910:8080 tomcat
为什么404?
#进入容器
docker exec -it 564ad8f5f291 /bin/bash
因为阿里云的镜像默认是最小的,把所有没必要的都给删了,保证了最小运行环境
但是我们也可以解决这个404问题
cp -r webapps.dist/* webapps
可视化
- portainer
docker run -d -p 9910:9000 \
--restart=always -v /var/run/docker.sock:/var/run/docker.sock --privileged=true portainer/portainer
第一次进入很慢,打开后设置一个密码进入
选择本地
local
连接,连接成功以后就可以以图形化界面管理我们的docker
Docker镜像
什么是镜像
镜像是一种轻量级、可执行的独立软件包,用来打包软件运行环境和基于运行环境开发的软件,它包含运行某个软件所需的所有内容,包括代码、运行时、库、环境变量和配置文件。——所有的应用打包部署
- 如何得到镜像
- 从远程仓库下载
- 朋友拷贝
- 自己制作一个镜像
Dockerfile
Docker镜像加载原理
UnionFS(联合文件系统)
UnionFS
(联合文件系统) :Union
文件系统是一种分层、轻量级并且高性能的文件系统,它支持对文件系统的修改作为一次提交来一层层的叠加,同时可以将不同目录挂载到同一个虚拟文件系统下。Union
文件系统是Docker
镜像的基础。镜像可以通过分层来进行继承,基于基础镜像(没有父镜像),可以制作各种具体的应用镜像。
特性:一次同时加载多个文件系统,但从外面看起来,只能看到一个文件系统,联合加载会把各层文件系统叠加起来,这样最终的文件系统会包含所有底层的文件和目录
Docker镜像加载原理
docker
的镜像实际上由一层一层的文件系统组成,这种层级的文件系统UnionFS
。
bootfs
(boot file system)
主要包含bootloader
(加载器)和kernel
(内核), bootloader
主要是引导加载kernel
, Linux
刚启动时会加载bootfs
文件系统,在Docker
镜像的最底层是bootfs
。这一层与我们典型的Linux/Unix
系统是一样的,包含boot
加载器和内核。当boot
加载完成之后整个内核就都在内存中了,此时内存的使用权已由bootfs
转交给内核,此时系统也会卸载bootfs
。
roots
(root fle system)
,在boots
之上。包含的就是典型Linux
系统中的/dev
, /proc
, /bin
,/etc
等标准目录和文件。rootfs
就是各种不同的操作系统发行版,比如Ubuntu
,Centos
等等。
分层的理解
分层的镜像
我们下载镜像的时候都是一层一层的下载的
而分层的最大的好处就是莫过于是资源共享了!比如有多个镜像都从相同的Base镜像构建而来,那么宿主机只需在磁盘上保留一份base镜像,同时内存中也只需要加载一份base镜像,这样就可以为所有的容器服务了,而且镜像的每一层都可以被共享。
#查看镜像分层
docker inspect tomcat:9.0
#安装一个新的镜像
docker pull redis
- 这里可以发现本地某一层已经下载了,就不会再下载了
理解
所有的Docker
镜像都起始于一个基础镜像层,当进行修改或增加新的内容时,就会在当前镜像层之上,创建新的镜像层。
举一个简单的例子,假如基于Ubuntu Linux 16.04
创建一个新的镜像,这就是新镜像的第一层;如果在该镜像中添加Python
包,就会在基础镜像层之上创建第二个镜像层;如果继续添加一个安全补丁,就会创建第三个镜像层。
该镜像当前已经包含3个镜像层,如下图所示(这只是一个用于演示的很简单的例子)。
在添加额外的镜像层的同时,镜像始终保持是当前所有镜像的组合,理解这一点非常重要。下图中举了一个简单的例子,每个镜像层包含3个文件,而镜像包含了来自两个镜像层的6个文件。
上图中的镜像层跟之前图中的略有区别,主要目的是便于展示文件。
下图中展示了一个稍微复杂的三层镜像,在外部看来整个镜像只有6个文件,这是因为最上层中的文件7是文件5的一个更新版本。
特点
Docker镜像都是只读的,当容器启动时,一个新的可写层被加载到镜像的顶部!这一层就是我们通常说的容器层,容器之下的都叫镜像层!
如何提交自己的镜像
docker commit 提交一个容器作为一个新的副本
docker commit -m="提交的信息" -a="作者" 容器id 目标镜像名[TAG]
例子:这里就以之前将tomcat
容器为例,我们将webapps.dist
中的文件都拷贝到webapps
中。
#然后收将我们修改过的容器提交为一个新的镜像,以后就可以使用我们修改过后的新的镜像
docker commit -a="phz" -m="add webapps app" 376128031a30 tomcat02:1.0
如果你想要保存当前容器的状态,就可以通过commit来提交,-获得一个镜像,就好比我们以前学习VM时候,快照!
容器数据卷
什么是容器数据卷
docker的理念回顾
将应用和环境打包成一个镜像!
数据?如果数据都在容器中,那么我们容器删除,数据就会丢失,需求∶数据可以持久化
MySQL
,容器删了,删库跑路,需求:MySQL数据可以存储在本地!
容器之间可以有一个数据共享的技术,Docker
容器中产生的数据,同步到本地
这就是卷技术!目录的挂载,将我们容器内的目录,挂载到Linux
上面
总结:容器的持久化和同步操作
使用数据卷
命令
#docker run -it -v 主机目录:容器内部目录 -p
docker run -it -v /home/ceshi:/home centos /bin/bash
容器起来以后就会在本地home
目录下面生成我们的ceshi
文件夹
#查看一下这个容器的挂载情况
docker inspect 9ac9da1b4038
- 来测试一下挂载
此时容器内的
home
文件夹和主机内home\ceshi
文件夹都没有数据,分别使用容器和主机新建文件
此时我们再把容器关掉,并在主机中修改hello文件内容
再启动容器
好处:我们以后修改只需要在本地修改即可,容器内会自动同步!
实验
创建一个mysql
容器
docker run -d -p 9910:3306 -v /home/mysql/conf:/etc/mysql/conf.d -v /home/mysql/data:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name mysql_test mysql:5.7
我们可以挂载多个文件目录,
-e
表示配置环境,这里表示给mysql
配置一个初始密码,在使用navicat
连接测试
可以看到挂载成功了,这里我们创建一个数据库
test
发现挂载文件夹里面也更新了
即使我们把这个容器直接给干掉了,本地保存的文件还依然在
具名和匿名挂载
#匿名挂载
-v 容器内路径
docker run -d -p 9910:80 --name nginx01 -v /etc/nginx nginx
#具名挂载
docker run -d -p 9910:80 --name nginx02 -v juming:/etc/nginx nginx
#查看所有卷(volume)的情况
docker volume ls
查看挂载目录
docker volume inspect juming#如果是匿名挂载就直接写他的volume name就可以了(就是很长的那个像id的东西,但不是id)
所有容器内的卷,如果没有指定目录的话,都是再/var/lib/docker/volumes/xxxxx/_data
下面
拓展
ro #read only,表示这个文件里面只能通过宿主机来修改,容器内部是不允许修改的
rw #read write
docker run -d -p 9910:80 -v juming:/etc/nginx:ro nginx
docker run -d -p 9910:80 -v juming:/etc/nginx:rw nginx
初识DockerFile
Dockerfile
就是用来构建docker
镜像的构建文件,命令脚本。
创建一个
dockerfile
文件,写入以下命令通过这个脚本可以生成镜像,注意都是大写
FROM centos
#匿名挂载
VOLUME ["volume01","volume02"]
CMD echo "------end--------id
CMD /bin/bash
保存退出,开始生成镜像
docker build -f dockerfile01 -t phz_centos .
进入这个镜像
发现这两个生成镜像的时候自动挂载的数据卷目录,那么他一定也会和外部某个目录与之同步
docker inspect ce1133572a72
为什么要在创建镜像的时候就挂载卷呢?
假设我们在构建镜像的时候没有挂载卷,当我们需要创建容器的时候,就必须得一个个手动挂载卷
数据卷容器
- 如果想要多个mysql同步数据呢?
实验:
分别创建三个容器:
#父容器
docker run -it --name docker01 phz_centos
docker run -it --name docker02 --volumes-from docker01 phz_centos
docker run -it --name docker03 --volumes-from docker01 phz_centos
在父容器
docker01
中的数据卷目录创建文件
进入子容器
docker02
和docker03
分别确认以下数据卷有没有同步
只要通过这种方式共享数据卷,无论在父容器还是子容器中的数据卷目录中做什么,三者之间都是同步的
如果这里把
docker01
删除掉,共享的数据卷目录依然存在
- 所以实现两个
mysql
之间数据同步,就可以:
#第一个
docker run -d -p 9910:3306 -v /etc/mysql/conf.d -v /var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name mysql_test01 mysql:5.7
#第二个
docker run -d -p 9910:3306 -v /etc/mysql/conf.d -v /var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 --name mysql_test02 --volumes-from mysql_test01 mysql:5.7
结论:容器之间配置信息的传递,数据卷容器的生命周期一直持续到没有容器去使用它为止(所有依赖于该容器卷的容器都不存在了),但是一旦以持久化方式写入到了本地,本地的数据是不会删除的
Dockerfile
dockerfile介绍
dockerfile
是用来构建dokcer
镜像的文件,命令参数脚本。
构建步骤∶
1、编写一个dockerfile文件
2、docker build
构建成为一个镜像
3、docker run
运行镜像
4、docker push
发布镜像(DockerHub
、阿里云镜像仓库!)
点击该镜像就会进入对应的
GitHub
仓库
所以我们发现,所谓的
docker
镜像,也就是由一个dockerfile
来生成的,所以dockerfile
就类似与一种源代码的味道
- 很多官方镜像都是基础包,很多功能没有,我们通常会搭建自己的镜像
dockerfile构建过程
基础知识
1、每个保留关键字(指令)都是必须是大写字母
2、执行从上到下顺序执行
3、#表示注释
4、每一个指令都会创建提交一个新的镜像层,并提交!
dockerfile
是面向开发的,我们以后要发布项目,做镜像,就需要编写dockerfile
文件。
DockerFile
:构建文件,定义了一切的步骤,源代码
Dockerlmages
:通过DockerFile
构建生成的镜像,最终发布和运行的产品!
dockerfile命令
docker hub
中99%
的镜像都是由这个基础镜像过来的FROM scratch
,然后配置来构建的
FROM #基础镜镜像,一切从这里开始构建
MAINTAINER #镜像是谁写的,姓名+邮箱
RUN #镜像构建的时候需要运行的命令
ADD #步骤:tomcat镜像,这个tomcat压缩包!添加内容
WORKDIR #镜像的工作目录
VOLUME #挂载的目录
EXPOSE #暴露端口配置
CMD #指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代
ENTRYPOINT #指定这个容器启动的时候要运行的命令,可以追加命令
ONBUILD #当构建一个被继承 DockerFile这个时候就会运行ONBUILD的指令。触发指令。
COPY #类似ADD ,将我们文件拷贝到镜像中
ENV #构建的时候设置环境变量!
创建一个自己的centos
创建
mycentos
文件
#基础镜像设置为centos
FROM centos
#填写配置者的信息
MAINTAINER phz<1551402789@qq.com>
#配置一个自己的工作目录
ENV MYPATH /usr/local
WORKDIR $MYPATH
#下载vim编辑器(因为基础镜像里面没有vim),和net-tools,可以执行ifconfig命令
RUN yum -y install vim
RUN yum -y install net-tools
#对外暴露80端口
EXPOSE 80
#输出一些构建信息测试是否构建成功
CMD echo $MYPATH
CMD echo "------end-------"
CMD /bin/bash
通过这个文件构建镜像
docker build -f mycentos -t mycentos:1.0 .
这里本人操作的时候在下载vim的时候速度奇慢,感觉是某个地方没有配置阿里的源,从国外下载的,这里就不深入测试了
CMD和ENTRYPOINT区别
CMD #指定这个容器启动的时候要运行的命令,只有最后一个会生效,可被替代
ENTRYPOINT #指定这个容器启动的时候要运行的命令,可以追加命令
- 这里创建一个
cmd
测试dockerfile
FROM centos
CMD ["ls","-a"]
#构建
docker build -f mycmdtest -t cmdtest .
docker run cmdtest
这个时候如果想给这个容器追加一个命令
docker run cmdtest -l
所以这个地方就发现了cmd
命令是不支持追加的,这里一个-l
就会被视为将原有的ls -a
替换为-l
,而-l
不是一个完整的命令所以会报错,所以可以像下面这样写
- 这里创建一个
ENTRYPOINT
测试镜像
FROM centos
ENTRYPOINT ["ls","-a"]
docker build -f myentrypoint -t entrypointtest .
测试:
Tomcat镜像
先下载jdk
:
wget https://download.oracle.com/otn/java/jdk/8u271-b09/61ae65e088624f5aaa0b1d2d801acb16/jdk-8u271-linux-x64.tar.gz
再下载tomcat
wget https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/tomcat-9/v9.0.40/bin/apache-tomcat-9.0.40.tar.gz
编写
Dockerfile
(官方指定命名方法,这样写的话就不用指定文件名-f
,可以自动识别)
FROM centos
MAINTAINER phz<1551402789@qq.com>
COPY readme.txt /user/localreadme.txt
ADD apache-tomcat-9.0.40.tar.gz /usr/local
ADD jdk-8u271-linux-x64.tar.gz /usr/local
ENV MYPATH /usr/local
WORKDIR $MYPATH
ENV JAVA_HOME /usr/local/jdk1.8.0_271
ENV CLASS_PATH $JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
ENV CATALINA_HOME /usr/local/apache-tomcat-9.0.40
ENV CATALINA_BASE /usr/local/apache-tomcat-9.0.40
ENV PATH $PATH:$JAVA_HOME/bin:$CATALINA_HOME/lib:$CATALINA_HOME/bin
EXPOSE 8080
CMD /usr/local/apache-tomcat-9.0.40/bin/startup.sh && tail -F /usr/local/apache-tomcat-9.0.40/bin/logs/catalina.out
构建容器
docker build -t mytomcat .
开始创建我们的
tomcat
docker run -d -p 9910:8080 --name mytomcat -v /home/dockerfile-test/tomcatDockerfile/test:/usr/local/apache-tomcat-9.0.40/webapps/test -v /home/dockerfile-test/tomcatDockerfile/tomcatlogs:/usl/local/apache-tomcat-9.0.40/logs mytomcat
测试
创建tomcat的时候为什么要挂载我们本地文件夹呢,就是因为这样我们就可以不用进入容器就可以发布项目了
手写一个网站
- 进入这个容器的
tomcat
工作目录,我们之前创建了一个test
文件夹,在里面创建两个文件:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>hello</title>
</head>
<body>
hello word
</body>
</html>
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
</web-app>
- 保存退出即可
将我们的镜像发布
Docker hub
首先当然是注册一个账号
再然后就是登录
docker login -u xxx -p xxx
提交镜像的时候必须保证镜像名称符合规范
#修改镜像tag,dockers hub账号/镜像名称
docker tag demo:latest 19990910/springboot
然后执行
push
指令
docker push 19990910/springboot
阿里云镜像仓库
打开阿里云服务器控制面板
创建命名空间
创建镜像仓库
点击进入
登录阿里云镜像
sudo docker login --username=cestbon_phz registry.cn-beijing.aliyuncs.com
修改符合阿里云镜像仓库命名规则的tag
sudo docker tag cestbon_phz/springboot:latest registry.cn-beijing.aliyuncs.com/phz_images_test/test_01:1.0
执行
push
指令
sudo docker push registry.cn-beijing.aliyuncs.com/phz_images_test/test_01:1.0
小结
Docker网络
开启一个
tomcat
容器,然后进入查看当前容器内部网络信息
docker exec -it mytomcat ip addr
#eth0这个网络地址就是docker自动分配的
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
24: eth0@if25: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
172.17.0.2/16 这里的16表示这32位地址中,高16位作为一个内网,剩下的16位为域内地址
docker network inspect 容器id
我们直接
ping
这个网络地址,看看能不能ping
通
ping 172.17.0.2
我们每启动一个
docker
容器,docker
就会给docker
容器分配一个ip
,我们只要安装了docker
,就会有一个网卡docker0
,我们的docker
采用的是桥接模式,使用的技术是evth-pair
技术
再次查询本机网络信息,发现多了一个网卡,而且这个网卡与
docker
内部获得的网络信息中的网卡很相似,且名称是成对的
再次启动一个新的
tomcat
再次测试本机的网络信息,发现也多了一个
所以我们发现,我们的容器所带来的网卡,都是成对成对的出现的,而原因就是因为evth-pair技术,它就是一对虚拟的网卡,一端连着协议,一端彼此相连
正因为有这个特性,
evth-pair
充当一个桥粱,连接各种虚拟网络设备,openstac
,Docker
容器之间的连接,0VS
的连接,都是使用evth-pair
技术
测试这里的两个
tomcat
是否能够ping
通,结果理所当然的是能啊
总结
为什么docker
要用虚拟的网桥呢?因为转发快,当我们把容器删除后,对应的那一对网桥也就自动删除了
思考一个问题,我们做微服务的时候,某个服务的
ip
更换掉了,我们希望跟他有关系的服务不被影响,直接通过名字访问容器可以吗?
–link
这里启动两个
tomcat
docker run -d -p --name mytomcat tomcat
docker run -d -p --name mytomcat01 tomcat
尝试直接通过名称来
ping
,发现ping
不通,但是我们是可以解决这个问题的
docker exec -it mytomcat ping mytomcat01
[root@iZ2ze5wj5w33v3gyd9kv1bZ ~]# docker exec -it mytomcat ping mytomcat01
ping: mytomcat01: Name or service not known
我们再启动一个
tomcat
镜像
docker run -d -P --name mytomcat02 --link mytomcat01 tomcat
尝试使用
mytomcat02
直接通过名称ping
mytomcat01
,发现是成功的
docker exec -it mytomcat02 ping mytomcat01
但是我们反向
ping
会发现ping
不通,也就是这样配置还只是单向的
探究
mytomcat02
的hosts
文件,发现,我们的mytomcat01
的ip
被写死在里面了,当然是可以ping
通
docker exec -it mytomcat02 cat /etc/hosts
我们再去看一下
01
,自然而然是没有
但是真实开发不建议–link,我们很多地方的自定义网络,不支持docker0,因为它不支持容器名来ping
自定义网络
查看所有的
docker
网络
docker network ls
网络模式
bridge:桥接 docker
(默认,自己定义网络也用桥接)
null:不配置网络
host:和宿主机共享网络
container:docker
容器内网络连通(很少使用)
#以前启动没写--net bridge,因为这是docker默认的方式,即docker0
#docker0特点:默认,不能通过域名访问,--link可以打通连接
docker run --name tomcat01 -P -d --net bridge tomcat
自定义网络测试
#192.168.0.0/16表示最多支持255*255个网络,如果时192.168.0.0/24就只能支持255个网络,192.168.0.1 - 192.168.1.254
#192.168.0.1表示网关地址,通往其他网段的请求都会经过它,除了网络地址,广播地址以外的可用主机地址都可以分配给他
docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet
使用我们自己定义的网络创建
tomcat
容器
docker run -d -P --name mynet-tomcat01 --network mynet tomcat
docker run -d -P --name mynet-tomcat02 --network mynet tomcat
这个时候我们通过
ip
和容器名来分别ping
试试能不能ping
通,结果是可以!
docker exec -it mynet-tomcat01 ping 192.168.0.2
#不使用--link照样能够ping通
docker exec -it mynet-tomcat01 ping mynet-tomcat02
我们自定义的网络docker
都已经帮我们维护好了对应的关系,推荐我们平时这样使用网络
好处∶
redis
-不同的集群使用不同的网络,保证集群是安全和健康的
mysql
-不同的集群使用不同的网络,保证集群是安全和健康的
网络联通
思考
结论自然是不能的,因为他们本身就处在不同的网段中
查看
docker network --help
帮助,发现有个connect
测试
#创建两个docker默认网络的tomcat
docker run -d -P --name docker0-tomcat01 tomcat
docker run -d -P --name docker0-tomcat02 tomcat
测一下吧,虽然肯定不行
打通网络和容器
docker network connect mynet docker0-tomcat01
docker network connect mynet docker0-tomcat02
再次测试
docker exec -it mynet-tomcat01 ping docker0-tomcat01
查看
mynet
网络信息
本质就是将
docker0-tomcat01
和02
放到了mynet
下面,也就是一个容器两个ip,就好比阿里云服务器,一个公网ip
一个内网ip
Redis部署集群实战
停止所有运行的容器
docker stop $(docker ps -aq)
移除之前创建的网络
docker network rm mynet
创建我们的
redis
集群网络
docker network create redis --subnet 172.38.0.0/16
编写
shell
脚本,批量配置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
查看配置文件
修改本地
redis
配置文件,开启集群部署
再次批量拉取
redis
镜像并启动redis
服务
#redis1
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
#redis2
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
#redis3
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
#redis4
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
#redis5
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
#redis6
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
进入
redis
容器配置主从复制,注意redis
镜像没有bash
命令,应该是sh
命令
docker exec -it redis-1 /bin/sh
redis
集群部署连接各个redis
服务
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
输入
yes
完成后连接测试
redis-cli
是单机,要-c
表示集群
redis-cli -c
进入
redis
服务查看集群节点信息
cluster info
cluster nodes
设置一个
key
,发现是13
这个主机节点处理的请求,那么他的从机自然也就能够获取到值
127.0.0.1:6379> set a b
-> Redirected to slot [15495] located at 172.38.0.13:6379
OK
这个时候我们把主机
13
这个主机停了,看看a
这个值能否正常获取
docker stop redis-3
然后我们再次获取以下
a
值,我这个地方是退出命令行重新进入redis-cli -c
的,不退就一直给我提示无法连接到13
,虽然本来就连不上。
所以我们发现这个
a
从14
获取到了,就是因为14
代替了13
成为了主机
再次查看节点信息
发现原来的13出现了故障,但是转移了,14成为master!!
Docker compose
什么是Docker compose
一次性定义,运行多个容器,解决集群部署的时候,大量容器部署的问题。
开发步骤
Using Compose is basically a three-step process:
- Define your app’s environment with a
Dockerfile
so it can be reproduced anywhere.(编写Dockerfile) - Define the services that make up your app in
docker-compose.yml
so they can be run together in an isolated environment.(编写docker-compose.yml配置文件) - Run
docker-compose up
and Compose starts and runs your entire app.(启动docker-compose)
docker-compose.yml文件官方示例
version: "3.8"
#一个集群中的多个服务
services:
#web服务
web:
build: .
ports:
- "5000:5000"
volumes:
- .:/code
- logvolume01:/var/log
#因为需要连接到redis,这里定义了links就需要先启动redis,docker compose自动识别启动顺序
links:
- redis
#redis服务
redis:
image: redis
volumes:
logvolume01: {}
compose
的一些重要概念
- 服务
services
:容器,应用,(web
,redis
,mysql
…) - 项目
project
:一组关联的容器
未来上线项目,不会简单的使用docker上线,肯定是需要docker compose的,但是docker compose还只是单机范围内部署,未来的集群还得docker swarm
安装
官方下载
sudo curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
有时候官方连接下载不是很快,如果下载缓慢,可以使用国内下载地址
curl -L https://get.daocloud.io/docker/compose/releases/download/1.25.5/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
给该文件开启可执行权限
chmod +x /usr/local/bin/docker-compose
查看
docker-compose
版本
docker-compose version
快速体验,官方提供快速开始的
python
使用redis
计数的一个应用
- 第一步,创建测试文件夹
mkdir composetest
cd composetest
- 第二步,编写
python
文件app.py
import time
import redis
from flask import Flask
app = Flask(__name__)
cache = redis.Redis(host='redis', port=6379)
def get_hit_count():
retries = 5
while True:
try:
return cache.incr('hits')
except redis.exceptions.ConnectionError as exc:
if retries == 0:
raise exc
retries -= 1
time.sleep(0.5)
@app.route('/')
def hello():
count = get_hit_count()
return 'Hello World! I have been seen {} times.\n'.format(count)
- 第三步,创建导入的依赖包
requirements.txt
flask
redis
- 第四步,编写一个
Dockerfile
FROM python:3.7-alpine
WORKDIR /code
ENV FLASK_APP=app.py
ENV FLASK_RUN_HOST=0.0.0.0
RUN apk add --no-cache gcc musl-dev linux-headers
COPY requirements.txt requirements.txt
RUN pip install -r requirements.txt
EXPOSE 5000
COPY . .
CMD ["flask", "run"]
- 第五步,创建
docker-compose.yml
配置文件
注意yml文件严格注意格式,":“和”-"后面都必须跟上一个空格,如果语法报错,建议手敲
version: "3.8"
services:
web:
build: .
ports:
- "5000:5000"
redis:
image: "redis:alpine"
- 第五步,开启
docker-compose
,第一次部署这个集群,会下载一些东西,这个过程略微有些缓慢,也就等了一个小时吧
docker-compose up
未完。。。。。。
更多推荐
所有评论(0)